-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathtool-schema.ts
More file actions
484 lines (455 loc) · 21.2 KB
/
tool-schema.ts
File metadata and controls
484 lines (455 loc) · 21.2 KB
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
import { Type } from "typebox";
export const TOOL_NAME = "interactive_shell";
export const TOOL_LABEL = "Interactive Shell";
export const TOOL_DESCRIPTION = `Run an interactive CLI coding agent in an overlay.
Use this ONLY for delegating tasks to other AI coding agents (Claude Code, Cursor CLI, Gemini CLI, Codex, etc.) that have their own TUI and benefit from user interaction.
DO NOT use this for regular bash commands - use the standard bash tool instead.
MODES:
- interactive (default): User supervises and controls the session
- hands-free: Agent monitors with periodic updates, user can take over anytime by typing
- dispatch: Agent is notified on completion via triggerTurn (no polling needed)
- monitor: Run in background and wake the agent on structured monitor events (stream, poll-diff, or file-watch)
RECOMMENDED DEFAULT FOR DELEGATED TASKS:
- For fire-and-forget delegations and QA-style checks, prefer mode="dispatch".
- Dispatch is the safest choice when the agent should continue immediately and be notified automatically on completion.
The user will see the process in an overlay. They can:
- Watch output in real-time
- Scroll through output (Shift+Up/Down)
- Transfer output to you (Ctrl+T) - closes overlay and sends output as your context
- Background (Ctrl+B) - dismiss overlay, keep process running
- Detach (Ctrl+Q) for menu: transfer/background/kill
- In hands-free mode: type anything to take over control
HANDS-FREE MODE (NON-BLOCKING):
When mode="hands-free", the tool returns IMMEDIATELY with a sessionId.
The overlay opens for the user to watch, but you (the agent) get control back right away.
Workflow:
1. Start session: interactive_shell({ command: 'pi "Fix bugs"', mode: "hands-free" })
-> Returns immediately with sessionId
2. Check status/output: interactive_shell({ sessionId: "calm-reef" })
-> Returns current status and any new output since last check
3. When task is done: interactive_shell({ sessionId: "calm-reef", kill: true })
-> Kills session and returns final output
The user sees the overlay and can:
- Watch output in real-time
- Take over by typing (you'll see "user-takeover" status on next query)
- Kill/background via Ctrl+Q
QUERYING SESSION STATUS:
- interactive_shell({ sessionId: "calm-reef" }) - get status + rendered terminal output (default: 20 lines, 5KB)
- interactive_shell({ sessionId: "calm-reef", outputLines: 50 }) - get more lines (max: 200)
- interactive_shell({ sessionId: "calm-reef", outputMaxChars: 20000 }) - get more content (max: 50KB)
- interactive_shell({ sessionId: "calm-reef", outputOffset: 0, outputLines: 50 }) - pagination (lines 0-49)
- interactive_shell({ sessionId: "calm-reef", incremental: true }) - get next N unseen lines (server tracks position)
- interactive_shell({ sessionId: "calm-reef", drain: true }) - only NEW output since last query (raw stream)
- interactive_shell({ sessionId: "calm-reef", kill: true }) - end session
- interactive_shell({ sessionId: "calm-reef", input: "..." }) - send input
- interactive_shell({ monitorStatus: true, monitorSessionId: "calm-reef" }) - query monitor lifecycle/state
- interactive_shell({ monitorEvents: true, monitorSessionId: "calm-reef" }) - query monitor event history
- interactive_shell({ monitorEvents: true, monitorSessionId: "calm-reef", monitorSinceEventId: 42 }) - fetch events after a cursor
- interactive_shell({ monitorEvents: true, monitorSessionId: "calm-reef", monitorTriggerId: "error" }) - filter monitor history by trigger id
- interactive_shell({ monitorEvents: true, monitorSessionId: "calm-reef", monitorEventLimit: 50, monitorEventOffset: 20 }) - paginate monitor history
IMPORTANT: Don't query too frequently! Wait 30-60 seconds between status checks.
The user is watching the overlay in real-time - you're just checking in periodically.
RATE LIMITING:
Queries are limited to once every 60 seconds (configurable). If you query too soon,
the tool will automatically wait until the limit expires before returning.
SENDING INPUT:
- interactive_shell({ sessionId: "calm-reef", input: "/help", submit: true }) - type text and press Enter
- interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] }) - named keys
- interactive_shell({ sessionId: "calm-reef", inputKeys: ["up", "up", "enter"] }) - multiple keys
- interactive_shell({ sessionId: "calm-reef", inputHex: ["0x1b", "0x5b", "0x41"] }) - raw escape sequences
- interactive_shell({ sessionId: "calm-reef", inputPaste: "multiline\\ntext" }) - bracketed paste (prevents auto-execution)
Named keys for inputKeys: up, down, left, right, enter, escape, tab, backspace, ctrl+c, ctrl+d, etc.
Modifiers: ctrl+x, alt+x, shift+tab, ctrl+alt+delete (or c-x, m-x, s-tab syntax)
For editor-based TUIs like pi, raw \`input\` only types text. It does NOT submit by itself. Prefer \`submit: true\` or \`inputKeys: ["enter"]\` instead of relying on \`\\n\`.
TIMEOUT (for TUI commands that don't exit cleanly):
Use timeout to auto-kill after N milliseconds. Useful for capturing output from commands like "pi --help":
- interactive_shell({ command: "pi --help", mode: "hands-free", timeout: 5000 })
DISPATCH MODE (NON-BLOCKING, NO POLLING):
When mode="dispatch", the tool returns IMMEDIATELY with a sessionId.
You do NOT need to poll. You'll be notified automatically when the session completes.
Workflow:
1. Start session: interactive_shell({ command: 'pi "Fix bugs"', mode: "dispatch" })
-> Returns immediately with sessionId
2. Do other work - no polling needed
3. When complete, you receive a notification with the session output
Dispatch defaults autoExitOnQuiet to true (opt-out with handsFree.autoExitOnQuiet: false).
You can still query with sessionId if needed, but it's not required.
BACKGROUND DISPATCH (HEADLESS):
Start a session without any overlay. Process runs headlessly, agent notified on completion:
- interactive_shell({ command: 'pi "fix bugs"', mode: "dispatch", background: true })
MONITOR MODE (EVENT-DRIVEN, HEADLESS):
Run a background process and wake the agent on structured monitor triggers:
- interactive_shell({ command: 'npm test --watch', mode: "monitor", monitor: { strategy: "stream", triggers: [{ id: "fail", literal: "FAIL" }] } })
- interactive_shell({ command: 'npm run dev', mode: "monitor", monitor: { strategy: "stream", triggers: [{ id: "warn", regex: "/error|warn/i" }] } })
- interactive_shell({ command: 'curl -sf http://localhost:3000/health', mode: "monitor", monitor: { strategy: "poll-diff", triggers: [{ id: "changed", regex: "/./" }], poll: { intervalMs: 5000 } } })
- interactive_shell({ mode: "monitor", monitor: { strategy: "file-watch", fileWatch: { path: "./uploads", recursive: true, events: ["rename", "change"] }, triggers: [{ id: "pdf", regex: "/\\.pdf$/i" }] } })
AGENT-INITIATED BACKGROUND:
Dismiss an existing overlay, keep the process running in background:
- interactive_shell({ sessionId: "calm-reef", background: true })
ATTACH (REATTACH TO BACKGROUND SESSION):
Open an overlay for a background session:
- interactive_shell({ attach: "calm-reef" }) - interactive (blocking)
- interactive_shell({ attach: "calm-reef", mode: "hands-free" }) - hands-free (poll)
- interactive_shell({ attach: "calm-reef", mode: "dispatch" }) - dispatch (non-blocking, notified)
LIST BACKGROUND SESSIONS:
- interactive_shell({ listBackground: true })
DISMISS BACKGROUND SESSIONS:
- interactive_shell({ dismissBackground: true }) - kill running, remove exited, clear all
- interactive_shell({ dismissBackground: "calm-reef" }) - dismiss specific session
When using raw \`command\`, this tool does NOT inject prompts for you.
If you want to start with a prompt, include it in the command using the CLI's own prompt form.
Structured \`spawn\` also supports a \`prompt\` field for Pi, Codex, Claude, and Cursor using their native startup prompt forms.
Examples:
- pi "Scan the current codebase"
- claude "Check the current directory and summarize"
- interactive_shell({ spawn: { agent: "codex" }, mode: "dispatch" })
- interactive_shell({ spawn: { agent: "cursor", prompt: "Review the diffs" }, mode: "dispatch" })
- interactive_shell({ spawn: { agent: "claude", prompt: "Review the diffs" }, mode: "dispatch" })
- interactive_shell({ spawn: { mode: "fork" } }) // pi-only fork of the current persisted session
- gemini (interactive, idle)
- aider --yes-always (hands-free, auto-approve)
- pi --help (with timeout: 5000 to capture help output)`;
export const toolParameters = Type.Object({
command: Type.Optional(
Type.String({
description: "The raw CLI command to run (e.g., 'pi \"Fix the bug\"'). Use this for arbitrary CLIs. Mutually exclusive with 'spawn'.",
}),
),
spawn: Type.Optional(
Type.Object({
agent: Type.Optional(Type.Union([
Type.Literal("pi"),
Type.Literal("codex"),
Type.Literal("claude"),
Type.Literal("cursor"),
], {
description: "Spawn agent to launch. Defaults to the configured spawn.defaultAgent.",
})),
mode: Type.Optional(Type.Union([
Type.Literal("fresh"),
Type.Literal("fork"),
], {
description: "Spawn mode. 'fork' is only supported for pi and requires a persisted current session.",
})),
worktree: Type.Optional(Type.Boolean({
description: "Launch in a separate git worktree. Defaults to spawn.worktree from config.",
})),
prompt: Type.Optional(Type.String({
description: "Optional startup prompt for pi, codex, claude, or cursor. Uses each CLI's native prompt-bearing startup form.",
})),
}, {
description: "Structured spawn request for pi, codex, claude, or cursor. Use this instead of building the command string manually when you want the extension's spawn defaults, Pi-only fork behavior, worktree support, or native startup prompts.",
}),
),
sessionId: Type.Optional(
Type.String({
description: "Session ID to interact with an existing hands-free session",
}),
),
kill: Type.Optional(
Type.Boolean({
description: "Kill the session (requires sessionId). Use when task appears complete.",
}),
),
outputLines: Type.Optional(
Type.Number({
description: "Number of lines to return when querying (default: 20, max: 200)",
}),
),
outputMaxChars: Type.Optional(
Type.Number({
description: "Max chars to return when querying (default: 5KB, max: 50KB)",
}),
),
outputOffset: Type.Optional(
Type.Number({
description: "Line offset for pagination (0-indexed). Use with outputLines to read specific ranges.",
}),
),
drain: Type.Optional(
Type.Boolean({
description: "If true, return only NEW output since last query (raw stream). More token-efficient for repeated polling.",
}),
),
incremental: Type.Optional(
Type.Boolean({
description: "If true, return next N lines not yet seen. Server tracks position - just keep calling to paginate through output.",
}),
),
settings: Type.Optional(
Type.Object({
updateInterval: Type.Optional(
Type.Number({ description: "Change max update interval for existing session (ms)" }),
),
quietThreshold: Type.Optional(
Type.Number({ description: "Change quiet threshold for existing session (ms)" }),
),
}),
),
input: Type.Optional(
Type.String({ description: "Raw text to send to the session (requires sessionId). This only types the text; it does not submit it. Use submit=true or inputKeys:['enter'] when you want to press Enter." }),
),
submit: Type.Optional(
Type.Boolean({ description: "Press Enter after sending any input. Prefer this when submitting slash commands or prompts to editor-based TUIs like pi. (requires sessionId)" }),
),
inputKeys: Type.Optional(
Type.Array(Type.String(), {
description: "Named keys with modifier support: up, down, enter, ctrl+c, alt+x, shift+tab, ctrl+alt+delete, etc. (requires sessionId)",
}),
),
inputHex: Type.Optional(
Type.Array(Type.String(), {
description: "Hex bytes to send as raw escape sequences (e.g., ['0x1b', '0x5b', '0x41'] for ESC[A). (requires sessionId)",
}),
),
inputPaste: Type.Optional(
Type.String({
description: "Text to paste with bracketed paste mode - prevents shells from auto-executing multiline input. (requires sessionId)",
}),
),
cwd: Type.Optional(
Type.String({
description: "Working directory for the command",
}),
),
name: Type.Optional(
Type.String({
description: "Optional session name (used for session IDs)",
}),
),
reason: Type.Optional(
Type.String({
description:
"Brief explanation shown in the overlay header only (not passed to the subprocess)",
}),
),
mode: Type.Optional(
Type.Union([
Type.Literal("interactive"),
Type.Literal("hands-free"),
Type.Literal("dispatch"),
Type.Literal("monitor"),
], {
description: "Mode: 'interactive' (default, user controls), 'hands-free' (agent monitors, user can take over), 'dispatch' (agent notified on completion, no polling needed), or 'monitor' (headless structured event monitor with stream/poll-diff/file-watch strategies).",
}),
),
monitor: Type.Optional(
Type.Object({
strategy: Type.Optional(Type.Union([
Type.Literal("stream"),
Type.Literal("poll-diff"),
Type.Literal("file-watch"),
], {
description: "Monitor strategy. stream = line-based trigger matching. poll-diff = periodic snapshot diffing. file-watch = first-class filesystem watch events.",
})),
triggers: Type.Array(Type.Object({
id: Type.String({ description: "Unique trigger id used in emitted event payloads." }),
literal: Type.Optional(Type.String({ description: "Literal substring trigger." })),
regex: Type.Optional(Type.String({ description: "Regex trigger string. Supports /pattern/flags format." })),
cooldownMs: Type.Optional(Type.Number({ description: "Optional per-trigger cooldown window in ms." })),
threshold: Type.Optional(Type.Object({
captureGroup: Type.Number({ description: "Regex capture group index parsed as number (requires regex matcher)." }),
op: Type.Union([
Type.Literal("lt"),
Type.Literal("lte"),
Type.Literal("gt"),
Type.Literal("gte"),
], { description: "Threshold operator." }),
value: Type.Number({ description: "Threshold numeric value." }),
})),
}), {
description: "Named trigger definitions. Each trigger must define exactly one matcher: literal or regex.",
}),
fileWatch: Type.Optional(Type.Object({
path: Type.String({ description: "Path to watch for strategy='file-watch'. Relative paths resolve from cwd." }),
recursive: Type.Optional(Type.Boolean({ description: "Watch subdirectories recursively (platform-dependent support)." })),
events: Type.Optional(Type.Array(Type.Union([
Type.Literal("rename"),
Type.Literal("change"),
]), { description: "Filesystem event names to emit." })),
})),
poll: Type.Optional(Type.Object({
intervalMs: Type.Optional(Type.Number({ description: "Poll interval in ms for strategy='poll-diff' (default: 5000)." })),
})),
persistence: Type.Optional(Type.Object({
stopAfterFirstEvent: Type.Optional(Type.Boolean({ description: "Stop monitor after first emitted event." })),
maxEvents: Type.Optional(Type.Number({ description: "Maximum emitted events before monitor stops." })),
})),
throttle: Type.Optional(Type.Object({
dedupeExactLine: Type.Optional(Type.Boolean({ description: "Suppress repeated exact line/diff payloads (default: true)." })),
cooldownMs: Type.Optional(Type.Number({ description: "Optional global cooldown in ms across triggers." })),
})),
detector: Type.Optional(Type.Object({
detectorCommand: Type.String({ description: "External detector command. Receives JSON candidate event on stdin and returns JSON decision on stdout." }),
timeoutMs: Type.Optional(Type.Number({ description: "Detector command timeout in ms (default: 3000)." })),
})),
}, {
description: "Structured monitor configuration required when mode='monitor'.",
}),
),
background: Type.Optional(
Type.Boolean({
description: "Run without overlay (with mode='dispatch' or mode='monitor') or dismiss existing overlay (with sessionId). Process runs in background, user can /attach.",
}),
),
attach: Type.Optional(
Type.String({
description: "Background session ID to reattach. Opens overlay with the specified mode.",
}),
),
listBackground: Type.Optional(
Type.Boolean({
description: "List all background sessions.",
}),
),
dismissBackground: Type.Optional(
Type.Union([Type.Boolean(), Type.String()], {
description: "Dismiss background sessions. true = all, string = specific session ID. Kills running sessions, removes exited ones.",
}),
),
monitorStatus: Type.Optional(
Type.Boolean({
description: "Query monitor lifecycle/state summary. Requires monitorSessionId or sessionId.",
}),
),
monitorEvents: Type.Optional(
Type.Boolean({
description: "Query structured monitor event history instead of session output. Requires monitorSessionId or sessionId.",
}),
),
monitorSessionId: Type.Optional(
Type.String({
description: "Target monitor session for monitorStatus/monitorEvents queries.",
}),
),
monitorEventLimit: Type.Optional(
Type.Number({
description: "Max monitor events to return (default: 20).",
}),
),
monitorEventOffset: Type.Optional(
Type.Number({
description: "How many newest monitor events to skip before returning results (default: 0).",
}),
),
monitorSinceEventId: Type.Optional(
Type.Number({
description: "Only return monitor events with eventId greater than this cursor.",
}),
),
monitorTriggerId: Type.Optional(
Type.String({
description: "Filter monitor events to a specific trigger id.",
}),
),
handsFree: Type.Optional(
Type.Object({
updateMode: Type.Optional(
Type.String({
description: "Update mode: 'on-quiet' (default, emit when output stops) or 'interval' (emit on fixed schedule)",
}),
),
updateInterval: Type.Optional(
Type.Number({ description: "Max interval between updates in ms (default: 60000)" }),
),
quietThreshold: Type.Optional(
Type.Number({ description: "Silence duration before emitting update in on-quiet mode (default: 8000ms)" }),
),
gracePeriod: Type.Optional(
Type.Number({ description: "Startup grace period before autoExitOnQuiet can kill the session (default: 15000ms)" }),
),
updateMaxChars: Type.Optional(
Type.Number({ description: "Max chars per update (default: 1500)" }),
),
maxTotalChars: Type.Optional(
Type.Number({ description: "Total char budget for all updates (default: 100000). Updates stop including content when exhausted." }),
),
autoExitOnQuiet: Type.Optional(
Type.Boolean({
description: "Auto-kill session when output stops (after quietThreshold). Defaults to false. Set to true for fire-and-forget single-task delegations.",
}),
),
}),
),
handoffPreview: Type.Optional(
Type.Object({
enabled: Type.Optional(Type.Boolean({ description: "Include last N lines in tool result details" })),
lines: Type.Optional(Type.Number({ description: "Tail lines to include (default from config)" })),
maxChars: Type.Optional(
Type.Number({ description: "Max chars to include in tail preview (default from config)" }),
),
}),
),
handoffSnapshot: Type.Optional(
Type.Object({
enabled: Type.Optional(Type.Boolean({ description: "Write a transcript snapshot on detach/exit" })),
lines: Type.Optional(Type.Number({ description: "Tail lines to capture (default from config)" })),
maxChars: Type.Optional(Type.Number({ description: "Max chars to write (default from config)" })),
}),
),
timeout: Type.Optional(
Type.Number({
description: "Auto-kill process after N milliseconds. Useful for TUI commands that don't exit cleanly (e.g., 'pi --help')",
}),
),
});
/** Parsed tool parameters type */
export interface ToolParams {
command?: string;
spawn?: { agent?: "pi" | "codex" | "claude" | "cursor"; mode?: "fresh" | "fork"; worktree?: boolean; prompt?: string };
sessionId?: string;
kill?: boolean;
outputLines?: number;
outputMaxChars?: number;
outputOffset?: number;
drain?: boolean;
incremental?: boolean;
settings?: { updateInterval?: number; quietThreshold?: number };
input?: string;
submit?: boolean;
inputKeys?: string[];
inputHex?: string[];
inputPaste?: string;
cwd?: string;
name?: string;
reason?: string;
mode?: "interactive" | "hands-free" | "dispatch" | "monitor";
background?: boolean;
monitor?: {
strategy?: "stream" | "poll-diff" | "file-watch";
triggers: Array<{
id: string;
literal?: string;
regex?: string;
cooldownMs?: number;
threshold?: { captureGroup: number; op: "lt" | "lte" | "gt" | "gte"; value: number };
}>;
fileWatch?: { path: string; recursive?: boolean; events?: Array<"rename" | "change"> };
poll?: { intervalMs?: number };
persistence?: { stopAfterFirstEvent?: boolean; maxEvents?: number };
throttle?: { dedupeExactLine?: boolean; cooldownMs?: number };
detector?: { detectorCommand: string; timeoutMs?: number };
};
attach?: string;
listBackground?: boolean;
dismissBackground?: boolean | string;
monitorStatus?: boolean;
monitorEvents?: boolean;
monitorSessionId?: string;
monitorEventLimit?: number;
monitorEventOffset?: number;
monitorSinceEventId?: number;
monitorTriggerId?: string;
handsFree?: {
updateMode?: "on-quiet" | "interval";
updateInterval?: number;
quietThreshold?: number;
gracePeriod?: number;
updateMaxChars?: number;
maxTotalChars?: number;
autoExitOnQuiet?: boolean;
};
handoffPreview?: { enabled?: boolean; lines?: number; maxChars?: number };
handoffSnapshot?: { enabled?: boolean; lines?: number; maxChars?: number };
timeout?: number;
}