1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
import { z } from "zod";
import type { AgentStatus, ToolDefinition } from "../types/index.js";
import type { TabResolution } from "./send-to-tab.js";
export interface ReadTabCallbacks {
/** Resolve a (possibly short) handle to one open tab. */
resolveShortId(prefix: string): TabResolution;
/**
* Return the target tab's most recent COMPLETED assistant turn as plain
* text, plus its current status. `text` is null when the tab has no
* completed assistant turn yet.
*/
getLastResponse(tabId: string): { text: string | null; status: AgentStatus };
/** Snapshot of currently-open tabs, for "available tabs" error hints. */
listOpenHandles(): Array<{ handle: string; title: string }>;
}
/** Render the "available tabs" hint shared by the none/ambiguous branches. */
function renderOpenHandles(handles: Array<{ handle: string; title: string }>): string {
if (handles.length === 0) return "No other tabs are currently open.";
const lines = handles.map((h) => ` - ${h.handle}: ${h.title}`);
return ["Currently open tabs:", ...lines].join("\n");
}
export function createReadTabTool(callbacks: ReadTabCallbacks): ToolDefinition {
return {
name: "read_tab",
description: [
"Read the most recent completed response from another tab (agent) by its short ID.",
"",
"Returns a SNAPSHOT — it does NOT block or wait for the target to finish.",
" - If the target is idle, you get its just-finished turn.",
" - If the target is still running, you get its PREVIOUS completed turn (if any);",
" call read_tab again later to get the newest one.",
"",
"Use this after send_to_tab to collect another agent's reply. IDs are git-style",
"prefixes: pass any length that uniquely identifies the target (min 4 chars).",
].join("\n"),
parameters: z.object({
tab_id: z
.string()
.describe(
"The short ID (handle) of the tab to read, as shown in the tab bar. Any unique-length prefix works (min 4 chars).",
),
}),
execute: async (args: Record<string, unknown>): Promise<string> => {
const rawId = (args.tab_id as string | undefined)?.trim() ?? "";
if (!rawId) {
return `Error: tab_id is required.\n\n${renderOpenHandles(callbacks.listOpenHandles())}`;
}
const resolution = callbacks.resolveShortId(rawId);
if (resolution.status === "none") {
return [
`Error: no open tab matches the ID "${rawId}".`,
"",
renderOpenHandles(callbacks.listOpenHandles()),
].join("\n");
}
if (resolution.status === "ambiguous") {
const matches = resolution.matches.map((m) => ` - ${m.handle}: ${m.title}`).join("\n");
return [
`Error: the ID "${rawId}" is ambiguous — it matches multiple open tabs:`,
matches,
"",
"Add one or more characters to disambiguate.",
].join("\n");
}
const target = resolution.tab;
const { text, status } = callbacks.getLastResponse(target.id);
const runningNote =
status === "running"
? " (this tab is still running; the response below is its previous completed turn — read again later for the newest)"
: "";
if (text === null) {
const reason =
status === "running"
? "it is still working on its first turn"
: "it has no assistant responses yet";
return `Tab ${target.handle} (${target.title}) has no completed response — ${reason}.`;
}
return [
`<tab_response tab="${target.handle}" status="${status}"${runningNote}>`,
text,
"</tab_response>",
].join("\n");
},
};
}
|