agent-sh 0.3.1 → 0.5.0
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 +66 -96
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +84 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +45 -2
- package/dist/shell.js +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -100
- package/dist/acp-client.js +0 -656
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code bridge — runs Claude Code Agent SDK in-process as agent-sh's backend.
|
|
3
|
+
*
|
|
4
|
+
* Uses the official @anthropic-ai/claude-agent-sdk to spawn a Claude Code
|
|
5
|
+
* session with a custom user_shell MCP tool for PTY access. Claude Code
|
|
6
|
+
* handles its own model selection, tool execution, and permissions.
|
|
7
|
+
*
|
|
8
|
+
* Setup:
|
|
9
|
+
* npm install @anthropic-ai/claude-agent-sdk
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* agent-sh -e examples/extensions/claude-code-bridge
|
|
13
|
+
*
|
|
14
|
+
* Requires: Claude Code CLI installed and authenticated (claude login).
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
query,
|
|
18
|
+
tool,
|
|
19
|
+
createSdkMcpServer,
|
|
20
|
+
type Query,
|
|
21
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import type { ExtensionContext } from "../../src/types.js";
|
|
24
|
+
import type { EventBus } from "../../src/event-bus.js";
|
|
25
|
+
|
|
26
|
+
// ── user_shell MCP tool ───────────────────────────────────────────
|
|
27
|
+
function createUserShellTool(bus: EventBus) {
|
|
28
|
+
let liveCwd = process.cwd();
|
|
29
|
+
bus.on("shell:cwd-change", ({ cwd }) => { liveCwd = cwd; });
|
|
30
|
+
|
|
31
|
+
return tool(
|
|
32
|
+
"user_shell",
|
|
33
|
+
"Run a command in the user's live shell (visible in terminal). " +
|
|
34
|
+
"Use for cd, export, source, or commands the user wants to see. " +
|
|
35
|
+
"Set return_output=true only if you need to inspect the result.",
|
|
36
|
+
{
|
|
37
|
+
command: z.string().describe("Command to execute in user's shell"),
|
|
38
|
+
return_output: z.boolean().optional().describe(
|
|
39
|
+
"Whether to return the command output. Default false.",
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
async (args) => {
|
|
43
|
+
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
44
|
+
command: args.command,
|
|
45
|
+
output: "",
|
|
46
|
+
cwd: liveCwd,
|
|
47
|
+
done: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const text = args.return_output
|
|
51
|
+
? result.output || "(no output)"
|
|
52
|
+
: "Command executed.";
|
|
53
|
+
|
|
54
|
+
return { content: [{ type: "text" as const, text }] };
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Extension entry point ─────────────────────────────────────────
|
|
60
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
61
|
+
const { bus } = ctx;
|
|
62
|
+
|
|
63
|
+
const shellTool = createUserShellTool(bus);
|
|
64
|
+
const shellServer = createSdkMcpServer({
|
|
65
|
+
name: "agent-sh",
|
|
66
|
+
version: "1.0.0",
|
|
67
|
+
tools: [shellTool],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let activeQuery: Query | null = null;
|
|
71
|
+
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
72
|
+
|
|
73
|
+
const wireListeners = () => {
|
|
74
|
+
const onSubmit = async ({ query: userQuery, modeInstruction, modeLabel }: any) => {
|
|
75
|
+
const prompt = modeInstruction
|
|
76
|
+
? `${modeInstruction}\n${userQuery}`
|
|
77
|
+
: userQuery;
|
|
78
|
+
|
|
79
|
+
bus.emit("agent:query", { query: userQuery, modeLabel });
|
|
80
|
+
bus.emit("agent:processing-start", {});
|
|
81
|
+
|
|
82
|
+
let fullResponseText = "";
|
|
83
|
+
let streamed = false;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
activeQuery = query({
|
|
87
|
+
prompt,
|
|
88
|
+
options: {
|
|
89
|
+
cwd: process.cwd(),
|
|
90
|
+
systemPrompt: {
|
|
91
|
+
type: "preset",
|
|
92
|
+
preset: "claude_code",
|
|
93
|
+
append:
|
|
94
|
+
"You are running inside agent-sh, a terminal wrapper.\n" +
|
|
95
|
+
"EXECUTE mode ('>'): Use your standard tools. Do NOT use user_shell.\n" +
|
|
96
|
+
"HELP mode ('?'): Run the command via mcp__agent-sh__user_shell. Just run it, no explanation.\n" +
|
|
97
|
+
"Each prompt includes a per-query mode instruction — follow it.",
|
|
98
|
+
},
|
|
99
|
+
mcpServers: { "agent-sh": shellServer },
|
|
100
|
+
allowedTools: [
|
|
101
|
+
"mcp__agent-sh__user_shell",
|
|
102
|
+
"Read", "Edit", "Write", "Bash", "Glob", "Grep",
|
|
103
|
+
],
|
|
104
|
+
permissionMode: "acceptEdits",
|
|
105
|
+
includePartialMessages: true,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
for await (const message of activeQuery) {
|
|
110
|
+
switch (message.type) {
|
|
111
|
+
case "stream_event": {
|
|
112
|
+
streamed = true;
|
|
113
|
+
const event = message.event;
|
|
114
|
+
if (event.type === "content_block_delta") {
|
|
115
|
+
const delta = event.delta as any;
|
|
116
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
117
|
+
bus.emitTransform("agent:response-chunk", {
|
|
118
|
+
blocks: [{ type: "text" as const, text: delta.text }],
|
|
119
|
+
});
|
|
120
|
+
fullResponseText += delta.text;
|
|
121
|
+
} else if (delta.type === "thinking_delta" && delta.thinking) {
|
|
122
|
+
bus.emit("agent:thinking-chunk", { text: delta.thinking });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "assistant": {
|
|
129
|
+
const msg = message.message;
|
|
130
|
+
for (const block of msg.content) {
|
|
131
|
+
const b = block as any;
|
|
132
|
+
if (b.type === "text" && b.text && !streamed) {
|
|
133
|
+
bus.emitTransform("agent:response-chunk", {
|
|
134
|
+
blocks: [{ type: "text" as const, text: b.text }],
|
|
135
|
+
});
|
|
136
|
+
fullResponseText += b.text;
|
|
137
|
+
} else if (b.type === "tool_use") {
|
|
138
|
+
bus.emit("agent:tool-started", {
|
|
139
|
+
title: b.name,
|
|
140
|
+
toolCallId: b.id,
|
|
141
|
+
kind: b.name.includes("shell") || b.name === "Bash"
|
|
142
|
+
? "execute"
|
|
143
|
+
: "read",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "result":
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
bus.emitTransform("agent:response-done", {
|
|
156
|
+
response: fullResponseText,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
bus.emit("agent:error", {
|
|
160
|
+
message: err instanceof Error ? err.message : String(err),
|
|
161
|
+
});
|
|
162
|
+
} finally {
|
|
163
|
+
activeQuery = null;
|
|
164
|
+
bus.emit("agent:processing-done", {});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const onCancel = () => { activeQuery?.interrupt(); };
|
|
169
|
+
const onReset = () => { /* each query() is a new session */ };
|
|
170
|
+
|
|
171
|
+
bus.on("agent:submit", onSubmit);
|
|
172
|
+
bus.on("agent:cancel-request", onCancel);
|
|
173
|
+
bus.on("agent:reset-session", onReset);
|
|
174
|
+
listeners.push(
|
|
175
|
+
{ event: "agent:submit", fn: onSubmit },
|
|
176
|
+
{ event: "agent:cancel-request", fn: onCancel },
|
|
177
|
+
{ event: "agent:reset-session", fn: onReset },
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const unwireListeners = () => {
|
|
182
|
+
for (const { event, fn } of listeners) bus.off(event as any, fn as any);
|
|
183
|
+
listeners.length = 0;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// ── Register as backend ───────────────────────────────────────
|
|
187
|
+
bus.emit("agent:register-backend", {
|
|
188
|
+
name: "claude-code",
|
|
189
|
+
start: async () => {
|
|
190
|
+
wireListeners();
|
|
191
|
+
bus.emit("agent:info", { name: "claude-code", version: "1.0" });
|
|
192
|
+
},
|
|
193
|
+
kill: () => {
|
|
194
|
+
activeQuery?.interrupt();
|
|
195
|
+
unwireListeners();
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-sh-claude-code-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code agent backend for agent-sh",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.0",
|
|
9
|
+
"zod": "^4.0.0"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter provider extension.
|
|
3
|
+
*
|
|
4
|
+
* Registers OpenRouter as a provider and fetches its full model catalog
|
|
5
|
+
* at startup. Models appear in /model autocomplete as "model [openrouter]"
|
|
6
|
+
* and are available for cycling with Shift+Tab.
|
|
7
|
+
*
|
|
8
|
+
* Model capabilities (reasoning, context window) are read from the
|
|
9
|
+
* OpenRouter API response — no hardcoded model lists.
|
|
10
|
+
*
|
|
11
|
+
* Setup:
|
|
12
|
+
* export OPENROUTER_API_KEY="your-key"
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* agent-sh -e ./examples/extensions/openrouter.ts
|
|
16
|
+
*
|
|
17
|
+
* # Or add to settings.json:
|
|
18
|
+
* { "extensions": ["./examples/extensions/openrouter.ts"] }
|
|
19
|
+
*/
|
|
20
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
21
|
+
|
|
22
|
+
const BASE_URL = "https://openrouter.ai/api/v1";
|
|
23
|
+
const API_KEY = process.env.OPENROUTER_API_KEY ?? "";
|
|
24
|
+
|
|
25
|
+
/** Curated default models — used immediately while the full catalog loads. */
|
|
26
|
+
const DEFAULT_MODELS = [
|
|
27
|
+
"anthropic/claude-sonnet-4",
|
|
28
|
+
"google/gemini-2.5-pro-preview",
|
|
29
|
+
"openai/gpt-4.1",
|
|
30
|
+
"deepseek/deepseek-r1",
|
|
31
|
+
"meta-llama/llama-4-maverick",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
interface OpenRouterModel {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
context_length?: number;
|
|
38
|
+
supported_parameters?: string[];
|
|
39
|
+
pricing?: { prompt: string; completion: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function activate({ bus }: ExtensionContext): void {
|
|
43
|
+
if (!API_KEY) {
|
|
44
|
+
bus.emit("ui:error", {
|
|
45
|
+
message: "OpenRouter extension: OPENROUTER_API_KEY not set. Skipping.",
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Register provider immediately with curated defaults
|
|
51
|
+
bus.emit("provider:register", {
|
|
52
|
+
id: "openrouter",
|
|
53
|
+
apiKey: API_KEY,
|
|
54
|
+
baseURL: BASE_URL,
|
|
55
|
+
defaultModel: DEFAULT_MODELS[0],
|
|
56
|
+
models: DEFAULT_MODELS,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Fetch full model catalog in background, re-register with capabilities
|
|
60
|
+
fetchModels().then((models) => {
|
|
61
|
+
if (models.length > 0) {
|
|
62
|
+
bus.emit("provider:register", {
|
|
63
|
+
id: "openrouter",
|
|
64
|
+
apiKey: API_KEY,
|
|
65
|
+
baseURL: BASE_URL,
|
|
66
|
+
defaultModel: DEFAULT_MODELS[0],
|
|
67
|
+
supportsReasoningEffort: true,
|
|
68
|
+
models: models.map((m) => ({
|
|
69
|
+
id: m.id,
|
|
70
|
+
reasoning: m.supported_parameters?.includes("reasoning") ?? false,
|
|
71
|
+
contextWindow: m.context_length,
|
|
72
|
+
})),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}).catch(() => {
|
|
76
|
+
// Silently fall back to curated defaults
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fetchModels(): Promise<OpenRouterModel[]> {
|
|
81
|
+
const res = await fetch(`${BASE_URL}/models`, {
|
|
82
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) return [];
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
return (data.data ?? []) as OpenRouterModel[];
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# pi-bridge
|
|
2
|
+
|
|
3
|
+
Runs [pi](https://github.com/nickarora/pi)'s full coding agent as an agent-sh backend. Uses pi's own configuration, models, tools, and extensions — agent-sh just provides the terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Copy or symlink into your extensions directory
|
|
9
|
+
cp -r examples/extensions/pi-bridge ~/.agent-sh/extensions/pi-bridge
|
|
10
|
+
|
|
11
|
+
# Install dependencies
|
|
12
|
+
cd ~/.agent-sh/extensions/pi-bridge
|
|
13
|
+
npm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configure
|
|
17
|
+
|
|
18
|
+
Set as default backend in `~/.agent-sh/settings.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"defaultBackend": "pi"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or switch at runtime:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
? /backend pi
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- pi must be configured separately (`~/.pi/settings.json`) with API keys and model preferences
|
|
35
|
+
- agent-sh does not override pi's configuration — it uses whatever pi is set up with
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi bridge — runs pi's full coding agent in-process as agent-sh's backend.
|
|
3
|
+
*
|
|
4
|
+
* Uses pi's own AgentSession with its full configuration: model registry,
|
|
5
|
+
* provider settings, extensions, session management, and tool system.
|
|
6
|
+
* Agent-sh provides the shell frontend and TUI rendering.
|
|
7
|
+
*
|
|
8
|
+
* In addition to pi's built-in tools, this bridge registers `user_shell`
|
|
9
|
+
* so pi can execute commands in agent-sh's live PTY (visible to the user,
|
|
10
|
+
* affects shell state like cd/export/source).
|
|
11
|
+
*
|
|
12
|
+
* Setup:
|
|
13
|
+
* npm install @mariozechner/pi-agent-core @mariozechner/pi-ai @mariozechner/pi-coding-agent
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* agent-sh -e examples/extensions/pi-bridge
|
|
17
|
+
*/
|
|
18
|
+
import type { AgentEvent } from "@mariozechner/pi-agent-core";
|
|
19
|
+
import {
|
|
20
|
+
createAgentSessionServices,
|
|
21
|
+
createAgentSessionFromServices,
|
|
22
|
+
createAgentSessionRuntime,
|
|
23
|
+
SessionManager,
|
|
24
|
+
} from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { Type } from "@sinclair/typebox";
|
|
26
|
+
import type { ExtensionContext } from "../../src/types.js";
|
|
27
|
+
import type { EventBus } from "../../src/event-bus.js";
|
|
28
|
+
|
|
29
|
+
// ── agent-sh context injected via tool promptGuidelines + promptSnippet ──
|
|
30
|
+
|
|
31
|
+
// ── user_shell as a pi ToolDefinition ─────────────────────────────
|
|
32
|
+
function createUserShellToolDef(bus: EventBus) {
|
|
33
|
+
// Track agent-sh's live cwd so user_shell always runs in the right place
|
|
34
|
+
let liveCwd = process.cwd();
|
|
35
|
+
bus.on("shell:cwd-change", ({ cwd }) => { liveCwd = cwd; });
|
|
36
|
+
|
|
37
|
+
const schema = Type.Object({
|
|
38
|
+
command: Type.String({ description: "Command to execute in user's shell" }),
|
|
39
|
+
return_output: Type.Optional(
|
|
40
|
+
Type.Boolean({
|
|
41
|
+
description:
|
|
42
|
+
"Whether to return the command output. Default false — output is shown directly to the user.",
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name: "user_shell",
|
|
49
|
+
label: "user_shell",
|
|
50
|
+
description:
|
|
51
|
+
"Run a command in the user's live shell (visible in terminal). " +
|
|
52
|
+
"Use for cd, export, source, or commands the user wants to see. " +
|
|
53
|
+
"Output is shown directly to the user. Set return_output=true only " +
|
|
54
|
+
"if you need to inspect the result.",
|
|
55
|
+
promptSnippet: "Execute commands in the user's live terminal (PTY). Use in HELP mode.",
|
|
56
|
+
promptGuidelines: [
|
|
57
|
+
"You are running inside agent-sh, a terminal wrapper with two interaction modes.",
|
|
58
|
+
"EXECUTE mode (triggered by '>'): Use your standard tools (bash, file ops). Do NOT use user_shell.",
|
|
59
|
+
"HELP mode (triggered by '?'): Run the command via user_shell. Do not explain or confirm — just run it.",
|
|
60
|
+
"Each prompt includes a per-query mode instruction — follow it.",
|
|
61
|
+
"user_shell executes in the user's actual shell (their aliases, env vars, cwd). Use bash for background work.",
|
|
62
|
+
],
|
|
63
|
+
parameters: schema,
|
|
64
|
+
|
|
65
|
+
async execute(_toolCallId, params) {
|
|
66
|
+
const command = params.command;
|
|
67
|
+
const returnOutput = params.return_output ?? false;
|
|
68
|
+
|
|
69
|
+
const result = await bus.emitPipeAsync("shell:exec-request", {
|
|
70
|
+
command,
|
|
71
|
+
output: "",
|
|
72
|
+
cwd: liveCwd,
|
|
73
|
+
done: false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const text = returnOutput
|
|
77
|
+
? result.output || "(no output)"
|
|
78
|
+
: "Command executed.";
|
|
79
|
+
|
|
80
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Extension entry point ─────────────────────────────────────────
|
|
86
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
87
|
+
const { bus } = ctx;
|
|
88
|
+
const cwd = process.cwd();
|
|
89
|
+
|
|
90
|
+
const userShellTool = createUserShellToolDef(bus);
|
|
91
|
+
|
|
92
|
+
// ── Boot pi session (async — register backend synchronously first) ──
|
|
93
|
+
let session: any = null;
|
|
94
|
+
let runtime: any = null;
|
|
95
|
+
let booting = true;
|
|
96
|
+
|
|
97
|
+
const boot = async () => {
|
|
98
|
+
try {
|
|
99
|
+
// Pi loads its own config: ~/.pi/agent/settings.json, models, extensions
|
|
100
|
+
const services = await createAgentSessionServices({ cwd });
|
|
101
|
+
const sessionManager = SessionManager.inMemory(cwd);
|
|
102
|
+
|
|
103
|
+
// createRuntime factory — returns { session, services, ... } as expected
|
|
104
|
+
// by createAgentSessionRuntime
|
|
105
|
+
const createRuntime = async (opts: any) => {
|
|
106
|
+
const result = await createAgentSessionFromServices({
|
|
107
|
+
services,
|
|
108
|
+
sessionManager: opts.sessionManager ?? sessionManager,
|
|
109
|
+
customTools: [userShellTool],
|
|
110
|
+
});
|
|
111
|
+
return { ...result, services };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
runtime = await createAgentSessionRuntime(createRuntime, {
|
|
115
|
+
cwd,
|
|
116
|
+
sessionManager,
|
|
117
|
+
});
|
|
118
|
+
session = runtime.session;
|
|
119
|
+
|
|
120
|
+
// Subscribe to pi events → agent-sh bus
|
|
121
|
+
let fullResponseText = "";
|
|
122
|
+
|
|
123
|
+
session.subscribe((event: AgentEvent) => {
|
|
124
|
+
switch (event.type) {
|
|
125
|
+
case "agent_start":
|
|
126
|
+
fullResponseText = "";
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "message_update": {
|
|
130
|
+
const ame = (event as any).assistantMessageEvent;
|
|
131
|
+
if (ame.type === "text_delta") {
|
|
132
|
+
bus.emitTransform("agent:response-chunk", {
|
|
133
|
+
blocks: [{ type: "text" as const, text: ame.delta }],
|
|
134
|
+
});
|
|
135
|
+
fullResponseText += ame.delta;
|
|
136
|
+
} else if (ame.type === "thinking_delta") {
|
|
137
|
+
bus.emit("agent:thinking-chunk", { text: ame.delta });
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case "tool_execution_start":
|
|
143
|
+
bus.emit("agent:tool-started", {
|
|
144
|
+
title: (event as any).toolName,
|
|
145
|
+
toolCallId: (event as any).toolCallId,
|
|
146
|
+
kind: (event as any).toolName === "user_shell" || (event as any).toolName === "bash"
|
|
147
|
+
? "execute"
|
|
148
|
+
: "read",
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case "tool_execution_update": {
|
|
153
|
+
const pr = (event as any).partialResult as
|
|
154
|
+
| { content?: Array<{ type: string; text?: string }> }
|
|
155
|
+
| undefined;
|
|
156
|
+
if (pr?.content) {
|
|
157
|
+
for (const c of pr.content) {
|
|
158
|
+
if (c.type === "text" && c.text) {
|
|
159
|
+
bus.emit("agent:tool-output-chunk", { chunk: c.text });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "tool_execution_end":
|
|
167
|
+
bus.emit("agent:tool-completed", {
|
|
168
|
+
toolCallId: (event as any).toolCallId,
|
|
169
|
+
exitCode: (event as any).isError ? 1 : 0,
|
|
170
|
+
kind: (event as any).toolName === "user_shell" || (event as any).toolName === "bash"
|
|
171
|
+
? "execute"
|
|
172
|
+
: "read",
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "agent_end":
|
|
177
|
+
bus.emitTransform("agent:response-done", {
|
|
178
|
+
response: fullResponseText,
|
|
179
|
+
});
|
|
180
|
+
bus.emit("agent:processing-done", {});
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Report model info
|
|
186
|
+
const model = session.model;
|
|
187
|
+
bus.emit("agent:info", {
|
|
188
|
+
name: "pi",
|
|
189
|
+
version: "0.66",
|
|
190
|
+
model: model ? `${model.provider}/${model.id}` : undefined,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
booting = false;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
booting = false;
|
|
196
|
+
bus.emit("ui:error", {
|
|
197
|
+
message: `pi-bridge: failed to initialize — ${err instanceof Error ? err.message : String(err)}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// ── Bus listeners (wired on start, unwired on kill) ────────────
|
|
203
|
+
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
204
|
+
|
|
205
|
+
const wireListeners = () => {
|
|
206
|
+
const onSubmit = async ({ query, modeInstruction, modeLabel }: any) => {
|
|
207
|
+
if (!session) {
|
|
208
|
+
bus.emit("agent:error", {
|
|
209
|
+
message: booting ? "pi is still starting up..." : "pi session not initialized",
|
|
210
|
+
});
|
|
211
|
+
bus.emit("agent:processing-done", {});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const prompt = modeInstruction ? `${modeInstruction}\n${query}` : query;
|
|
216
|
+
bus.emit("agent:query", { query, modeLabel });
|
|
217
|
+
bus.emit("agent:processing-start", {});
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await session.prompt(prompt);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
bus.emit("agent:error", {
|
|
223
|
+
message: err instanceof Error ? err.message : String(err),
|
|
224
|
+
});
|
|
225
|
+
bus.emit("agent:processing-done", {});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const onCancel = async () => { await session?.abort(); };
|
|
230
|
+
const onReset = async () => {
|
|
231
|
+
await runtime?.newSession();
|
|
232
|
+
session = runtime?.session;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
bus.on("agent:submit", onSubmit);
|
|
236
|
+
bus.on("agent:cancel-request", onCancel);
|
|
237
|
+
bus.on("agent:reset-session", onReset);
|
|
238
|
+
listeners.push(
|
|
239
|
+
{ event: "agent:submit", fn: onSubmit },
|
|
240
|
+
{ event: "agent:cancel-request", fn: onCancel },
|
|
241
|
+
{ event: "agent:reset-session", fn: onReset },
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const unwireListeners = () => {
|
|
246
|
+
for (const { event, fn } of listeners) bus.off(event as any, fn as any);
|
|
247
|
+
listeners.length = 0;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// ── Register as backend ───────────────────────────────────────
|
|
251
|
+
bus.emit("agent:register-backend", {
|
|
252
|
+
name: "pi",
|
|
253
|
+
start: async () => {
|
|
254
|
+
await boot();
|
|
255
|
+
wireListeners();
|
|
256
|
+
},
|
|
257
|
+
kill: () => {
|
|
258
|
+
unwireListeners();
|
|
259
|
+
runtime?.dispose();
|
|
260
|
+
session = null;
|
|
261
|
+
runtime = null;
|
|
262
|
+
booting = true;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-sh-pi-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi coding agent backend for agent-sh",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@mariozechner/pi-agent-core": "^0.66.0",
|
|
9
|
+
"@mariozechner/pi-ai": "^0.66.0",
|
|
10
|
+
"@mariozechner/pi-coding-agent": "^0.66.0",
|
|
11
|
+
"@sinclair/typebox": "^0.34.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent extension — lets the main agent spawn focused sub-agents.
|
|
3
|
+
*
|
|
4
|
+
* The main agent gets a `spawn_agent` tool that creates a fresh agent
|
|
5
|
+
* with its own context. The LLM decides how to specialize — no
|
|
6
|
+
* predefined categories, no registry, no config.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* agent-sh -e ./examples/extensions/subagents.ts
|
|
10
|
+
*/
|
|
11
|
+
import type { ExtensionContext } from "../../src/types.js";
|
|
12
|
+
import { runSubagent } from "../../src/agent/subagent.js";
|
|
13
|
+
|
|
14
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
15
|
+
const { bus, llmClient, contextManager } = ctx;
|
|
16
|
+
if (!llmClient) return;
|
|
17
|
+
|
|
18
|
+
const allToolNames = () => ctx.getTools().map(t => t.name);
|
|
19
|
+
|
|
20
|
+
ctx.registerTool({
|
|
21
|
+
name: "spawn_agent",
|
|
22
|
+
description:
|
|
23
|
+
"Spawn a subagent with its own fresh context to handle a focused task. " +
|
|
24
|
+
"Use this to delegate work that needs investigation or multiple tool calls, " +
|
|
25
|
+
"without polluting your main conversation context. " +
|
|
26
|
+
"The subagent runs to completion and returns its result.",
|
|
27
|
+
input_schema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
task: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Clear description of what the subagent should do",
|
|
33
|
+
},
|
|
34
|
+
tools: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: { type: "string" },
|
|
37
|
+
description: `Tool names the subagent can use. Available: ${allToolNames().join(", ")}`,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["task"],
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
showOutput: false,
|
|
44
|
+
|
|
45
|
+
getDisplayInfo: () => ({
|
|
46
|
+
kind: "execute",
|
|
47
|
+
}),
|
|
48
|
+
|
|
49
|
+
async execute(args) {
|
|
50
|
+
const task = args.task as string;
|
|
51
|
+
const toolNames = args.tools as string[] | undefined;
|
|
52
|
+
|
|
53
|
+
const allTools = ctx.getTools();
|
|
54
|
+
// Filter to requested tools, or give all tools (minus spawn_agent to prevent recursion)
|
|
55
|
+
const tools = toolNames
|
|
56
|
+
? allTools.filter(t => toolNames.includes(t.name))
|
|
57
|
+
: allTools.filter(t => t.name !== "spawn_agent");
|
|
58
|
+
|
|
59
|
+
const systemPrompt =
|
|
60
|
+
`You are a focused subagent. Complete the task and return a clear, concise result.\n` +
|
|
61
|
+
`Working directory: ${contextManager.getCwd()}`;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await runSubagent({
|
|
65
|
+
llmClient,
|
|
66
|
+
tools,
|
|
67
|
+
systemPrompt,
|
|
68
|
+
task,
|
|
69
|
+
bus,
|
|
70
|
+
maxIterations: 25,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
content: result || "(no response)",
|
|
75
|
+
exitCode: 0,
|
|
76
|
+
isError: false,
|
|
77
|
+
};
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
content: `Subagent error: ${err instanceof Error ? err.message : String(err)}`,
|
|
81
|
+
exitCode: 1,
|
|
82
|
+
isError: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|