Skip to content
Closed
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
17 changes: 17 additions & 0 deletions packages/client-runtime/src/t3ToolSummary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ describe("summarizeT3ToolCalls", () => {
).toBe("Sent 2 messages to 1 thread");
});

it("reads an MCP OkayOutput string as the tool result", () => {
const result = { messageId: "message-1", threadId: "thread-1" };
expect(
summarizeT3ToolCalls("thread-send", [
completed(
{ toolName: "t3_thread_send", args: { threadId: "thread-1" } },
{
type: "MCP",
tool_name: "t3_thread_send",
server_name: "t3-code",
output: { OkayOutput: JSON.stringify(result) },
},
),
]).label,
).toBe("Sent 1 message to 1 thread");
});

it("excludes failed and unfinished sends even if the provider reports completed", () => {
const calls: T3ToolSummaryCall[] = [
completed({ threadId: "success" }, { messageId: "ok", threadId: "success" }),
Expand Down
12 changes: 12 additions & 0 deletions packages/client-runtime/src/t3ToolSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ interface ToolResult {
readonly failed: boolean;
}

function mcpOkayOutput(record: Record<string, unknown>): string | undefined {
if (record.type !== "MCP") return undefined;
const output = asRecord(record.output);
const okay = output?.OkayOutput;
return typeof okay === "string" && okay.length > 0 ? okay : undefined;
}

function readResult(value: unknown, depth = 0): ToolResult {
if (depth > 4) return { failed: false };
if (typeof value === "string") {
Expand Down Expand Up @@ -49,6 +56,11 @@ function readResult(value: unknown, depth = 0): ToolResult {
record.is_error === true ||
(typeof record._tag === "string" && /(?:Error|Failure)$/.test(record._tag)) ||
record.error != null;
const okay = mcpOkayOutput(record);
if (okay !== undefined) {
const result = readResult(okay, depth + 1);
return { ...(result.data ? { data: result.data } : {}), failed: failed || result.failed };
}
const content = record.structuredContent ?? record.content;
if (content !== undefined) {
const result = readResult(content, depth + 1);
Expand Down
21 changes: 21 additions & 0 deletions packages/shared/src/t3McpToolPresentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,25 @@ describe("resolveT3McpToolPresentation", () => {
).toBe("Delegate a child task");
expect(resolveT3McpToolPresentation("t3-code-thread_opencode2-adapter_not_a_tool")).toBeNull();
});

it("ignores a trailing source annotation after the tool id", () => {
expect(resolveT3McpToolPresentation("t3-code__html_render — source file: page.html")).toEqual({
displayName: "Render an HTML page",
logo: "t3-code",
});
expect(resolveT3McpToolPresentation("t3-code.html_preview — source file: page.html")).toEqual({
displayName: "Preview an HTML page",
logo: "t3-code",
});
expect(
resolveT3McpToolPresentation("mcp__t3-code__html_render — source file: page.html"),
).toEqual({
displayName: "Render an HTML page",
logo: "t3-code",
});
expect(resolveT3McpToolPresentation("t3-code.html_render-v2")).toBeNull();
expect(resolveT3McpToolPresentation("t3-code__html_render-v2")).toBeNull();
expect(resolveT3McpToolPresentation("mcp__t3-code__html_render-v2")).toBeNull();
});

});
15 changes: 10 additions & 5 deletions packages/shared/src/t3McpToolPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ function normalizeT3McpToolLabel(value: string): string {
*/
function resolveT3McpToolName(value: string): string | null {
const label = normalizeT3McpToolLabel(value);
const mcpMatch = /^mcp__(?<server>.+?)__(?<tool>.+)$/i.exec(label);
const mcpMatch = /^mcp__(?<server>.+?)__(?<tool>[A-Za-z0-9_]+)(?=$|[^A-Za-z0-9_-])/i.exec(label);
if (mcpMatch?.groups) {
const { server, tool } = mcpMatch.groups;
return server !== undefined &&
Expand All @@ -358,14 +358,19 @@ function resolveT3McpToolName(value: string): string | null {
: null;
}

const namespaceMatch = /^(?<server>t3-code|t3_code|t3code)(?:[.:/]|\s*·\s*)(?<tool>.+)$/i.exec(
label,
);
const namespaceMatch =
/^(?<server>t3-code|t3_code|t3code)(?:[.:/]|\s*·\s*)(?<tool>[A-Za-z0-9_]+)(?=$|[^A-Za-z0-9_-])/i.exec(
label,
);
if (namespaceMatch?.groups) {
return namespaceMatch.groups.tool ?? null;
}

const prefixed = /^(?:mcp[-_]{1,2})?t3[-_ ]?code(?:__|[-_.:/ ])(?<tool>.+)$/i.exec(label);
// The id ends at the title or at an annotation. A hyphen, as in html_render-v2, is still part of the name.
const prefixed =
/^(?:mcp[-_]{1,2})?t3[-_ ]?code(?:__|[-_.:/ ])(?<tool>[A-Za-z0-9_]+)(?=$|[^A-Za-z0-9_-])/i.exec(
label,
);
const candidate = prefixed?.groups?.tool ?? label;
if (Object.hasOwn(T3_MCP_TOOLS, candidate)) return candidate;
// OpenCode 2 registers one server per thread, `t3-code-<thread>`, and joins
Expand Down
28 changes: 28 additions & 0 deletions packages/shared/src/toolOutput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test";

import {
compactDynamicToolOutput,
htmlRenderFromToolItem,
MAX_TOOL_OUTPUT_IMAGES,
toolOutputImages,
toolOutputIndicatesFailure,
Expand Down Expand Up @@ -132,6 +133,33 @@ describe("compactDynamicToolOutput", () => {
threadId: "known",
});
});

it("reads an MCP OkayOutput string, including a suffixed html_render name", () => {
const reference = { attachmentId: "render-1", title: "Example", height: 120 };
const output = {
type: "MCP",
tool_name: "html_render",
server_name: "t3-code",
output: { OkayOutput: JSON.stringify({ htmlRender: reference, message: "Shown" }) },
};
expect(compactDynamicToolOutput(output)?.htmlRender).toEqual(reference);
expect(
htmlRenderFromToolItem({
toolName: "t3-code__html_render — source file: page.html",
output,
}),
).toEqual(reference);
expect(
compactDynamicToolOutput({
type: "MCP",
isError: true,
output: { OkayOutput: JSON.stringify({ threadId: "thread-1" }) },
}),
).toEqual({ isError: true, threadId: "thread-1" });
expect(
compactDynamicToolOutput({ type: "MCP", output: { OkayOutput: "" }, threadId: "thread-1" }),
).toEqual({ threadId: "thread-1" });
});
});

describe("toolOutputIndicatesFailure", () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/shared/src/toolOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ interface CompactToolOutput {
threads?: Array<{ threadId?: string; status?: "rolled_back" }>;
}

function mcpOkayOutput(value: object): string | undefined {
if (!("type" in value) || value.type !== "MCP" || !("output" in value)) return undefined;
const output = value.output;
if (!Predicate.isObject(output) || !("OkayOutput" in output)) return undefined;
const okay = output.OkayOutput;
return typeof okay === "string" && okay.length > 0 ? okay : undefined;
}

function readResult(value: unknown, budget: ResultReadBudget, depth = 0): ResultEnvelope {
if (depth > MAX_ENVELOPE_DEPTH || budget.remainingNodes-- <= 0) {
budget.exceeded = true;
Expand Down Expand Up @@ -93,6 +101,11 @@ function readResult(value: unknown, budget: ResultReadBudget, depth = 0): Result
value.is_error === true ||
value._tag === "OrchestratorMcpFailure" ||
value.error != null;
const okay = mcpOkayOutput(value);
if (okay !== undefined) {
const nested = readResult(okay, budget, depth + 1);
return { ...nested, failed: failed || nested.failed };
}
const content = value.structuredContent ?? value.content;
if (content !== undefined) {
const nested = readResult(content, budget, depth + 1);
Expand Down
Loading