agent-sh 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -30
- package/dist/agent/agent-loop.d.ts +43 -6
- package/dist/agent/agent-loop.js +817 -157
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +364 -151
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +84 -3
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +10 -13
- package/dist/agent/token-budget.js +6 -46
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +1 -2
- package/dist/context-manager.d.ts +16 -19
- package/dist/context-manager.js +48 -152
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -3
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +75 -17
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +72 -50
- package/dist/extensions/index.js +0 -2
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +67 -80
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +39 -16
- package/dist/settings.js +51 -11
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +84 -76
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +15 -0
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +69 -8
- package/dist/utils/box-frame.js +8 -2
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/markdown.js +23 -8
- package/dist/utils/package-version.d.ts +1 -0
- package/dist/utils/package-version.js +10 -0
- package/dist/utils/shell-output-spill.d.ts +2 -0
- package/dist/utils/shell-output-spill.js +81 -0
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/README.md +14 -0
- package/examples/extensions/claude-code-bridge/index.ts +204 -145
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/README.md +16 -0
- package/examples/extensions/pi-bridge/index.ts +9 -155
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/shell-recall.d.ts +0 -9
- package/dist/extensions/shell-recall.js +0 -8
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
|
@@ -268,6 +268,54 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
268
268
|
define("peer:context-search", (query: string) => contextManager.search(query));
|
|
269
269
|
server.expose("peer:context-search");
|
|
270
270
|
|
|
271
|
+
// ── Inter-peer messaging ───────────────────────────────────
|
|
272
|
+
|
|
273
|
+
interface InboxEntry { from: string; text: string; at: number; }
|
|
274
|
+
const inbox: InboxEntry[] = [];
|
|
275
|
+
const INBOX_MAX = 100;
|
|
276
|
+
|
|
277
|
+
const pending: InboxEntry[] = [];
|
|
278
|
+
|
|
279
|
+
define("peer:message", (from: string, text: string) => {
|
|
280
|
+
if (typeof from !== "string" || typeof text !== "string") {
|
|
281
|
+
throw new Error("peer:message requires (from: string, text: string)");
|
|
282
|
+
}
|
|
283
|
+
const entry: InboxEntry = { from, text, at: Date.now() };
|
|
284
|
+
inbox.push(entry);
|
|
285
|
+
if (inbox.length > INBOX_MAX) inbox.splice(0, inbox.length - INBOX_MAX);
|
|
286
|
+
pending.push(entry);
|
|
287
|
+
bus.emit("peer:message-received", entry);
|
|
288
|
+
bus.emit("ui:info", { message: `[peer ${from}] ${text}` });
|
|
289
|
+
setTimeout(drainPending, 100);
|
|
290
|
+
return { ok: true };
|
|
291
|
+
});
|
|
292
|
+
server.expose("peer:message");
|
|
293
|
+
|
|
294
|
+
// Drain pending peer messages by injecting a synthetic user turn.
|
|
295
|
+
// Only one submission per processing cycle — wait for agent:processing-done
|
|
296
|
+
// before releasing the next batch.
|
|
297
|
+
let busy = false;
|
|
298
|
+
|
|
299
|
+
function drainPending(): void {
|
|
300
|
+
if (busy || pending.length === 0) return;
|
|
301
|
+
const batch = pending.splice(0, pending.length);
|
|
302
|
+
const lines = batch.map((e) => `[from peer ${e.from}] ${e.text}`);
|
|
303
|
+
const query = [
|
|
304
|
+
"You received message(s) from other peer(s) in the mesh:",
|
|
305
|
+
"",
|
|
306
|
+
...lines,
|
|
307
|
+
"",
|
|
308
|
+
"Decide whether to reply (via `peer_send`), act on the request, or note and continue.",
|
|
309
|
+
].join("\n");
|
|
310
|
+
busy = true;
|
|
311
|
+
bus.emit("agent:submit", { query });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
bus.on("agent:processing-done", () => {
|
|
315
|
+
busy = false;
|
|
316
|
+
setTimeout(drainPending, 100);
|
|
317
|
+
});
|
|
318
|
+
|
|
271
319
|
// ── Handler registry API (for other extensions) ────────────
|
|
272
320
|
|
|
273
321
|
define("peer:discover", () => server.discover());
|
|
@@ -412,6 +460,71 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
412
460
|
}),
|
|
413
461
|
});
|
|
414
462
|
|
|
463
|
+
registerTool({
|
|
464
|
+
name: "peer_send",
|
|
465
|
+
description: "Send a text message to another running agent-sh peer. The peer will see it in their UI and on their next turn.",
|
|
466
|
+
input_schema: {
|
|
467
|
+
type: "object",
|
|
468
|
+
properties: {
|
|
469
|
+
peer_id: { type: "string", description: "The instance ID of the peer (from the peers tool)." },
|
|
470
|
+
text: { type: "string", description: "Message body." },
|
|
471
|
+
},
|
|
472
|
+
required: ["peer_id", "text"],
|
|
473
|
+
},
|
|
474
|
+
showOutput: false,
|
|
475
|
+
getDisplayInfo: () => ({ kind: "write" as const }),
|
|
476
|
+
formatCall: (args) => `peer ${args.peer_id}: "${String(args.text).slice(0, 40)}"`,
|
|
477
|
+
|
|
478
|
+
async execute(args) {
|
|
479
|
+
try {
|
|
480
|
+
await server.call(args.peer_id as string, "peer:message", ctx.instanceId, args.text as string);
|
|
481
|
+
return { content: `Sent to peer ${args.peer_id}.`, exitCode: 0, isError: false };
|
|
482
|
+
} catch (e) {
|
|
483
|
+
return {
|
|
484
|
+
content: `Failed to send: ${e instanceof Error ? e.message : String(e)}`,
|
|
485
|
+
exitCode: 1,
|
|
486
|
+
isError: true,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
formatResult: (_args, result) => ({
|
|
492
|
+
summary: result.isError ? "failed" : "sent",
|
|
493
|
+
}),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
registerTool({
|
|
497
|
+
name: "peer_inbox",
|
|
498
|
+
description: "Read recent messages received from other peers via peer_send.",
|
|
499
|
+
input_schema: {
|
|
500
|
+
type: "object",
|
|
501
|
+
properties: {
|
|
502
|
+
count: { type: "number", description: "Number of recent messages to return (default: 20)." },
|
|
503
|
+
},
|
|
504
|
+
required: [],
|
|
505
|
+
},
|
|
506
|
+
showOutput: false,
|
|
507
|
+
getDisplayInfo: () => ({ kind: "read" as const }),
|
|
508
|
+
formatCall: () => "reading inbox",
|
|
509
|
+
|
|
510
|
+
async execute(args) {
|
|
511
|
+
const n = (args.count as number) || 20;
|
|
512
|
+
const recent = inbox.slice(-n);
|
|
513
|
+
if (recent.length === 0) {
|
|
514
|
+
return { content: "(inbox empty)", exitCode: 0, isError: false };
|
|
515
|
+
}
|
|
516
|
+
const lines = recent.map((e) => {
|
|
517
|
+
const ago = Math.round((Date.now() - e.at) / 1000);
|
|
518
|
+
return `[${ago}s ago] ${e.from}: ${e.text}`;
|
|
519
|
+
});
|
|
520
|
+
return { content: lines.join("\n"), exitCode: 0, isError: false };
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
formatResult: (_args, result) => ({
|
|
524
|
+
summary: result.content === "(inbox empty)" ? "empty" : `${result.content.split("\n").length} msg`,
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
|
|
415
528
|
// ── Slash command ──────────────────────────────────────────
|
|
416
529
|
|
|
417
530
|
registerCommand("peers", "List running agent-sh peer instances", () => {
|
|
@@ -435,6 +548,8 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
435
548
|
"- `peer_terminal` to see what's on another terminal's screen",
|
|
436
549
|
"- `peer_history` to see what commands they ran recently",
|
|
437
550
|
"- `peer_search` to search their shell context by keyword",
|
|
551
|
+
"- `peer_send` to deliver a text message to another peer (appears in their UI)",
|
|
552
|
+
"- `peer_inbox` to read messages other peers have sent you",
|
|
438
553
|
"When the user references 'the other terminal' or 'my other shell', use these tools.",
|
|
439
554
|
].join("\n"));
|
|
440
555
|
|
|
@@ -33,3 +33,19 @@ Or switch at runtime:
|
|
|
33
33
|
|
|
34
34
|
- pi must be configured separately (`~/.pi/settings.json`) with API keys and model preferences
|
|
35
35
|
- agent-sh does not override pi's configuration — it uses whatever pi is set up with
|
|
36
|
+
|
|
37
|
+
## What this bridge is
|
|
38
|
+
|
|
39
|
+
A pure protocol translator between pi's event stream and agent-sh's bus events. Pi's built-in tools (command execution, file ops, etc.) are used exactly as pi ships them. The bridge adds no tools of its own.
|
|
40
|
+
|
|
41
|
+
## What this bridge intentionally does NOT bundle
|
|
42
|
+
|
|
43
|
+
Three PTY-access tools are left out on purpose:
|
|
44
|
+
|
|
45
|
+
- `terminal_read` — observe the user's live terminal screen
|
|
46
|
+
- `terminal_keys` — send keystrokes to the user's PTY
|
|
47
|
+
- `user_shell` — run commands in the user's live shell with lasting `cd`/`export`/`source` effects
|
|
48
|
+
|
|
49
|
+
These are opt-in capabilities that belong in their own extensions. If you want any of them with pi, write a small companion extension that registers the tool as a pi `ToolDefinition` (TypeBox schema, wired to the relevant bus event: `shell:pty-write`, `shell:exec-request`, or `ctx.terminalBuffer.readScreen()`) and load it alongside pi-bridge.
|
|
50
|
+
|
|
51
|
+
Keeping this split means the bridge stays narrow — only translating events — and the capability surface is composable per-backend.
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* provider settings, extensions, session management, and tool system.
|
|
6
6
|
* Agent-sh provides the shell frontend and TUI rendering.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The bridge is a pure protocol translator between pi's event stream and
|
|
9
|
+
* agent-sh's bus events. Pi brings its own tools for command execution,
|
|
10
|
+
* file ops, etc. PTY-access tools (`terminal_read`, `terminal_keys`,
|
|
11
|
+
* `user_shell`) are intentionally NOT bundled here — if you want pi to
|
|
12
|
+
* observe or mutate the user's live terminal, load a companion extension
|
|
13
|
+
* that registers those tools in pi's ToolDefinition format.
|
|
11
14
|
*
|
|
12
15
|
* Setup:
|
|
13
16
|
* npm install @mariozechner/pi-agent-core @mariozechner/pi-ai @mariozechner/pi-coding-agent
|
|
@@ -22,157 +25,13 @@ import {
|
|
|
22
25
|
createAgentSessionRuntime,
|
|
23
26
|
SessionManager,
|
|
24
27
|
} from "@mariozechner/pi-coding-agent";
|
|
25
|
-
import {
|
|
26
|
-
import type { ExtensionContext } from "../../src/types.js";
|
|
27
|
-
import type { EventBus } from "../../src/event-bus.js";
|
|
28
|
-
|
|
29
|
-
// ── Helpers ──────────────────────────────────────────────────────
|
|
30
|
-
function interpretEscapes(str: string): string {
|
|
31
|
-
return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
|
|
32
|
-
if (seq === "r") return "\r";
|
|
33
|
-
if (seq === "n") return "\n";
|
|
34
|
-
if (seq === "t") return "\t";
|
|
35
|
-
if (seq === "\\") return "\\";
|
|
36
|
-
if (seq === "0") return "\0";
|
|
37
|
-
if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
38
|
-
return seq;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function settle(ms = 100): Promise<void> {
|
|
43
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ── user_shell as a pi ToolDefinition ─────────────────────────────
|
|
47
|
-
function createUserShellToolDef(bus: EventBus) {
|
|
48
|
-
// Track agent-sh's live cwd so user_shell always runs in the right place
|
|
49
|
-
let liveCwd = process.cwd();
|
|
50
|
-
bus.on("shell:cwd-change", ({ cwd }) => { liveCwd = cwd; });
|
|
51
|
-
|
|
52
|
-
const schema = Type.Object({
|
|
53
|
-
command: Type.String({ description: "Command to execute in user's shell" }),
|
|
54
|
-
return_output: Type.Optional(
|
|
55
|
-
Type.Boolean({
|
|
56
|
-
description:
|
|
57
|
-
"Whether to return the command output. Default false — output is shown directly to the user.",
|
|
58
|
-
}),
|
|
59
|
-
),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
name: "user_shell",
|
|
64
|
-
label: "user_shell",
|
|
65
|
-
description:
|
|
66
|
-
"Run a command with lasting effects in the user's live shell (cd, export, " +
|
|
67
|
-
"install packages, start servers) or show output the user wants to see. " +
|
|
68
|
-
"Output is shown directly to the user. Set return_output=true only " +
|
|
69
|
-
"if you need to inspect the result.",
|
|
70
|
-
promptSnippet: "Execute commands in the user's live terminal (PTY).",
|
|
71
|
-
promptGuidelines: [
|
|
72
|
-
"You are running inside agent-sh, a terminal wrapper.",
|
|
73
|
-
"Use your standard tools (bash, file ops) for investigation — output goes to you, not the user.",
|
|
74
|
-
"Use user_shell to run commands in the user's live shell when they ask to see output or need lasting effects (cd, install, start servers).",
|
|
75
|
-
"Default to standard tools. Use user_shell when the user is the intended audience for the output or the command has real effects.",
|
|
76
|
-
],
|
|
77
|
-
parameters: schema,
|
|
78
|
-
|
|
79
|
-
async execute(_toolCallId, params) {
|
|
80
|
-
const command = params.command;
|
|
81
|
-
const returnOutput = params.return_output ?? false;
|
|
82
|
-
|
|
83
|
-
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
84
|
-
command,
|
|
85
|
-
output: "",
|
|
86
|
-
cwd: liveCwd,
|
|
87
|
-
done: false,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const text = returnOutput
|
|
91
|
-
? result.output || "(no output)"
|
|
92
|
-
: "Command executed.";
|
|
93
|
-
|
|
94
|
-
return { content: [{ type: "text", text }], details: undefined };
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── terminal_read as a pi ToolDefinition ─────────────────────────
|
|
100
|
-
function createTerminalReadToolDef(ctx: ExtensionContext) {
|
|
101
|
-
return {
|
|
102
|
-
name: "terminal_read",
|
|
103
|
-
label: "terminal_read",
|
|
104
|
-
description:
|
|
105
|
-
"Read the current terminal screen contents. Returns clean text (ANSI stripped) " +
|
|
106
|
-
"with cursor position and whether an alternate-screen program (vim, htop, less) is active.",
|
|
107
|
-
promptSnippet: "Read the terminal screen to see what the user sees.",
|
|
108
|
-
promptGuidelines: [
|
|
109
|
-
"Use terminal_read to see the current terminal screen before sending keystrokes.",
|
|
110
|
-
"Check altScreen to know if a full-screen program (vim, htop) is running.",
|
|
111
|
-
],
|
|
112
|
-
parameters: Type.Object({}),
|
|
113
|
-
async execute() {
|
|
114
|
-
const tb = ctx.terminalBuffer;
|
|
115
|
-
if (!tb) return { content: [{ type: "text", text: "terminal buffer not available" }], details: undefined };
|
|
116
|
-
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
117
|
-
const info = [
|
|
118
|
-
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
119
|
-
`cursor: row=${cursorY} col=${cursorX}`,
|
|
120
|
-
].join(", ");
|
|
121
|
-
return { content: [{ type: "text", text: `[${info}]\n\n${text}` }], details: undefined };
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── terminal_keys as a pi ToolDefinition ─────────────────────────
|
|
127
|
-
function createTerminalKeysToolDef(bus: EventBus, ctx: ExtensionContext) {
|
|
128
|
-
return {
|
|
129
|
-
name: "terminal_keys",
|
|
130
|
-
label: "terminal_keys",
|
|
131
|
-
description:
|
|
132
|
-
"Send keystrokes to the user's live terminal as if the user typed them. " +
|
|
133
|
-
"Use escape sequences: \\x1b for Escape, \\r for Enter, \\t for Tab, " +
|
|
134
|
-
"\\x03 for Ctrl+C, \\x1b[A/B/C/D for arrow keys, \\x7f for Backspace. " +
|
|
135
|
-
"Example: \\x1b:q!\\r to quit vim. Always call terminal_read after.",
|
|
136
|
-
promptSnippet: "Send keystrokes to interactive programs in the terminal.",
|
|
137
|
-
promptGuidelines: [
|
|
138
|
-
"Use terminal_keys to type into interactive programs (vim, htop, less).",
|
|
139
|
-
"Always call terminal_read after sending keys to verify the result.",
|
|
140
|
-
],
|
|
141
|
-
parameters: Type.Object({
|
|
142
|
-
keys: Type.String({ description: "Keystrokes to send (use \\x1b for Escape, \\r for Enter, etc.)" }),
|
|
143
|
-
settle_ms: Type.Optional(
|
|
144
|
-
Type.Number({ description: "Wait time in ms after sending keys (default: 150)" }),
|
|
145
|
-
),
|
|
146
|
-
}),
|
|
147
|
-
async execute(_toolCallId: string, params: any) {
|
|
148
|
-
const keys = interpretEscapes(params.keys);
|
|
149
|
-
const settleMs = params.settle_ms ?? 150;
|
|
150
|
-
bus.emit("shell:stdout-show", {});
|
|
151
|
-
process.stdout.write("\n");
|
|
152
|
-
bus.emit("shell:pty-write", { data: keys });
|
|
153
|
-
await settle(settleMs);
|
|
154
|
-
|
|
155
|
-
const tb = ctx.terminalBuffer;
|
|
156
|
-
if (!tb) return { content: [{ type: "text", text: "Keys sent." }], details: undefined };
|
|
157
|
-
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
158
|
-
const info = [
|
|
159
|
-
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
160
|
-
`cursor: row=${cursorY} col=${cursorX}`,
|
|
161
|
-
].join(", ");
|
|
162
|
-
return { content: [{ type: "text", text: `Keys sent. Screen after:\n[${info}]\n\n${text}` }], details: undefined };
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
28
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
166
29
|
|
|
167
30
|
// ── Extension entry point ─────────────────────────────────────────
|
|
168
31
|
export default function activate(ctx: ExtensionContext): void {
|
|
169
32
|
const { bus } = ctx;
|
|
170
33
|
const cwd = process.cwd();
|
|
171
34
|
|
|
172
|
-
const userShellTool = createUserShellToolDef(bus);
|
|
173
|
-
const termReadTool = createTerminalReadToolDef(ctx);
|
|
174
|
-
const termKeysTool = createTerminalKeysToolDef(bus, ctx);
|
|
175
|
-
|
|
176
35
|
// ── Boot pi session (async — register backend synchronously first) ──
|
|
177
36
|
let session: any = null;
|
|
178
37
|
let runtime: any = null;
|
|
@@ -190,7 +49,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
190
49
|
const result = await createAgentSessionFromServices({
|
|
191
50
|
services,
|
|
192
51
|
sessionManager: opts.sessionManager ?? sessionManager,
|
|
193
|
-
customTools: [userShellTool, termReadTool, termKeysTool],
|
|
194
52
|
});
|
|
195
53
|
return { ...result, services };
|
|
196
54
|
};
|
|
@@ -227,9 +85,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
227
85
|
bus.emit("agent:tool-started", {
|
|
228
86
|
title: (event as any).toolName,
|
|
229
87
|
toolCallId: (event as any).toolCallId,
|
|
230
|
-
kind: (event as any).toolName === "
|
|
231
|
-
? "execute"
|
|
232
|
-
: "read",
|
|
88
|
+
kind: (event as any).toolName === "bash" ? "execute" : "read",
|
|
233
89
|
});
|
|
234
90
|
break;
|
|
235
91
|
|
|
@@ -251,9 +107,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
251
107
|
bus.emit("agent:tool-completed", {
|
|
252
108
|
toolCallId: (event as any).toolCallId,
|
|
253
109
|
exitCode: (event as any).isError ? 1 : 0,
|
|
254
|
-
kind: (event as any).toolName === "
|
|
255
|
-
? "execute"
|
|
256
|
-
: "read",
|
|
110
|
+
kind: (event as any).toolName === "bash" ? "execute" : "read",
|
|
257
111
|
});
|
|
258
112
|
break;
|
|
259
113
|
|
|
@@ -55,15 +55,26 @@ interface QuestionnaireResult {
|
|
|
55
55
|
|
|
56
56
|
// ── Extension ────────────────────────────────────────────────────
|
|
57
57
|
|
|
58
|
-
export default function activate({ registerTool }: ExtensionContext) {
|
|
58
|
+
export default function activate({ registerTool, registerInstruction }: ExtensionContext) {
|
|
59
|
+
registerInstruction("questionnaire", [
|
|
60
|
+
"# When to use the questionnaire tool",
|
|
61
|
+
"ALWAYS use the `questionnaire` tool instead of asking the user a question in plain text when:",
|
|
62
|
+
"- You need the user to choose from specific options (e.g. frameworks, approaches, yes/no decisions)",
|
|
63
|
+
"- You are unsure about a preference and can enumerate reasonable choices",
|
|
64
|
+
"- The user's answer determines a significant branch in your behavior",
|
|
65
|
+
"",
|
|
66
|
+
"Do NOT just list options in your reply prose — call the questionnaire tool so the user can select interactively.",
|
|
67
|
+
"This applies to single questions too (yes/no, pick-one).",
|
|
68
|
+
].join("\n"));
|
|
69
|
+
|
|
59
70
|
registerTool({
|
|
60
71
|
name: "questionnaire",
|
|
61
72
|
displayName: "questionnaire",
|
|
62
73
|
description:
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
74
|
+
"Present the user with one or more questions with selectable options and wait for their interactive response. " +
|
|
75
|
+
"PREFER THIS over asking questions in plain text — it gives the user an interactive picker (arrow keys + enter). " +
|
|
76
|
+
"Use for: clarifying requirements, getting preferences, confirming decisions, choosing between options. " +
|
|
77
|
+
"Single question → simple option list. Multiple questions → tab-based multi-page interface.",
|
|
67
78
|
input_schema: {
|
|
68
79
|
type: "object",
|
|
69
80
|
properties: {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Usage:
|
|
9
9
|
* agent-sh -e ./examples/extensions/subagents.ts
|
|
10
10
|
*/
|
|
11
|
-
import type { ExtensionContext } from "
|
|
12
|
-
import { runSubagent } from "
|
|
11
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
12
|
+
import { runSubagent } from "agent-sh/agent/subagent";
|
|
13
13
|
|
|
14
14
|
export default function activate(ctx: ExtensionContext): void {
|
|
15
15
|
const { bus, llmClient, contextManager } = ctx;
|
|
@@ -17,6 +17,12 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
17
17
|
|
|
18
18
|
const allToolNames = () => ctx.getTools().map(t => t.name);
|
|
19
19
|
|
|
20
|
+
ctx.registerInstruction("subagent-guide", [
|
|
21
|
+
"You have a spawn_agent tool for delegating work to a subagent with its own context.",
|
|
22
|
+
"The subagent inherits your session history, so write a short directive (what to do), not a briefing (what happened).",
|
|
23
|
+
"Use it for tasks that need multiple tool calls you don't need to see — research, exploration, independent implementation.",
|
|
24
|
+
].join("\n"));
|
|
25
|
+
|
|
20
26
|
ctx.registerTool({
|
|
21
27
|
name: "spawn_agent",
|
|
22
28
|
description:
|
|
@@ -44,8 +50,15 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
44
50
|
|
|
45
51
|
getDisplayInfo: () => ({
|
|
46
52
|
kind: "execute",
|
|
53
|
+
icon: "⤵",
|
|
47
54
|
}),
|
|
48
55
|
|
|
56
|
+
formatCall: (args) => {
|
|
57
|
+
const task = String(args.task ?? "");
|
|
58
|
+
const max = 80;
|
|
59
|
+
return task.length > max ? task.slice(0, max - 1) + "…" : task;
|
|
60
|
+
},
|
|
61
|
+
|
|
49
62
|
async execute(args) {
|
|
50
63
|
const task = args.task as string;
|
|
51
64
|
const toolNames = args.tools as string[] | undefined;
|
|
@@ -56,9 +69,12 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
56
69
|
? allTools.filter(t => toolNames.includes(t.name))
|
|
57
70
|
: allTools.filter(t => t.name !== "spawn_agent");
|
|
58
71
|
|
|
72
|
+
const nuclearSummary = bus.emitPipe("agent:get-nuclear-summary", { summary: null }).summary;
|
|
73
|
+
|
|
59
74
|
const systemPrompt =
|
|
60
75
|
`You are a focused subagent. Complete the task and return a clear, concise result.\n` +
|
|
61
|
-
`Working directory: ${contextManager.getCwd()}
|
|
76
|
+
`Working directory: ${contextManager.getCwd()}` +
|
|
77
|
+
(nuclearSummary ? `\n\n[Parent session history]\n${nuclearSummary}` : "");
|
|
62
78
|
|
|
63
79
|
try {
|
|
64
80
|
const result = await runSubagent({
|
|
@@ -66,7 +82,6 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
66
82
|
tools,
|
|
67
83
|
systemPrompt,
|
|
68
84
|
task,
|
|
69
|
-
bus,
|
|
70
85
|
maxIterations: 25,
|
|
71
86
|
});
|
|
72
87
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal buffer extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers two agent tools:
|
|
5
|
+
* - terminal_read: get the current screen contents + cursor position
|
|
6
|
+
* - terminal_keys: send raw keystrokes into the user's live PTY
|
|
7
|
+
*
|
|
8
|
+
* Together these let the agent operate inside interactive programs
|
|
9
|
+
* (vim, htop, less, etc.) by reading the screen and typing keys.
|
|
10
|
+
*
|
|
11
|
+
* Requires xterm in the extension directory:
|
|
12
|
+
* npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
13
|
+
*
|
|
14
|
+
* Core already loads xterm lazily (for floating-panel compositing), so
|
|
15
|
+
* installing these deps anywhere on the NODE_PATH is enough.
|
|
16
|
+
*/
|
|
17
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
18
|
+
|
|
19
|
+
/** Interpret C-style escape sequences (e.g. \r → CR, \x1b → ESC). */
|
|
20
|
+
function interpretEscapes(str: string): string {
|
|
21
|
+
return str.replace(/\\(x[0-9a-fA-F]{2}|r|n|t|\\|0)/g, (_, seq: string) => {
|
|
22
|
+
if (seq === "r") return "\r";
|
|
23
|
+
if (seq === "n") return "\n";
|
|
24
|
+
if (seq === "t") return "\t";
|
|
25
|
+
if (seq === "\\") return "\\";
|
|
26
|
+
if (seq === "0") return "\0";
|
|
27
|
+
if (seq.startsWith("x")) return String.fromCharCode(parseInt(seq.slice(1), 16));
|
|
28
|
+
return seq;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function settle(ms = 100): Promise<void> {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
37
|
+
const { bus, terminalBuffer: tb, registerTool, registerInstruction } = ctx;
|
|
38
|
+
if (!tb) return; // @xterm/headless not installed
|
|
39
|
+
|
|
40
|
+
registerTool({
|
|
41
|
+
name: "terminal_read",
|
|
42
|
+
description:
|
|
43
|
+
"Read what is currently visible on the user's terminal screen. Returns clean text (ANSI stripped) " +
|
|
44
|
+
"with cursor position and whether an alternate-screen program (vim, htop, less) is active. " +
|
|
45
|
+
"Use this to observe what the user sees — helpful for answering questions about terminal output, " +
|
|
46
|
+
"diagnosing errors on screen, or checking state before/after sending keystrokes with terminal_keys.",
|
|
47
|
+
input_schema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
include_scrollback: {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
description:
|
|
53
|
+
"If true, include scrollback buffer (content that scrolled off screen) " +
|
|
54
|
+
"in addition to the visible viewport. Useful for capturing output from " +
|
|
55
|
+
"long-running or streaming commands. Default: false.",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
showOutput: true,
|
|
60
|
+
|
|
61
|
+
getDisplayInfo: () => ({
|
|
62
|
+
kind: "read" as const,
|
|
63
|
+
icon: "⊞",
|
|
64
|
+
locations: [],
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
async execute(args) {
|
|
68
|
+
const includeScrollback = (args.include_scrollback as boolean) ?? false;
|
|
69
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen({ includeScrollback });
|
|
70
|
+
const info = [
|
|
71
|
+
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
72
|
+
`cursor: row=${cursorY} col=${cursorX}`,
|
|
73
|
+
].join(", ");
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content: `[${info}]\n\n${text}`,
|
|
77
|
+
exitCode: 0,
|
|
78
|
+
isError: false,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
registerTool({
|
|
84
|
+
name: "terminal_keys",
|
|
85
|
+
description:
|
|
86
|
+
"Send keystrokes directly into the user's live terminal PTY, as if the user typed them. " +
|
|
87
|
+
"Use this to interact with programs already running in the terminal (vim, htop, less, ssh, REPLs, etc.) " +
|
|
88
|
+
"or to type commands at the shell prompt. This types directly into whatever is currently on screen.\n\n" +
|
|
89
|
+
"Escape sequences for special keys:\n" +
|
|
90
|
+
" - Escape: \\x1b\n" +
|
|
91
|
+
" - Enter/Return: \\r\n" +
|
|
92
|
+
" - Tab: \\t\n" +
|
|
93
|
+
" - Ctrl+C: \\x03\n" +
|
|
94
|
+
" - Ctrl+D: \\x04\n" +
|
|
95
|
+
" - Ctrl+Z: \\x1a\n" +
|
|
96
|
+
" - Arrow keys: \\x1b[A (up), \\x1b[B (down), \\x1b[C (right), \\x1b[D (left)\n" +
|
|
97
|
+
" - Backspace: \\x7f\n\n" +
|
|
98
|
+
"Example: to quit vim without saving, send keys=\"\\x1b:q!\\r\" (Escape, :q!, Enter).\n" +
|
|
99
|
+
"Always call terminal_read after sending keys to verify the result.",
|
|
100
|
+
input_schema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
keys: {
|
|
104
|
+
type: "string",
|
|
105
|
+
description:
|
|
106
|
+
"The keystrokes to send. Use \\x1b for Escape, \\r for Enter, \\t for Tab, " +
|
|
107
|
+
"\\x03 for Ctrl+C, etc. Regular characters are sent as-is.",
|
|
108
|
+
},
|
|
109
|
+
settle_ms: {
|
|
110
|
+
type: "number",
|
|
111
|
+
description:
|
|
112
|
+
"Milliseconds to wait after sending keys for the terminal to settle before " +
|
|
113
|
+
"returning (default: 150). Increase for slow programs.",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ["keys"],
|
|
117
|
+
},
|
|
118
|
+
showOutput: false,
|
|
119
|
+
|
|
120
|
+
getDisplayInfo: () => ({
|
|
121
|
+
kind: "execute" as const,
|
|
122
|
+
icon: "⌨",
|
|
123
|
+
locations: [],
|
|
124
|
+
}),
|
|
125
|
+
|
|
126
|
+
formatCall: (args) => {
|
|
127
|
+
const keys = args.keys as string;
|
|
128
|
+
return keys
|
|
129
|
+
.replace(/\\x1b|\x1b/g, "ESC")
|
|
130
|
+
.replace(/\\r|\r/g, "⏎")
|
|
131
|
+
.replace(/\\n|\n/g, "↵")
|
|
132
|
+
.replace(/\\t|\t/g, "TAB")
|
|
133
|
+
.replace(/\\x03|\x03/g, "^C")
|
|
134
|
+
.replace(/\\x04|\x04/g, "^D")
|
|
135
|
+
.replace(/\\x7f|\x7f/g, "BS");
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async execute(args) {
|
|
139
|
+
const raw = args.keys as string;
|
|
140
|
+
const keys = interpretEscapes(raw);
|
|
141
|
+
const settleMs = (args.settle_ms as number) ?? 150;
|
|
142
|
+
|
|
143
|
+
bus.emit("shell:stdout-show", {});
|
|
144
|
+
process.stdout.write("\n");
|
|
145
|
+
bus.emit("shell:pty-write", { data: keys });
|
|
146
|
+
|
|
147
|
+
await settle(settleMs);
|
|
148
|
+
bus.emit("shell:stdout-hide", {});
|
|
149
|
+
|
|
150
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen();
|
|
151
|
+
const info = [
|
|
152
|
+
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
153
|
+
`cursor: row=${cursorY} col=${cursorX}`,
|
|
154
|
+
].join(", ");
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
content: `Keys sent. Screen after:\n[${info}]\n\n${text}`,
|
|
158
|
+
exitCode: 0,
|
|
159
|
+
isError: false,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|