Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/htmlRender/HtmlRender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ describe("HtmlRender", () => {
const reused = yield* htmlRender.publish({ threadId, ...page });
yield* mcpSessions.set({ ...session, browserToolsAvailable: true });
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toEqual(reused);
const first = yield* htmlRender.publish({ threadId, ...page });
const second = yield* htmlRender.publish({ threadId, ...page });
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toEqual(first);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toEqual(second);
expect(yield* mcpSessions.takeHtmlRender(threadId, page)).toBe(undefined);

yield* htmlRender.publish({ threadId, ...page });
yield* mcpSessions.set({
Expand Down
141 changes: 141 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { TestClock } from "effect/testing";
import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
import { htmlRenderFromToolItem } from "@t3tools/shared/toolOutput";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
import {
Expand Down Expand Up @@ -3059,6 +3060,146 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => {
}).pipe(Effect.scoped),
);

it.effect("shows each page a Code Mode script published, whatever the script returned", () =>
Effect.gen(function* () {
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
yield* Effect.addFinalizer(() => mcpSessions.clear(threadId));
const server = "t3-code-thread_opencode2-adapter";
const pages = [
{ html: "<p>orbits</p>", title: "Orbits", height: 480 },
{ html: "<p>scale</p>", title: "Scale", height: 1490 },
];
const references = pages.map((page, index) => ({
attachmentId: `thread_opencode2-adapter-0000000${index}-0000-4000-8000-000000000000-html`,
title: page.title,
height: page.height,
}));
yield* Effect.forEach(pages, (page, index) =>
mcpSessions.recordHtmlRender(threadId, page, references[index]!),
);
const tool = { sessionID: SESSION, assistantMessageID: "msg_assistant", id: "call-execute" };
const { runtime, thread } = yield* resumed([
out("session.prompt", { sessionID: SESSION, text: "<any>" }),
promptAccepted,
event("session.tool.input.started", { ...tool, name: "execute" }),
event("session.tool.called", { ...tool, input: { code: "<script>" }, executed: true }),
event(
"session.tool.success",
{
...tool,
content: [{ type: "text", text: '"done"' }],
metadata: {
toolCalls: [
{ tool: "search", status: "completed", input: { query: "html" } },
{
tool: `${server}.html_preview`,
status: "completed",
input: { html: pages[0]!.html, width: 728 },
},
{ tool: `${server}.html_render`, status: "completed", input: pages[0] },
{ tool: "t3-code-thread_other.html_render", status: "completed", input: pages[1] },
{ tool: `${server}.html_render`, status: "completed", input: pages[1] },
],
truncated: false,
},
executed: true,
},
undefined,
2,
),
event("session.execution.succeeded", { sessionID: SESSION }),
]);
const events = yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
Effect.forkScoped,
);
yield* runtime.startTurn(turnInput(thread));
const shown = (yield* Fiber.join(events)).flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "dynamic_tool"
? [htmlRenderFromToolItem(event.turnItem)].filter((page) => page !== undefined)
: [],
);
assert.deepEqual(shown, references);
}).pipe(Effect.scoped),
);

it.effect("keeps a script replayed after a reconnect from taking a later script's page", () =>
Effect.gen(function* () {
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
yield* Effect.addFinalizer(() => mcpSessions.clear(threadId));
const page = { html: "<p>orbits</p>", title: "Orbits", height: 480 };
const references = [0, 1].map((index) => ({
attachmentId: `thread_opencode2-adapter-0000000${index}-0000-4000-8000-000000000000-html`,
title: page.title,
height: page.height,
}));
yield* Effect.forEach(references, (reference) =>
mcpSessions.recordHtmlRender(threadId, page, reference),
);
const metadata = {
toolCalls: [
{
tool: "t3-code-thread_opencode2-adapter.html_render",
status: "completed",
input: page,
},
],
truncated: false,
};
const content = [{ type: "text", text: '"done"' }];
const script = (id: string) => {
const tool = { sessionID: SESSION, assistantMessageID: "msg_assistant_gap", id };
return [
event("session.tool.input.started", { ...tool, name: "execute" }),
event("session.tool.called", { ...tool, input: { code: "<script>" }, executed: true }),
event(
"session.tool.success",
{ ...tool, content, metadata, executed: true },
undefined,
2,
),
];
};
const { runtime, thread } = yield* resumed([
out("session.prompt", { sessionID: SESSION, id: PROMPT_ID, text: "<any>" }),
promptAccepted,
...script("call-first"),
...reconnected({ [SESSION]: { type: "running" } }, undefined, {
content: [
{
type: "tool",
id: "call-first",
name: "execute",
executed: true,
state: { status: "completed", input: { code: "<script>" }, content, metadata },
time: { created: 2, ran: 2, completed: 2 },
},
],
}),
...openRequests([]),
...script("call-second"),
event("session.execution.succeeded", { sessionID: SESSION }),
]);
const events = yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
Effect.forkScoped,
);
yield* runtime.startTurn(turnInput(thread));
const shown = new Map(
(yield* Fiber.join(events)).flatMap((event) => {
if (event.type !== "turn_item.updated" || event.turnItem.type !== "dynamic_tool") {
return [];
}
const shownPage = htmlRenderFromToolItem(event.turnItem);
return shownPage === undefined ? [] : [[event.turnItem.id, shownPage] as const];
}),
);
assert.deepEqual([...shown.values()], references);
}).pipe(Effect.scoped),
);

