t3-code-android-nightly/packages/client-runtime/src/t3ToolSummary.ts
Julius Marminge 9f61ba6741
feat: agents ask the user for a secret through a private card (#15907)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:14:12 -07:00

412 lines
14 KiB
TypeScript

import type { T3McpToolSummaryAction } from "@t3tools/shared/t3McpToolPresentation";
export interface T3ToolSummaryCall {
readonly input: unknown;
readonly output: unknown;
readonly outcome: "completed" | "failed" | "unfinished";
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function id(value: unknown): string | undefined {
return typeof value === "string" && value.trim() !== "" ? value : undefined;
}
interface ToolResult {
readonly data?: Record<string, unknown>;
readonly failed: boolean;
}
/** Reads the structured/JSON MCP result envelopes retained by the provider adapters. */
function readResult(value: unknown, depth = 0): ToolResult {
if (depth > 4) return { failed: false };
if (typeof value === "string") {
try {
return readResult(JSON.parse(value), depth + 1);
} catch {
return { failed: false };
}
}
if (Array.isArray(value)) {
let data: ToolResult["data"];
let failed = false;
for (const block of value) {
const record = asRecord(block);
const text = record?.text ?? asRecord(record?.content)?.text;
const result = readResult(asRecord(text)?.text ?? text, depth + 1);
data ??= result.data;
failed ||= result.failed;
}
return { ...(data ? { data } : {}), failed };
}
const record = asRecord(value);
if (!record) return { failed: false };
const failed =
record.isError === true ||
record.is_error === true ||
(typeof record._tag === "string" && /(?:Error|Failure)$/.test(record._tag)) ||
record.error != null;
const content = record.structuredContent ?? record.content;
if (content !== undefined) {
const result = readResult(content, depth + 1);
return { ...(result.data ? { data: result.data } : {}), failed: failed || result.failed };
}
return { data: record, failed };
}
function readInput(value: unknown): Record<string, unknown> | undefined {
const input = readResult(value).data;
// Cursor retains its MCP args envelope; other adapters retain the arguments directly.
return input && typeof input.toolName === "string" ? asRecord(input.args) : input;
}
/** MCP errors can be returned as data even when the provider completed the tool call. */
export function t3ToolResultIndicatesFailure(output: unknown): boolean {
return readResult(output).failed;
}
function countEntities(ids: ReadonlyArray<string | undefined>): number {
return (
new Set(ids.filter((value) => value !== undefined)).size +
ids.filter((value) => value === undefined).length
);
}
function quantity(count: number, noun: string, plural = `${noun}s`): string {
return `${count} ${count === 1 ? noun : plural}`;
}
/** Counts successful effects separately from failed or unfinished tool calls. */
export function summarizeT3ToolCalls(
action: T3McpToolSummaryAction,
calls: ReadonlyArray<T3ToolSummaryCall>,
): { label: string; failedCount: number } {
const results = calls.map((call) => {
const result = readResult(call.output);
return {
input: readInput(call.input),
output: result.data,
outcome: result.failed ? ("failed" as const) : call.outcome,
};
});
const completed = results.filter((call) => call.outcome === "completed");
const failedCount = results.filter((call) => call.outcome === "failed").length;
const selected = completed.length > 0 ? completed : results;
const times = quantity(selected.length, "time");
const phrase = (past: string, infinitive: string, object: string) =>
`${completed.length > 0 ? past : `Tried to ${infinitive}`} ${object}`;
const entityIds = (key: string) =>
selected.map((call) => id(call.output?.[key]) ?? id(call.input?.[key]));
const projectIds = selected.map(
(call) => id(call.output?.id) ?? id(call.output?.projectId) ?? id(call.input?.projectId),
);
const threadIds = selected.map(
(call) =>
id(call.output?.threadId) ??
id(asRecord(call.output?.thread)?.threadId) ??
id(call.input?.threadId),
);
let label: string;
switch (action) {
case "thread-send": {
const messages = countEntities(selected.map((call) => id(call.output?.messageId)));
const targetsKnown = threadIds.every((value) => value !== undefined);
const threads = new Set(threadIds).size;
const object = targetsKnown
? messages === threads && messages > 1
? `messages to ${quantity(threads, "thread")}`
: `${quantity(messages, "message")} to ${quantity(threads, "thread")}`
: quantity(messages, "message");
label = phrase("Sent", "send", object);
break;
}
case "thread-create": {
const createdIds: string[] = [];
const resultsKnown =
completed.length > 0 &&
completed.every((call) => {
const threads = Array.isArray(call.output?.threads) ? call.output.threads : [call.output];
return threads.every((thread) => {
const record = asRecord(thread);
if (record?.status === "rolled_back") return true;
const threadId = id(record?.threadId);
if (!threadId) return false;
createdIds.push(threadId);
return true;
});
});
label = resultsKnown
? `Created ${quantity(new Set(createdIds).size, "thread")}`
: `Requested thread creation ${times}`;
break;
}
case "delegate":
label = phrase("Delegated", "delegate", quantity(countEntities(entityIds("taskId")), "task"));
break;
case "thread-read":
case "thread-wait": {
const targets = threadIds.every((value) => value !== undefined)
? quantity(new Set(threadIds).size, "thread")
: `threads ${times}`;
label =
action === "thread-read"
? phrase("Read", "read", targets)
: phrase("Waited on", "wait on", targets);
break;
}
case "thread-list":
label = phrase("Listed", "list", `threads ${times}`);
break;
case "thread-interrupt":
label = phrase(
"Requested interrupts for",
"interrupt",
quantity(countEntities(threadIds), "thread"),
);
break;
case "task-status":
label = phrase("Checked", "check", `task status ${times}`);
break;
case "task-cancel":
label = phrase(
"Requested cancellation of",
"cancel",
quantity(countEntities(entityIds("taskId")), "task"),
);
break;
case "schedule-create":
label = phrase(
"Scheduled",
"schedule",
quantity(countEntities(entityIds("scheduledTaskId")), "task"),
);
break;
case "schedule-list":
label = phrase("Listed", "list", `scheduled tasks ${times}`);
break;
case "schedule-update":
label = phrase(
"Updated",
"update",
quantity(countEntities(entityIds("scheduledTaskId")), "scheduled task"),
);
break;
case "schedule-delete":
// A successful delete can report deleted:false; it still represents a deletion request.
label = phrase(
"Requested deletion of",
"delete",
quantity(countEntities(entityIds("scheduledTaskId")), "scheduled task"),
);
break;
case "schedule-run":
label = phrase("Requested", "request", quantity(selected.length, "scheduled task run"));
break;
case "thread-configuration":
label = phrase("Checked", "check", `thread configuration ${times}`);
break;
case "thread-configure":
label = phrase("Set", "set", `thread model ${times}`);
break;
case "thread-fork":
label = phrase("Requested", "request", quantity(selected.length, "thread fork"));
break;
case "thread-merge":
label = phrase("Requested", "request", quantity(selected.length, "context merge"));
break;
case "thread-search":
label = phrase("Searched", "search", `threads ${times}`);
break;
case "thread-transfers":
label = phrase("Checked", "check", `thread transfers ${times}`);
break;
case "thread-organize":
label = phrase("Organized", "organize", `threads ${times}`);
break;
case "thread-update":
label = phrase("Updated", "update", quantity(countEntities(threadIds), "thread"));
break;
case "queue-list":
label = phrase("Listed", "list", `queued messages ${times}`);
break;
case "queue-read":
label = phrase(
"Read",
"read",
quantity(countEntities(entityIds("queuedRunId")), "queued message"),
);
break;
case "queue-edit":
label = phrase(
"Edited",
"edit",
quantity(countEntities(entityIds("queuedRunId")), "queued message"),
);
break;
case "queue-cancel":
label = phrase(
"Requested cancellation of",
"cancel",
quantity(countEntities(entityIds("queuedRunId")), "queued run"),
);
break;
case "queue-reorder":
label = phrase(
"Reordered",
"reorder",
quantity(countEntities(entityIds("queuedRunId")), "queued run"),
);
break;
case "queue-steer":
label = phrase(
"Requested steering with",
"steer with",
quantity(countEntities(entityIds("queuedRunId")), "queued message"),
);
break;
case "question-list":
label = phrase("Listed", "list", `pending questions ${times}`);
break;
case "question-read":
label = phrase(
"Read",
"read",
quantity(countEntities(entityIds("requestId")), "pending question request"),
);
break;
case "question-respond":
label = phrase(
"Answered",
"answer",
quantity(countEntities(entityIds("requestId")), "pending question request"),
);
break;
case "secret-request":
label = phrase("Asked for", "ask for", quantity(selected.length, "secret"));
break;
case "worktree-handoff":
label = phrase(
"Handed off to",
"hand off to",
quantity(countEntities(entityIds("worktreePath")), "worktree"),
);
break;
case "worktree-list":
label = phrase("Listed", "list", `workspace branches ${times}`);
break;
case "worktree-status":
label = phrase("Checked", "check", `worktree status ${times}`);
break;
case "project-list":
label = phrase("Listed", "list", `projects ${times}`);
break;
case "project-read":
label = phrase("Read", "read", quantity(countEntities(projectIds), "project"));
break;
case "project-create":
label = phrase("Registered", "register", quantity(countEntities(projectIds), "project"));
break;
case "project-update":
label = phrase("Updated", "update", quantity(countEntities(projectIds), "project"));
break;
case "project-delete":
label = phrase("Deleted", "delete", quantity(countEntities(projectIds), "project"));
break;
case "project-clone":
label = phrase(
"Cloned",
"clone",
quantity(countEntities(entityIds("cwd")), "repository", "repositories"),
);
break;
case "environment-read":
label = phrase("Checked", "check", `environment preferences ${times}`);
break;
case "environment-update":
label = phrase("Updated", "update", `environment preferences ${times}`);
break;
case "attachment-prepare":
label = phrase(
"Prepared",
"prepare",
quantity(countEntities(entityIds("attachmentId")), "attachment upload"),
);
break;
case "attachment-discard":
label = phrase(
"Discarded",
"discard",
quantity(countEntities(entityIds("attachmentId")), "pending attachment"),
);
break;
case "attachment-send": {
// A message can contain several attachments. Count retries by message identity.
const messages = new Map<string, (typeof selected)[number]>();
selected.forEach((call, index) =>
messages.set(id(call.output?.messageId) ?? `call-${index}`, call),
);
const attachmentCount = [...messages.values()].reduce(
(count, call) =>
count + (Array.isArray(call.input?.attachments) ? call.input.attachments.length : 0),
0,
);
const countsKnown = [...messages.values()].every(
(call) => Array.isArray(call.input?.attachments) && call.input.attachments.length > 0,
);
const targets = threadIds.every((value) => value !== undefined)
? ` to ${quantity(new Set(threadIds).size, "thread")}`
: "";
label = phrase(
"Sent",
"send",
countsKnown
? `${quantity(attachmentCount, "attachment")}${targets}`
: `attachments${targets} ${times}`,
);
break;
}
case "link-pr":
label = phrase("Linked", "link", quantity(selected.length, "pull request"));
break;
case "unlink-pr":
label = phrase("Unlinked", "unlink", quantity(selected.length, "pull request"));
break;
case "watch-pr":
label = phrase("Watching", "watch", quantity(selected.length, "pull request"));
break;
case "unwatch-pr":
label = phrase(
"Stopped watching",
"stop watching",
quantity(selected.length, "pull request"),
);
break;
case "list-prs":
label = phrase(
"Checked",
"check",
`linked pull requests${selected.length === 1 ? "" : ` ${times}`}`,
);
break;
case "browser":
label = phrase("Used", "use", `browser ${times}`);
break;
case "device":
label = phrase("Used", "use", `device controls ${times}`);
break;
case "html-preview":
label = phrase("Previewed", "preview", quantity(selected.length, "HTML page"));
break;
case "html-render":
label = phrase("Rendered", "render", quantity(selected.length, "HTML page"));
break;
case "capabilities":
label = phrase("Checked", "check", `orchestration capabilities ${times}`);
break;
}
return { label, failedCount };
}