agent-sh 0.12.0 → 0.12.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.
|
@@ -55,6 +55,13 @@ export declare class ConversationState {
|
|
|
55
55
|
addToolResultInline(content: string): void;
|
|
56
56
|
addSystemNote(text: string): void;
|
|
57
57
|
getMessages(): ChatCompletionMessageParam[];
|
|
58
|
+
/**
|
|
59
|
+
* If a stream was interrupted mid-tool-execution, an assistant message
|
|
60
|
+
* with tool_calls can land in history without matching tool results.
|
|
61
|
+
* Strict providers (DeepSeek) 400 on this. Stub each missing result
|
|
62
|
+
* with a [cancelled] marker so the protocol stays valid.
|
|
63
|
+
*/
|
|
64
|
+
private stubDanglingToolCalls;
|
|
58
65
|
/**
|
|
59
66
|
* DeepSeek 400s if any assistant in a thinking-mode conversation is
|
|
60
67
|
* missing reasoning_content. Cross-alias here (OpenRouter streams as
|
|
@@ -111,7 +111,37 @@ export class ConversationState {
|
|
|
111
111
|
this.invalidateMessagesCache();
|
|
112
112
|
}
|
|
113
113
|
getMessages() {
|
|
114
|
-
return this.normalizeReasoningConsistency(this.messages);
|
|
114
|
+
return this.normalizeReasoningConsistency(this.stubDanglingToolCalls(this.messages));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* If a stream was interrupted mid-tool-execution, an assistant message
|
|
118
|
+
* with tool_calls can land in history without matching tool results.
|
|
119
|
+
* Strict providers (DeepSeek) 400 on this. Stub each missing result
|
|
120
|
+
* with a [cancelled] marker so the protocol stays valid.
|
|
121
|
+
*/
|
|
122
|
+
stubDanglingToolCalls(messages) {
|
|
123
|
+
const result = [];
|
|
124
|
+
let i = 0;
|
|
125
|
+
while (i < messages.length) {
|
|
126
|
+
const msg = messages[i];
|
|
127
|
+
result.push(msg);
|
|
128
|
+
i++;
|
|
129
|
+
if (msg.role !== "assistant" || !("tool_calls" in msg) || !msg.tool_calls)
|
|
130
|
+
continue;
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
while (i < messages.length && messages[i].role === "tool") {
|
|
133
|
+
const t = messages[i];
|
|
134
|
+
seen.add(t.tool_call_id);
|
|
135
|
+
result.push(t);
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
for (const tc of msg.tool_calls) {
|
|
139
|
+
if (!seen.has(tc.id)) {
|
|
140
|
+
result.push({ role: "tool", tool_call_id: tc.id, content: "[cancelled]" });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
115
145
|
}
|
|
116
146
|
/**
|
|
117
147
|
* DeepSeek 400s if any assistant in a thinking-mode conversation is
|
package/dist/event-bus.js
CHANGED
|
@@ -6,7 +6,7 @@ import { EventEmitter } from "node:events";
|
|
|
6
6
|
* can modify the payload before passing to the next
|
|
7
7
|
*/
|
|
8
8
|
export class EventBus {
|
|
9
|
-
emitter = new EventEmitter();
|
|
9
|
+
emitter = new EventEmitter().setMaxListeners(0);
|
|
10
10
|
pipeListeners = new Map();
|
|
11
11
|
asyncPipeListeners = new Map();
|
|
12
12
|
/** Subscribe to a fire-and-forget event. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/core.js",
|
|
@@ -89,7 +89,13 @@
|
|
|
89
89
|
},
|
|
90
90
|
"files": [
|
|
91
91
|
"dist",
|
|
92
|
-
"examples"
|
|
92
|
+
"examples/extensions/*.ts",
|
|
93
|
+
"examples/extensions/*/package.json",
|
|
94
|
+
"examples/extensions/*/tsconfig.json",
|
|
95
|
+
"examples/extensions/*/README.md",
|
|
96
|
+
"examples/extensions/*/src",
|
|
97
|
+
"examples/extensions/*/index.ts",
|
|
98
|
+
"examples/extensions/*/index.js"
|
|
93
99
|
],
|
|
94
100
|
"scripts": {
|
|
95
101
|
"dev": "tsx src/index.ts",
|
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* agent-sh-acp — ACP (Agent Client Protocol) server wrapping agent-sh's
|
|
4
|
-
* headless core. Speaks JSON-RPC 2.0 over stdin/stdout so agent-shell
|
|
5
|
-
* (Emacs) can drive it as a backend.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* agent-sh-acp # uses settings from ~/.agent-sh/settings.json
|
|
9
|
-
* agent-sh-acp --model gpt-4o # override model
|
|
10
|
-
*
|
|
11
|
-
* In agent-shell (Emacs):
|
|
12
|
-
* (setq agent-shell-agentsh-acp-command '("agent-sh-acp"))
|
|
13
|
-
*/
|
|
14
|
-
import { createCore, type AgentShellCore } from "agent-sh";
|
|
15
|
-
import { loadExtensions } from "agent-sh/extension-loader";
|
|
16
|
-
import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
17
|
-
import { getSettings } from "agent-sh/settings";
|
|
18
|
-
import type { ContentBlock } from "agent-sh/types";
|
|
19
|
-
|
|
20
|
-
// ── JSON-RPC types ──────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
interface JsonRpcRequest {
|
|
23
|
-
jsonrpc: "2.0";
|
|
24
|
-
method: string;
|
|
25
|
-
params?: Record<string, unknown>;
|
|
26
|
-
id?: number | string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface JsonRpcResponse {
|
|
30
|
-
jsonrpc: "2.0";
|
|
31
|
-
id: number | string;
|
|
32
|
-
result?: unknown;
|
|
33
|
-
error?: { code: number; message: string; data?: unknown };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface JsonRpcNotification {
|
|
37
|
-
jsonrpc: "2.0";
|
|
38
|
-
method: string;
|
|
39
|
-
params?: Record<string, unknown>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ── ACP content block ───────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
interface AcpContentBlock {
|
|
45
|
-
type: string;
|
|
46
|
-
text?: string;
|
|
47
|
-
data?: string;
|
|
48
|
-
mimeType?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ── Stdio transport ─────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
function send(msg: JsonRpcResponse | JsonRpcNotification): void {
|
|
54
|
-
const line = JSON.stringify(msg) + "\n";
|
|
55
|
-
process.stdout.write(line);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function sendResult(id: number | string, result: unknown): void {
|
|
59
|
-
send({ jsonrpc: "2.0", id, result });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function sendError(id: number | string, code: number, message: string, data?: unknown): void {
|
|
63
|
-
send({ jsonrpc: "2.0", id, error: { code, message, data } });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sendNotification(method: string, params: Record<string, unknown>): void {
|
|
67
|
-
send({ jsonrpc: "2.0", method, params });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ── ACP session/update helpers ──────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
function sendSessionUpdate(update: Record<string, unknown>): void {
|
|
73
|
-
sendNotification("session/update", { update });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function sendTextChunk(text: string): void {
|
|
77
|
-
sendSessionUpdate({
|
|
78
|
-
sessionUpdate: "agent_message_chunk",
|
|
79
|
-
content: { type: "text", text },
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function sendThinkingChunk(text: string): void {
|
|
84
|
-
sendSessionUpdate({
|
|
85
|
-
sessionUpdate: "agent_thought_chunk",
|
|
86
|
-
content: { type: "text", text },
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sendToolCall(
|
|
91
|
-
toolCallId: string,
|
|
92
|
-
title: string,
|
|
93
|
-
kind: string,
|
|
94
|
-
rawInput?: unknown,
|
|
95
|
-
): void {
|
|
96
|
-
sendSessionUpdate({
|
|
97
|
-
sessionUpdate: "tool_call",
|
|
98
|
-
toolCallId,
|
|
99
|
-
title,
|
|
100
|
-
status: "pending",
|
|
101
|
-
kind,
|
|
102
|
-
content: [],
|
|
103
|
-
rawInput,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function sendToolCallUpdate(
|
|
108
|
-
toolCallId: string,
|
|
109
|
-
status: string,
|
|
110
|
-
content: AcpContentBlock[],
|
|
111
|
-
kind?: string,
|
|
112
|
-
): void {
|
|
113
|
-
sendSessionUpdate({
|
|
114
|
-
sessionUpdate: "tool_call_update",
|
|
115
|
-
toolCallId,
|
|
116
|
-
status,
|
|
117
|
-
content,
|
|
118
|
-
kind,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function sendUsageUpdate(
|
|
123
|
-
inputTokens: number,
|
|
124
|
-
outputTokens: number,
|
|
125
|
-
): void {
|
|
126
|
-
sendSessionUpdate({
|
|
127
|
-
sessionUpdate: "usage_update",
|
|
128
|
-
inputTokens,
|
|
129
|
-
outputTokens,
|
|
130
|
-
cacheCreationInputTokens: 0,
|
|
131
|
-
cacheReadInputTokens: 0,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Permission bridge ───────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
let nextPermissionId = 1;
|
|
138
|
-
const pendingPermissions = new Map<
|
|
139
|
-
number,
|
|
140
|
-
{ resolve: (outcome: string) => void }
|
|
141
|
-
>();
|
|
142
|
-
|
|
143
|
-
function buildPermissionToolCall(
|
|
144
|
-
title: string,
|
|
145
|
-
kind: string,
|
|
146
|
-
metadata: Record<string, unknown>,
|
|
147
|
-
toolCallId: string,
|
|
148
|
-
): { toolCall: Record<string, unknown> } {
|
|
149
|
-
const args = (metadata.args ?? {}) as Record<string, unknown>;
|
|
150
|
-
|
|
151
|
-
// Map agent-sh permission kinds → ACP tool call shapes
|
|
152
|
-
if (kind === "file-write") {
|
|
153
|
-
// File edit/write — send diff content block + rawInput for agent-shell
|
|
154
|
-
const content: unknown[] = [];
|
|
155
|
-
const rawInput: Record<string, unknown> = {};
|
|
156
|
-
|
|
157
|
-
// Set path for title display
|
|
158
|
-
const filePath = (args.path as string) ?? "";
|
|
159
|
-
rawInput.path = filePath;
|
|
160
|
-
rawInput.file_path = filePath;
|
|
161
|
-
|
|
162
|
-
// For edit_file: old_str/new_str so agent-shell can render a diff
|
|
163
|
-
if (typeof args.old_text === "string") {
|
|
164
|
-
rawInput.old_str = args.old_text;
|
|
165
|
-
rawInput.new_str = args.new_text ?? "";
|
|
166
|
-
content.push({
|
|
167
|
-
type: "diff",
|
|
168
|
-
oldText: args.old_text,
|
|
169
|
-
newText: args.new_text ?? "",
|
|
170
|
-
path: filePath,
|
|
171
|
-
});
|
|
172
|
-
} else if (typeof args.content === "string") {
|
|
173
|
-
// write_file (new file or full overwrite)
|
|
174
|
-
rawInput.new_str = args.content;
|
|
175
|
-
rawInput.old_str = "";
|
|
176
|
-
content.push({
|
|
177
|
-
type: "diff",
|
|
178
|
-
oldText: "",
|
|
179
|
-
newText: args.content,
|
|
180
|
-
path: filePath,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (typeof args.description === "string") {
|
|
185
|
-
rawInput.description = args.description;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
toolCall: {
|
|
190
|
-
toolCallId,
|
|
191
|
-
title,
|
|
192
|
-
status: "pending",
|
|
193
|
-
kind: "diff",
|
|
194
|
-
content,
|
|
195
|
-
rawInput,
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Generic tool call (bash, etc.)
|
|
201
|
-
const rawInput: Record<string, unknown> = {};
|
|
202
|
-
if (typeof args.command === "string") {
|
|
203
|
-
rawInput.command = args.command;
|
|
204
|
-
}
|
|
205
|
-
if (typeof args.description === "string") {
|
|
206
|
-
rawInput.description = args.description;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
toolCall: {
|
|
211
|
-
toolCallId,
|
|
212
|
-
title,
|
|
213
|
-
status: "pending",
|
|
214
|
-
kind: kind === "tool-call" ? "execute" : kind,
|
|
215
|
-
content: [],
|
|
216
|
-
rawInput,
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function requestPermission(
|
|
222
|
-
title: string,
|
|
223
|
-
kind: string,
|
|
224
|
-
metadata: Record<string, unknown>,
|
|
225
|
-
toolCallId?: string,
|
|
226
|
-
): Promise<string> {
|
|
227
|
-
const id = nextPermissionId++;
|
|
228
|
-
const tcId = toolCallId ?? `perm-${id}`;
|
|
229
|
-
return new Promise((resolve) => {
|
|
230
|
-
pendingPermissions.set(id, { resolve });
|
|
231
|
-
const { toolCall } = buildPermissionToolCall(title, kind, metadata, tcId);
|
|
232
|
-
send({
|
|
233
|
-
jsonrpc: "2.0",
|
|
234
|
-
method: "session/request_permission",
|
|
235
|
-
id,
|
|
236
|
-
params: {
|
|
237
|
-
toolCall,
|
|
238
|
-
options: [
|
|
239
|
-
{ id: "accepted", name: "Accept", description: "Accept this action" },
|
|
240
|
-
{ id: "rejected", name: "Reject", description: "Reject this action" },
|
|
241
|
-
{ id: "always", name: "Always allow", description: "Always allow for this session" },
|
|
242
|
-
],
|
|
243
|
-
},
|
|
244
|
-
} as any);
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ── Core setup ──────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
function parseArgs(): { model?: string; provider?: string } {
|
|
251
|
-
const args = process.argv.slice(2);
|
|
252
|
-
const result: Record<string, string> = {};
|
|
253
|
-
for (let i = 0; i < args.length; i++) {
|
|
254
|
-
if (args[i] === "--model" && args[i + 1]) result.model = args[++i];
|
|
255
|
-
if (args[i] === "--provider" && args[i + 1]) result.provider = args[++i];
|
|
256
|
-
}
|
|
257
|
-
return result;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const cliArgs = parseArgs();
|
|
261
|
-
let core: AgentShellCore | null = null;
|
|
262
|
-
let sessionId: string | null = null;
|
|
263
|
-
let sessionCwd: string = process.cwd();
|
|
264
|
-
|
|
265
|
-
// Track tool output chunks per toolCallId so we can send accumulated content
|
|
266
|
-
const toolOutputBuffers = new Map<string, string>();
|
|
267
|
-
|
|
268
|
-
// Track the active prompt request id so we can respond when processing is done
|
|
269
|
-
let activePromptRequestId: number | string | null = null;
|
|
270
|
-
|
|
271
|
-
// Track always-allowed permission kinds
|
|
272
|
-
const alwaysAllowed = new Set<string>();
|
|
273
|
-
|
|
274
|
-
// Track in-flight async operations so stdin end can wait
|
|
275
|
-
let pendingOp: Promise<void> = Promise.resolve();
|
|
276
|
-
|
|
277
|
-
// ── Wire agent-sh events → ACP notifications ───────────────────────
|
|
278
|
-
|
|
279
|
-
function wireEvents(core: AgentShellCore): void {
|
|
280
|
-
const { bus } = core;
|
|
281
|
-
|
|
282
|
-
bus.on("agent:response-chunk", ({ blocks }) => {
|
|
283
|
-
for (const block of blocks) {
|
|
284
|
-
if (block.type === "text") {
|
|
285
|
-
sendTextChunk(block.text);
|
|
286
|
-
}
|
|
287
|
-
// code-block blocks are sent as text (agent-shell renders markdown)
|
|
288
|
-
if (block.type === "code-block") {
|
|
289
|
-
sendTextChunk("```" + block.language + "\n" + block.code + "\n```");
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
bus.on("agent:thinking-chunk", ({ text }) => {
|
|
295
|
-
sendThinkingChunk(text);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
bus.on("agent:tool-started", (e) => {
|
|
299
|
-
const id = e.toolCallId ?? `tool-${Date.now()}`;
|
|
300
|
-
toolOutputBuffers.set(id, "");
|
|
301
|
-
sendToolCall(id, e.title, e.kind ?? "tool", e.rawInput);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
bus.on("agent:tool-output-chunk", ({ chunk }) => {
|
|
305
|
-
// Accumulate — we don't know toolCallId here, but only one tool runs at a time
|
|
306
|
-
// in sequential mode. For parallel tools this is best-effort.
|
|
307
|
-
for (const [id, buf] of toolOutputBuffers) {
|
|
308
|
-
toolOutputBuffers.set(id, buf + chunk);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
bus.on("agent:tool-completed", (e) => {
|
|
313
|
-
const id = e.toolCallId ?? [...toolOutputBuffers.keys()].pop() ?? "unknown";
|
|
314
|
-
const output = toolOutputBuffers.get(id) ?? "";
|
|
315
|
-
toolOutputBuffers.delete(id);
|
|
316
|
-
|
|
317
|
-
const status = e.exitCode === 0 || e.exitCode === null ? "completed" : "failed";
|
|
318
|
-
const content: AcpContentBlock[] = output
|
|
319
|
-
? [{ type: "text", text: output }]
|
|
320
|
-
: [];
|
|
321
|
-
sendToolCallUpdate(id, status, content, e.kind);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
bus.on("agent:usage", ({ prompt_tokens, completion_tokens }) => {
|
|
325
|
-
sendUsageUpdate(prompt_tokens, completion_tokens);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
bus.on("agent:processing-done", () => {
|
|
329
|
-
if (activePromptRequestId !== null) {
|
|
330
|
-
sendResult(activePromptRequestId, { stopReason: "end_turn" });
|
|
331
|
-
activePromptRequestId = null;
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
bus.on("agent:error", ({ message }) => {
|
|
336
|
-
if (activePromptRequestId !== null) {
|
|
337
|
-
sendError(activePromptRequestId, -32603, message);
|
|
338
|
-
activePromptRequestId = null;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
bus.on("agent:cancelled", () => {
|
|
343
|
-
if (activePromptRequestId !== null) {
|
|
344
|
-
sendResult(activePromptRequestId, { stopReason: "cancelled" });
|
|
345
|
-
activePromptRequestId = null;
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// Permission gating — auto-approve all tool calls.
|
|
350
|
-
// agent-sh's built-in tools handle their own safety; the ACP layer
|
|
351
|
-
// doesn't add a second permission gate. If you want to bridge
|
|
352
|
-
// permissions to agent-shell's UI, replace this with the
|
|
353
|
-
// requestPermission() flow.
|
|
354
|
-
bus.onPipeAsync("permission:request", async (payload) => {
|
|
355
|
-
payload.decision = { outcome: "approved" };
|
|
356
|
-
return payload;
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ── ACP method handlers ─────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
function getModelsPayload(): Record<string, unknown> | undefined {
|
|
363
|
-
if (!core) return undefined;
|
|
364
|
-
const info = core.bus.emitPipe("config:get-models", { models: [], active: null });
|
|
365
|
-
if (!info.models.length) return undefined;
|
|
366
|
-
return {
|
|
367
|
-
currentModelId: info.active ?? info.models[0]?.model,
|
|
368
|
-
availableModels: info.models.map((m) => ({
|
|
369
|
-
modelId: m.model,
|
|
370
|
-
name: m.provider ? `${m.provider}/${m.model}` : m.model,
|
|
371
|
-
description: m.provider ? `Provider: ${m.provider}` : "",
|
|
372
|
-
})),
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function handleInitialize(id: number | string): void {
|
|
377
|
-
sendResult(id, {
|
|
378
|
-
agentCapabilities: {
|
|
379
|
-
promptCapabilities: {
|
|
380
|
-
image: false,
|
|
381
|
-
embeddedContext: true,
|
|
382
|
-
},
|
|
383
|
-
sessionCapabilities: {},
|
|
384
|
-
},
|
|
385
|
-
modes: {
|
|
386
|
-
currentModeId: "default",
|
|
387
|
-
availableModes: [
|
|
388
|
-
{ id: "default", name: "Default", description: "Standard mode" },
|
|
389
|
-
],
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async function handleSessionNew(id: number | string, params: Record<string, unknown>): Promise<void> {
|
|
395
|
-
sessionCwd = (params.cwd as string) ?? process.cwd();
|
|
396
|
-
process.chdir(sessionCwd);
|
|
397
|
-
|
|
398
|
-
// Create core lazily on first session
|
|
399
|
-
if (!core) {
|
|
400
|
-
core = createCore({
|
|
401
|
-
model: cliArgs.model,
|
|
402
|
-
provider: cliArgs.provider,
|
|
403
|
-
});
|
|
404
|
-
wireEvents(core);
|
|
405
|
-
|
|
406
|
-
const extCtx = core.extensionContext({ quit: () => process.exit(0) });
|
|
407
|
-
const settings = getSettings();
|
|
408
|
-
|
|
409
|
-
// Load built-in extensions first (agent-backend, slash-commands, etc.)
|
|
410
|
-
// Skip TUI-only extensions that don't apply in headless mode
|
|
411
|
-
const headlessDisabled = [
|
|
412
|
-
"tui-renderer",
|
|
413
|
-
"file-autocomplete",
|
|
414
|
-
"overlay-agent",
|
|
415
|
-
...(settings.disabledBuiltins ?? []),
|
|
416
|
-
];
|
|
417
|
-
await loadBuiltinExtensions(extCtx, headlessDisabled);
|
|
418
|
-
|
|
419
|
-
// Load user extensions with a timeout (some may hang in headless mode)
|
|
420
|
-
const TIMEOUT_MS = 10000;
|
|
421
|
-
await Promise.race([
|
|
422
|
-
loadExtensions(extCtx),
|
|
423
|
-
new Promise<void>((_, reject) =>
|
|
424
|
-
setTimeout(() => reject(new Error(`Extension loading timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS),
|
|
425
|
-
),
|
|
426
|
-
]).catch((err) => {
|
|
427
|
-
process.stderr.write(`Warning: ${err instanceof Error ? err.message : err}\n`);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// Signal deferred-init listeners (agent-backend) that the provider
|
|
431
|
-
// registry is complete — they resolve their LLM config on this event.
|
|
432
|
-
core.bus.emit("core:extensions-loaded", {});
|
|
433
|
-
|
|
434
|
-
core.activateBackend();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
sessionId = `session-${Date.now()}`;
|
|
438
|
-
const result: Record<string, unknown> = {
|
|
439
|
-
sessionId,
|
|
440
|
-
modes: {
|
|
441
|
-
currentModeId: "default",
|
|
442
|
-
availableModes: [
|
|
443
|
-
{ id: "default", name: "Default", description: "Standard mode" },
|
|
444
|
-
],
|
|
445
|
-
},
|
|
446
|
-
};
|
|
447
|
-
const models = getModelsPayload();
|
|
448
|
-
if (models) result.models = models;
|
|
449
|
-
sendResult(id, result);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function handleSessionPrompt(id: number | string, params: Record<string, unknown>): void {
|
|
453
|
-
if (!core) {
|
|
454
|
-
sendError(id, -32603, "No active session");
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Extract text from prompt content blocks
|
|
459
|
-
const prompt = params.prompt as Array<{ type: string; text?: string; resource?: { text?: string } }>;
|
|
460
|
-
const parts: string[] = [];
|
|
461
|
-
for (const block of prompt) {
|
|
462
|
-
if (block.type === "text" && block.text) {
|
|
463
|
-
parts.push(block.text);
|
|
464
|
-
} else if (block.type === "resource" && block.resource?.text) {
|
|
465
|
-
parts.push(block.resource.text);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const query = parts.join("\n");
|
|
470
|
-
if (!query) {
|
|
471
|
-
sendResult(id, { stopReason: "end_turn" });
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Store the request id — we'll respond when agent:processing-done fires
|
|
476
|
-
activePromptRequestId = id;
|
|
477
|
-
core.bus.emit("agent:submit", { query });
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function handleSessionSetMode(id: number | string, _params: Record<string, unknown>): void {
|
|
481
|
-
// Acknowledge — agent-sh doesn't have distinct modes yet
|
|
482
|
-
sendResult(id, {});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// ── Message dispatcher ──────────────────────────────────────────────
|
|
486
|
-
|
|
487
|
-
function dispatch(msg: JsonRpcRequest): void {
|
|
488
|
-
const { method, params, id } = msg;
|
|
489
|
-
|
|
490
|
-
// Handle responses to our outgoing requests (permission responses)
|
|
491
|
-
if (!method && id !== undefined && (msg as any).result !== undefined) {
|
|
492
|
-
const pending = pendingPermissions.get(id as number);
|
|
493
|
-
if (pending) {
|
|
494
|
-
pendingPermissions.delete(id as number);
|
|
495
|
-
const result = (msg as any).result;
|
|
496
|
-
const outcome = result?.outcome?.optionId ?? result?.outcome?.outcome ?? "rejected";
|
|
497
|
-
pending.resolve(outcome);
|
|
498
|
-
}
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (!id && !method) return; // ignore malformed
|
|
503
|
-
|
|
504
|
-
switch (method) {
|
|
505
|
-
case "initialize":
|
|
506
|
-
handleInitialize(id!);
|
|
507
|
-
break;
|
|
508
|
-
case "session/new":
|
|
509
|
-
pendingOp = handleSessionNew(id!, params ?? {}).catch((err) => {
|
|
510
|
-
sendError(id!, -32603, err instanceof Error ? err.message : String(err));
|
|
511
|
-
});
|
|
512
|
-
break;
|
|
513
|
-
case "session/prompt":
|
|
514
|
-
handleSessionPrompt(id!, params ?? {});
|
|
515
|
-
break;
|
|
516
|
-
case "session/set_mode":
|
|
517
|
-
handleSessionSetMode(id!, params ?? {});
|
|
518
|
-
break;
|
|
519
|
-
case "session/set_model":
|
|
520
|
-
if (core && params?.modelId) {
|
|
521
|
-
core.bus.emit("config:switch-model", { model: params.modelId as string });
|
|
522
|
-
}
|
|
523
|
-
sendResult(id!, {
|
|
524
|
-
models: getModelsPayload() ?? {},
|
|
525
|
-
});
|
|
526
|
-
break;
|
|
527
|
-
case "session/cancel":
|
|
528
|
-
if (core) {
|
|
529
|
-
core.bus.emit("agent:cancel-request", {});
|
|
530
|
-
}
|
|
531
|
-
// Notification — no response needed
|
|
532
|
-
break;
|
|
533
|
-
default:
|
|
534
|
-
if (id !== undefined) {
|
|
535
|
-
sendError(id, -32601, `Method not found: ${method}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ── Stdin line reader ───────────────────────────────────────────────
|
|
541
|
-
|
|
542
|
-
let buffer = "";
|
|
543
|
-
|
|
544
|
-
process.stdin.setEncoding("utf-8");
|
|
545
|
-
process.stdin.on("data", (chunk: string) => {
|
|
546
|
-
buffer += chunk;
|
|
547
|
-
let newlineIdx: number;
|
|
548
|
-
while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
|
|
549
|
-
const line = buffer.slice(0, newlineIdx).trim();
|
|
550
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
551
|
-
if (!line) continue;
|
|
552
|
-
try {
|
|
553
|
-
const msg = JSON.parse(line) as JsonRpcRequest;
|
|
554
|
-
dispatch(msg);
|
|
555
|
-
} catch {
|
|
556
|
-
// Skip malformed JSON
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
process.stdin.on("end", async () => {
|
|
562
|
-
// Wait for any in-flight async operations (e.g. session/new) to settle
|
|
563
|
-
await pendingOp;
|
|
564
|
-
core?.kill();
|
|
565
|
-
process.exit(0);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
// Log unhandled rejections to stderr (don't crash, but don't swallow silently)
|
|
569
|
-
process.on("unhandledRejection", (err) => {
|
|
570
|
-
process.stderr.write(`[ash-acp-bridge] unhandled rejection: ${err instanceof Error ? err.message : err}\n`);
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// Redirect stderr from agent-sh internals so it doesn't pollute the protocol
|
|
574
|
-
// (agent-shell reads stdout only; stderr goes to its log)
|