it.effect("stores a shell command that exits non-zero as failed", () =>
Effect.gen(function* () {
const tool = { sessionID: SESSION, assistantMessageID: "msg_assistant", id: "call-exit" };
Expand Down
4 changes: 4 additions & 0 deletions packages/provider-core/src/server/McpProviderSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ const matchingPageIndex = (kept: ReadonlyArray<KeptHtmlRender>, input: PageInput
? [index]
: [],
);
// Identical pages are interchangeable, so repeats are taken in publish order.
if (matches.length > 1 && matches.every((index) => kept[index]!.htmlHash === htmlHash)) {
return matches[0]!;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
return matches.length === 1 ? matches[0]! : -1;
};

Expand Down
21 changes: 21 additions & 0 deletions packages/provider-opencode/src/server/toolItems.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OrchestrationV2TurnItem } from "@t3tools/contracts";
import { HTML_RENDER_TOOL_NAME } from "@t3tools/shared/htmlRender";
import { resolveT3McpToolId } from "@t3tools/shared/t3McpToolPresentation";
import { formatReadToolLabel, formatSearchToolLabel } from "@t3tools/shared/toolActivity";
import {
Expand Down Expand Up @@ -122,6 +123,26 @@ export function openCodeCommandFailed(toolName: string, completedMetadata: unkno
);
}

/** Code Mode reports nested calls with their input but no result, so pages are found by input. */
export function openCodeCodeModeHtmlRenders(
completedMetadata: unknown,
mcpServerName: string,
): ReadonlyArray<{ readonly index: number; readonly input: Record<string, unknown> }> {
if (typeof completedMetadata !== "object" || completedMetadata === null) return [];
const toolCalls = (completedMetadata as Record<string, unknown>)["toolCalls"];
if (!Array.isArray(toolCalls)) return [];
return toolCalls.flatMap((call: unknown, index) => {
if (typeof call !== "object" || call === null) return [];
const { tool, status, input } = call as Record<string, unknown>;
return tool === `${mcpServerName}.${HTML_RENDER_TOOL_NAME}` &&
status === "completed" &&
typeof input === "object" &&
input !== null
? [{ index, input: input as Record<string, unknown> }]
: [];
});
}

export function openCodeToolTurnItem(
base: ToolItemBase,
tool: {
Expand Down
34 changes: 33 additions & 1 deletion packages/provider-opencode/src/server/v2/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderS
import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
import { t3OrchestrationSystemPrompt } from "@t3tools/provider-core/server/orchestrationInstructions";
import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";
import { HTML_RENDER_TOOL_NAME } from "@t3tools/shared/htmlRender";
import { compareSemverVersions } from "@t3tools/shared/semver";
import * as KeyedLock from "@t3tools/shared/KeyedLock";
import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";
Expand Down Expand Up @@ -91,7 +92,11 @@ import {
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import { turnScopedSelectionTransition } from "@t3tools/provider-core/server/selectionTransition";
import { OPENCODE_PROVIDER, openCodePermissionRequestKind } from "../adapter.ts";
import { openCodeCommandFailed, openCodeToolTurnItem } from "../toolItems.ts";
import {
openCodeCodeModeHtmlRenders,
openCodeCommandFailed,
openCodeToolTurnItem,
} from "../toolItems.ts";

const NativeGoal = Schema.NullOr(
Schema.Struct({
Expand Down Expand Up @@ -255,6 +260,8 @@ interface ActiveTurn {
unsent: boolean;
readonly steers: Set<string>;
readonly settledInbox: Set<string>;
// Reconnects replay finished tools, so each nested html_render takes a page only once.
readonly takenPages: Set<string>;
heldEnd: TurnTerminal | undefined;
awaitingStart: boolean;
/** Scope child tool IDs because model-generated IDs can repeat parent IDs. */
Expand Down Expand Up @@ -1184,6 +1191,30 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr
},
),
});
if (status === "running") return;
const pages = openCodeCodeModeHtmlRenders(
result?.metadata,
yield* mcpServerNameFor(turn.input.threadId),
);
for (const { index, input } of pages) {
const pageId = `${nativeId}:${HTML_RENDER_TOOL_NAME}:${index}`;
if (turn.takenPages.has(pageId)) continue;
turn.takenPages.add(pageId);
const page = yield* mcpSessions.takeHtmlRender(turn.input.threadId, input);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (page === undefined) continue;
yield* emitNode(state, turn, pageId, "tool_call", "completed", startedAt, updatedAt);
yield* emit({
type: "turn_item.updated",
driver,
turnItem: {
...itemBase(state, turn, pageId, "completed", startedAt, updatedAt, updatedAt),
type: "dynamic_tool",
toolName: `t3-code.${HTML_RENDER_TOOL_NAME}`,
input: { title: page.title, height: page.height },
output: { htmlRender: page },
},
});
}
});

const emitCompaction = Effect.fnUntraced(function* (
Expand Down Expand Up @@ -1319,6 +1350,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr
unsent: false,
steers: new Set(),
settledInbox: new Set(),
takenPages: new Set(),
heldEnd: undefined,
awaitingStart: options.awaitingStart,
scope: options.scope,
Expand Down
Loading