@netnodeag/kraftwerk 0.43.0 → 0.45.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.
Files changed (38) hide show
  1. package/README.md +68 -0
  2. package/dist/acp.d.ts +131 -1
  3. package/dist/acp.js +163 -2
  4. package/dist/harnesses/acp.js +9 -5
  5. package/dist/inspector/chat/acp.d.ts +48 -2
  6. package/dist/inspector/chat/acp.js +471 -47
  7. package/dist/inspector/chat/backend.d.ts +30 -3
  8. package/dist/inspector/chat/pi.js +4 -1
  9. package/dist/inspector/chat/sessions.d.ts +60 -2
  10. package/dist/inspector/chat/sessions.js +274 -16
  11. package/dist/inspector/chat/store.d.ts +12 -0
  12. package/dist/inspector/chat/store.js +46 -0
  13. package/dist/inspector/chat/types.d.ts +185 -2
  14. package/dist/inspector/runner.js +29 -4
  15. package/dist/inspector/runs.d.ts +5 -0
  16. package/dist/inspector/runs.js +56 -16
  17. package/dist/inspector/server.js +133 -7
  18. package/inspector/dist/assets/{dist-LuoAT8wo.js → dist-3XKl7K9N.js} +1 -1
  19. package/inspector/dist/assets/{dist-NNs7Vtl3.js → dist-BPzx-u5t.js} +1 -1
  20. package/inspector/dist/assets/dist-BnF1Rm2M.js +1 -0
  21. package/inspector/dist/assets/{dist-Rc-i9obk.js → dist-CGccQQRD.js} +1 -1
  22. package/inspector/dist/assets/{dist-C-Yu0i8M.js → dist-CIjxdHNW.js} +1 -1
  23. package/inspector/dist/assets/{dist-FHTVbzBi.js → dist-CdchVWU8.js} +1 -1
  24. package/inspector/dist/assets/{dist-2tkXRwU5.js → dist-DCZKo3cg.js} +1 -1
  25. package/inspector/dist/assets/{dist-CypKz49n.js → dist-DJnrih_T.js} +1 -1
  26. package/inspector/dist/assets/{dist-Ck7zPi0Z.js → dist-DWGd5Kj_.js} +1 -1
  27. package/inspector/dist/assets/{dist-BX5KQfrx.js → dist-DgaFP-8o.js} +1 -1
  28. package/inspector/dist/assets/{dist-BoDJE1PN.js → dist-bVw1IlxS.js} +1 -1
  29. package/inspector/dist/assets/{editor-BI5xRg8C.js → editor-DITzQiO1.js} +3 -3
  30. package/inspector/dist/assets/index-CS1G-A97.css +1 -0
  31. package/inspector/dist/assets/index-CiJ79dQy.js +219 -0
  32. package/inspector/dist/assets/roboto-latin-600-normal-CzqH9ZEY.woff2 +0 -0
  33. package/inspector/dist/assets/roboto-latin-600-normal-X4S5XP0J.woff +0 -0
  34. package/inspector/dist/index.html +2 -2
  35. package/package.json +2 -2
  36. package/inspector/dist/assets/dist-Q3GFwcs6.js +0 -1
  37. package/inspector/dist/assets/index-BUp72TiG.js +0 -218
  38. package/inspector/dist/assets/index-h5Vh1ZaF.css +0 -1
package/README.md CHANGED
@@ -237,6 +237,26 @@ pass-through. `--run-id` pins the run folder name for external triggers, which
237
237
  is what the inspector uses. `runner.json` in the run dir records container,
238
238
  exit code, and timing.
239
239
 
240
+ ### Runs in the inspector
241
+
242
+ The runs screen shows every folder under `output/runs/`, live ones with
243
+ their phase timeline, finished ones opening on their artifacts. A run's
244
+ status comes from `trace.jsonl` first: a `run_summary` settles it, a failed
245
+ or blocked phase fails it, and a trace nobody has written to for fifteen
246
+ minutes counts as aborted. Two markers settle it earlier: `runner.json`
247
+ (sandbox container exit code) and `trigger.json`, which the inspector
248
+ writes when a launcher it started exits. A folder that never got a trace at
249
+ all — the launcher died first, typically over a missing env var, and only
250
+ `trigger.log` tells why — ages by its newest file and shows as failed
251
+ instead of running forever; its workflow name is read from the run id.
252
+
253
+ Every live run has a stop button (`POST /api/runs/:id/stop`): it ends the
254
+ detached launcher process group for a local run this inspector started, or
255
+ `docker stop`s the `kw-<run-id>` container for a sandboxed one, and answers
256
+ 404 when neither exists (a run started from the CLI in another terminal).
257
+ Every finished run has a remove button (`DELETE /api/runs/:id`) that deletes
258
+ the folder; a run that still looks live is refused with 409 — stop it first.
259
+
240
260
  `run` prompts for whatever is missing, both the workflow picker and the
241
261
  request input. Invalid workflows show up red in `list` with their validation
242
262
  error instead of breaking the listing. `create` is meant to be run BY an LLM
@@ -290,6 +310,46 @@ and write knowledge through `kraftwerk knowledge`, with writes stamped with
290
310
  the agent's own actor, `<slug>/<harness>`. So the agent triggers its own
291
311
  workflows when a request matches, and keeps its bundles current.
292
312
 
313
+ A chat keeps the agent's own session. The ACP session id is stored with the
314
+ chat (`sessions` in `meta.json`), and the next process — after an inspector
315
+ restart, or once the idle reaper released the agent — resumes it over
316
+ `session/resume`, so the agent continues with its memory rather than with
317
+ the transcript as a summary. A session that cannot be resumed (its
318
+ transcript is gone) falls back to a fresh one and says so in the thread.
319
+ "Fork" branches a chat: a new chat with the same transcript whose agent
320
+ continues from a copy of the session (`session/fork`, claude), leaving the
321
+ original as it is. Chats over ACP negotiate claude's native subagent and
322
+ async task streams: a delegated subagent shows as its own card with its
323
+ stream folded underneath, background work (backgrounded shells, monitors)
324
+ as task cards that outlive the tool call, and a context compaction as a
325
+ "compact" card with the token counts. Session failures the harness reports
326
+ (a rate limit, an expired login, a provider outage) arrive structured, not
327
+ as prose: a card names the category and the action the harness recommends,
328
+ retry, sign in again, or start a fresh session
329
+ (`POST /api/chats/:id/reset-session` forgets the stored session id).
330
+
331
+ The rest of what the adapters announce is in the chat too. The agent's
332
+ plan is a checklist card that updates in place. Its questions (Claude's
333
+ AskUserQuestion, MCP elicitations) are form cards answered in the thread —
334
+ unattended sessions skip an unanswered question after the same deadline as
335
+ permissions. A message sent while the agent works is steered into the
336
+ running turn instead of waiting (`POST /api/chats/:id/steer`). The header
337
+ shows who the agent is signed in as, and in expert mode its context use
338
+ and cost plus the model and thinking settings it lets you change live
339
+ (`POST /api/chats/:id/config`). The agent's slash commands join the
340
+ skills in the `/` menu. Each turn ends with the files the agent says it
341
+ changed. Background tasks that can be stopped have a stop button
342
+ (`POST /api/chats/:id/task-stop`). Deleting a chat deletes the agent's own
343
+ sessions with it, and "continue a session" on the new-chat screen lists
344
+ the agent's sessions in the project (`GET /api/agent-sessions`) to pick
345
+ one up as a chat.
346
+
347
+ Files dropped, pasted or attached in the composer go with the message
348
+ (`POST /api/chats/:id/attachments`, then `attachments` on the message):
349
+ they are stored under the chat's folder, images reach the agent as image
350
+ blocks (a screenshot is seen, not described), text files as embedded
351
+ resources, everything else by path.
352
+
293
353
  Model and effort ride on backend-specific channels. The claude adapter takes
294
354
  the model via ACP session options and the thinking budget via
295
355
  `MAX_THINKING_TOKENS`. Codex gets a `CODEX_CONFIG` env override (`model`,
@@ -461,6 +521,14 @@ browser. API: `GET/POST /api/channels`, `GET/PUT/DELETE /api/channels/:slug`,
461
521
  `POST /api/channels/from-chat {chatId, name, members}`; messages go through
462
522
  the chat endpoint with `from` for the poster's name.
463
523
 
524
+ Each member chip in a channel shows what that agent is on: the message
525
+ that last addressed it, and while it works, its current step (the tool it
526
+ is using, else the start of its reply). Clicking a chip opens the agent's
527
+ own session — its stream alone, humans' messages for context, tool
528
+ activity always visible — and a stop button interrupts just that agent
529
+ (`POST /api/chats/:id/cancel {agent}`); "stop all" in the header
530
+ interrupts every agent working in the channel.
531
+
464
532
  ## Knowledge
465
533
 
466
534
  Alongside runs and chats, a project can keep curated knowledge as
package/dist/acp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { ClientSideConnection, type Client, type McpServer } from "@agentclientprotocol/sdk";
2
+ import { ClientSideConnection, type Client, type McpServer, type SessionConfigOption, type SessionModeState } from "@agentclientprotocol/sdk";
3
3
  import type { McpServerConfig } from "./harness.js";
4
4
  /**
5
5
  * Agent Client Protocol plumbing shared by the inspector's chats and the
@@ -23,6 +23,98 @@ export declare function adapterEnv(agent: AcpAgent, tuning: {
23
23
  }): NodeJS.ProcessEnv;
24
24
  /** Kraftwerk's MCP config (stdio or url) -> the protocol's session mcpServers entries. */
25
25
  export declare function acpMcpServers(servers?: Record<string, McpServerConfig>): McpServer[];
26
+ /**
27
+ * Session updates beyond the published ACP schema, from the "AIR"
28
+ * extension claude-agent-acp negotiates (agent-client-protocol#1992 for
29
+ * subagents; async tasks alongside). A client that advertises the
30
+ * capabilities gets Claude's subagents as independent child sessions
31
+ * (`subagent_spawned`, then ordinary updates carrying the child's session
32
+ * id) and its background work (backgrounded shells, monitors, workflows)
33
+ * as async tasks — instead of the flattened generic Agent/Task tool call.
34
+ *
35
+ * The SDK validates every session/update against its schema and does not
36
+ * know these kinds yet, so they never reach the Client through the SDK:
37
+ * connectAcp diverts them off the wire before the SDK sees them.
38
+ */
39
+ export type SubagentState = "completed" | "failed" | "cancelled" | "disconnected";
40
+ export type AsyncTaskState = "running" | "paused" | "completed" | "failed" | "stopped";
41
+ export type AcpExtensionUpdate = {
42
+ sessionUpdate: "subagent_spawned";
43
+ subagentSessionId: string;
44
+ name: string;
45
+ task: string;
46
+ } | {
47
+ sessionUpdate: "subagent_state_update";
48
+ subagentSessionId: string;
49
+ state: SubagentState;
50
+ } | {
51
+ sessionUpdate: "async_task_spawned";
52
+ asyncTaskId: string;
53
+ name: string;
54
+ taskType: string;
55
+ description: string;
56
+ showInTranscript?: boolean;
57
+ canStop?: boolean;
58
+ outputFilePath?: string;
59
+ toolCallId?: string;
60
+ } | {
61
+ sessionUpdate: "async_task_progress";
62
+ asyncTaskId: string;
63
+ description?: string;
64
+ summary?: string;
65
+ lastToolName?: string;
66
+ outputFilePath?: string;
67
+ toolCallId?: string;
68
+ } | {
69
+ sessionUpdate: "async_task_state_update";
70
+ asyncTaskId: string;
71
+ state: AsyncTaskState;
72
+ summary?: string;
73
+ outputFilePath?: string;
74
+ toolCallId?: string;
75
+ };
76
+ export interface AcpExtensionNotification {
77
+ sessionId: string;
78
+ update: AcpExtensionUpdate;
79
+ }
80
+ /**
81
+ * What kraftwerk advertises: the AIR extension's capability list in the
82
+ * client's `_meta` — native subagent and async task streams, session
83
+ * failures (rate limits, expired logins, provider outages) as structured
84
+ * `session_info_update` metadata with a recommended action instead of
85
+ * prose, and the per-turn file change report (see fileChangeReportMeta).
86
+ * Both adapters implement the extension; an adapter without it ignores
87
+ * the list and keeps its plain stream.
88
+ */
89
+ export declare function extensionClientMeta(): Record<string, unknown>;
90
+ /**
91
+ * Prompt `_meta` asking for a file change report at the end of the turn:
92
+ * the adapter gives the agent a `report_changed_files` tool and the
93
+ * result arrives as `session_info_update` metadata. One id per prompt.
94
+ */
95
+ export declare function fileChangeReportMeta(requestId: string): Record<string, unknown>;
96
+ /** Extension request methods the claude adapter offers (advertised in its initialize `_meta`). */
97
+ export declare const EXT_METHODS: {
98
+ /** Inject a message into the running turn. */
99
+ readonly steer: "_session/steering";
100
+ /** Stop one background task without cancelling the turn. */
101
+ readonly stopAsyncTask: "_session/async_task/stop";
102
+ };
103
+ /** Extension notifications an adapter pushes to the client. */
104
+ export declare const EXT_NOTIFICATIONS: {
105
+ /** Who the agent is signed in as (claude-agent-acp authStatus extension). */
106
+ readonly authStatus: "_auth/status_update";
107
+ };
108
+ /** The AIR payload for `capability` inside a `_meta`, if the peer sent one. */
109
+ export declare function airMeta<T = Record<string, unknown>>(meta: unknown, capability: string): T | undefined;
110
+ /** The extension notification on one wire line, if that is what the line is. */
111
+ export declare function parseExtensionLine(line: string): AcpExtensionNotification | null;
112
+ /**
113
+ * Filter the adapter's newline-delimited JSON-RPC output: extension
114
+ * session updates go to `onExtension`, everything else passes through
115
+ * byte-for-byte for the SDK. Lines may arrive split across chunks.
116
+ */
117
+ export declare function divertExtensionUpdates(source: ReadableStream<Uint8Array>, onExtension: (n: AcpExtensionNotification) => void): ReadableStream<Uint8Array>;
26
118
  export interface AcpProcess {
27
119
  child: ChildProcessWithoutNullStreams;
28
120
  conn: ClientSideConnection;
@@ -32,12 +124,50 @@ export interface AcpProcess {
32
124
  * `client`, and run the initialize handshake. Process failures reach the
33
125
  * caller through the callbacks (a failed spawn is otherwise an unhandled
34
126
  * 'error' event that takes the host process down).
127
+ *
128
+ * `onExtension` opts into the native subagent / async task streams (see
129
+ * AcpExtensionUpdate); without it the adapter keeps its generic tool-call
130
+ * representation of that work.
35
131
  */
36
132
  export declare function connectAcp(agent: AcpAgent, opts: {
37
133
  cwd: string;
38
134
  env: NodeJS.ProcessEnv;
39
135
  client: Client;
40
136
  clientName: string;
137
+ onExtension?: (n: AcpExtensionNotification) => void;
138
+ /** The client answers form elicitations (`createElicitation` on the Client): agents may ask questions. */
139
+ elicitation?: boolean;
41
140
  onError(err: Error): void;
42
141
  onClose(code: number | null, stderr: string): void;
43
142
  }): Promise<AcpProcess>;
143
+ /**
144
+ * Run `fn` against a short-lived adapter that opens no session: for
145
+ * listing or deleting the agent's own sessions. The adapter is gone when
146
+ * `fn` settles.
147
+ */
148
+ export declare function withAdapter<T>(agent: AcpAgent, cwd: string, fn: (conn: ClientSideConnection) => Promise<T>): Promise<T>;
149
+ /** The subset of the connection a session open needs (a test can stand in for it). */
150
+ export type SessionOpener = Pick<ClientSideConnection, "newSession" | "resumeSession">;
151
+ export interface OpenedSession {
152
+ sessionId: string;
153
+ modes?: SessionModeState | null;
154
+ /** Session settings the agent lets the client change (model, thinking depth, ...). */
155
+ configOptions?: SessionConfigOption[] | null;
156
+ /** The session continues an earlier one (same id, the agent's own history). */
157
+ resumed: boolean;
158
+ /** A resume was asked for but refused; a fresh session was opened instead. */
159
+ resumeError?: string;
160
+ }
161
+ /**
162
+ * Open a session: continue `resume` when given (session/resume — the
163
+ * agent's own transcript carries the history, no replay), else a new one.
164
+ * A refused resume (unknown id, adapter without the method, transcript
165
+ * gone) falls back to a new session and says so, instead of failing the
166
+ * chat: the conversation on disk is still there for the human, the agent
167
+ * just starts without it in context.
168
+ */
169
+ export declare function openSession(conn: SessionOpener, req: {
170
+ cwd: string;
171
+ mcpServers: McpServer[];
172
+ _meta?: Record<string, unknown>;
173
+ }, resume?: string): Promise<OpenedSession>;
package/dist/acp.js CHANGED
@@ -44,11 +44,117 @@ export function acpMcpServers(servers = {}) {
44
44
  env: Object.entries(cfg.env ?? {}).map(([n, value]) => ({ name: n, value })),
45
45
  });
46
46
  }
47
+ const EXTENSION_KINDS = new Set([
48
+ "subagent_spawned",
49
+ "subagent_state_update",
50
+ "async_task_spawned",
51
+ "async_task_progress",
52
+ "async_task_state_update",
53
+ ]);
54
+ // Cheap pre-check so only candidate lines are parsed.
55
+ const EXTENSION_LINE = /"sessionUpdate"\s*:\s*"(subagent_|async_task_)/;
56
+ /**
57
+ * What kraftwerk advertises: the AIR extension's capability list in the
58
+ * client's `_meta` — native subagent and async task streams, session
59
+ * failures (rate limits, expired logins, provider outages) as structured
60
+ * `session_info_update` metadata with a recommended action instead of
61
+ * prose, and the per-turn file change report (see fileChangeReportMeta).
62
+ * Both adapters implement the extension; an adapter without it ignores
63
+ * the list and keeps its plain stream.
64
+ */
65
+ export function extensionClientMeta() {
66
+ return {
67
+ jetbrains: {
68
+ air: { version: 1, capabilities: ["nativeSubagentSessions", "asyncTasks", "sessionFailure", "agentFileChangeReport"] },
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * Prompt `_meta` asking for a file change report at the end of the turn:
74
+ * the adapter gives the agent a `report_changed_files` tool and the
75
+ * result arrives as `session_info_update` metadata. One id per prompt.
76
+ */
77
+ export function fileChangeReportMeta(requestId) {
78
+ return { jetbrains: { air: { version: 1, agentFileChangeReportRequest: { version: 1, requestId } } } };
79
+ }
80
+ /** Extension request methods the claude adapter offers (advertised in its initialize `_meta`). */
81
+ export const EXT_METHODS = {
82
+ /** Inject a message into the running turn. */
83
+ steer: "_session/steering",
84
+ /** Stop one background task without cancelling the turn. */
85
+ stopAsyncTask: "_session/async_task/stop",
86
+ };
87
+ /** Extension notifications an adapter pushes to the client. */
88
+ export const EXT_NOTIFICATIONS = {
89
+ /** Who the agent is signed in as (claude-agent-acp authStatus extension). */
90
+ authStatus: "_auth/status_update",
91
+ };
92
+ /** The AIR payload for `capability` inside a `_meta`, if the peer sent one. */
93
+ export function airMeta(meta, capability) {
94
+ const air = meta?.jetbrains?.air;
95
+ const payload = air?.[capability];
96
+ return payload && typeof payload === "object" ? payload : undefined;
97
+ }
98
+ /** The extension notification on one wire line, if that is what the line is. */
99
+ export function parseExtensionLine(line) {
100
+ if (!EXTENSION_LINE.test(line))
101
+ return null;
102
+ try {
103
+ const msg = JSON.parse(line);
104
+ if (msg.method !== "session/update")
105
+ return null;
106
+ const update = msg.params?.update;
107
+ if (!update || !EXTENSION_KINDS.has(update.sessionUpdate ?? "") || typeof msg.params?.sessionId !== "string")
108
+ return null;
109
+ return { sessionId: msg.params.sessionId, update: update };
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ /**
116
+ * Filter the adapter's newline-delimited JSON-RPC output: extension
117
+ * session updates go to `onExtension`, everything else passes through
118
+ * byte-for-byte for the SDK. Lines may arrive split across chunks.
119
+ */
120
+ export function divertExtensionUpdates(source, onExtension) {
121
+ const decoder = new TextDecoder();
122
+ const encoder = new TextEncoder();
123
+ let buffered = "";
124
+ const handle = (line, ctrl, tail) => {
125
+ const ext = parseExtensionLine(line);
126
+ if (ext)
127
+ onExtension(ext);
128
+ else
129
+ ctrl.enqueue(encoder.encode(line + tail));
130
+ };
131
+ return source.pipeThrough(new TransformStream({
132
+ transform(chunk, ctrl) {
133
+ buffered += decoder.decode(chunk, { stream: true });
134
+ let nl;
135
+ while ((nl = buffered.indexOf("\n")) >= 0) {
136
+ const line = buffered.slice(0, nl);
137
+ buffered = buffered.slice(nl + 1);
138
+ handle(line, ctrl, "\n");
139
+ }
140
+ },
141
+ flush(ctrl) {
142
+ buffered += decoder.decode();
143
+ if (buffered)
144
+ handle(buffered, ctrl, "");
145
+ buffered = "";
146
+ },
147
+ }));
148
+ }
47
149
  /**
48
150
  * Spawn the adapter for `agent` in `cwd`, wire the ACP connection to
49
151
  * `client`, and run the initialize handshake. Process failures reach the
50
152
  * caller through the callbacks (a failed spawn is otherwise an unhandled
51
153
  * 'error' event that takes the host process down).
154
+ *
155
+ * `onExtension` opts into the native subagent / async task streams (see
156
+ * AcpExtensionUpdate); without it the adapter keeps its generic tool-call
157
+ * representation of that work.
52
158
  */
53
159
  export async function connectAcp(agent, opts) {
54
160
  const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
@@ -65,12 +171,67 @@ export async function connectAcp(agent, opts) {
65
171
  });
66
172
  child.on("error", opts.onError);
67
173
  child.on("close", (code) => opts.onClose(code, stderr.trim()));
68
- const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
174
+ let output = Readable.toWeb(child.stdout);
175
+ if (opts.onExtension)
176
+ output = divertExtensionUpdates(output, opts.onExtension);
177
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), output);
69
178
  const conn = new ClientSideConnection(() => opts.client, stream);
70
179
  await conn.initialize({
71
180
  protocolVersion: PROTOCOL_VERSION,
72
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
181
+ clientCapabilities: {
182
+ fs: { readTextFile: false, writeTextFile: false },
183
+ ...(opts.elicitation ? { elicitation: { form: {} } } : {}),
184
+ ...(opts.onExtension ? { _meta: extensionClientMeta() } : {}),
185
+ },
73
186
  clientInfo: { name: opts.clientName, version: "1.0.0" },
74
187
  });
75
188
  return { child, conn };
76
189
  }
190
+ /**
191
+ * Run `fn` against a short-lived adapter that opens no session: for
192
+ * listing or deleting the agent's own sessions. The adapter is gone when
193
+ * `fn` settles.
194
+ */
195
+ export async function withAdapter(agent, cwd, fn) {
196
+ const { child, conn } = await connectAcp(agent, {
197
+ cwd,
198
+ env: adapterEnv(agent, {}),
199
+ clientName: "kraftwerk",
200
+ client: {
201
+ sessionUpdate() { },
202
+ async requestPermission() {
203
+ return { outcome: { outcome: "cancelled" } };
204
+ },
205
+ },
206
+ onError() { },
207
+ onClose() { },
208
+ });
209
+ try {
210
+ return await fn(conn);
211
+ }
212
+ finally {
213
+ child.kill("SIGTERM");
214
+ }
215
+ }
216
+ /**
217
+ * Open a session: continue `resume` when given (session/resume — the
218
+ * agent's own transcript carries the history, no replay), else a new one.
219
+ * A refused resume (unknown id, adapter without the method, transcript
220
+ * gone) falls back to a new session and says so, instead of failing the
221
+ * chat: the conversation on disk is still there for the human, the agent
222
+ * just starts without it in context.
223
+ */
224
+ export async function openSession(conn, req, resume) {
225
+ let resumeError;
226
+ if (resume) {
227
+ try {
228
+ const res = await conn.resumeSession({ sessionId: resume, ...req });
229
+ return { sessionId: resume, modes: res.modes, configOptions: res.configOptions, resumed: true };
230
+ }
231
+ catch (err) {
232
+ resumeError = err.message;
233
+ }
234
+ }
235
+ const res = await conn.newSession(req);
236
+ return { sessionId: res.sessionId, modes: res.modes, configOptions: res.configOptions, resumed: false, ...(resumeError ? { resumeError } : {}) };
237
+ }
@@ -1,5 +1,5 @@
1
1
  import { RequestError, } from "@agentclientprotocol/sdk";
2
- import { acpMcpServers, adapterEnv, connectAcp } from "../acp.js";
2
+ import { acpMcpServers, adapterEnv, connectAcp, openSession } from "../acp.js";
3
3
  import { declineOption, unattendedMode } from "../inspector/chat/permissions.js";
4
4
  const live = new Map();
5
5
  // The run process is the adapters' only owner: never leave one behind.
@@ -31,7 +31,7 @@ export function toolTarget(u) {
31
31
  return target.length > 160 ? `${target.slice(0, 160)}…` : target;
32
32
  }
33
33
  const wait = (ms) => new Promise((r) => setTimeout(r, ms));
34
- async function open(agent, inv) {
34
+ async function open(agent, inv, resume) {
35
35
  let ended;
36
36
  const closed = new Promise((r) => (ended = r));
37
37
  const state = {
@@ -92,11 +92,14 @@ async function open(agent, inv) {
92
92
  state.child = child;
93
93
  state.conn = conn;
94
94
  try {
95
- const session = await conn.newSession({
95
+ const session = await openSession(conn, {
96
96
  cwd: inv.cwd,
97
97
  mcpServers: acpMcpServers(inv.mcpServers),
98
98
  ...(agent === "claude" ? { _meta: { claudeCode: { options: claudeSessionOptions(inv) } } } : {}),
99
- });
99
+ }, resume);
100
+ if (session.resumeError) {
101
+ console.log(` ⚠ could not resume session ${resume} (${session.resumeError}) — a new ${agent} session starts without the earlier phases in context`);
102
+ }
100
103
  state.sessionId = session.sessionId;
101
104
  const modeId = unattendedMode(agent, session.modes?.currentModeId);
102
105
  if (modeId) {
@@ -135,7 +138,8 @@ async function invoke(agent, inv) {
135
138
  live.delete(s.sessionId);
136
139
  s = undefined;
137
140
  }
138
- s ??= await open(agent, inv);
141
+ // No live adapter for the id (a new process, or the adapter died): resume it in a fresh one.
142
+ s ??= await open(agent, inv, inv.resume);
139
143
  const turn = { text: "", onToolUse: inv.onToolUse, pending: new Map() };
140
144
  s.turn = turn;
141
145
  const prevCost = s.cost;
@@ -1,2 +1,48 @@
1
- import type { BackendHooks, BackendTuning, ChatBackend } from "./backend.js";
2
- export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks, tuning?: BackendTuning): Promise<ChatBackend>;
1
+ import { type ContentBlock, type SessionConfigOption, type SessionNotification } from "@agentclientprotocol/sdk";
2
+ import { type AcpAgent, type AcpExtensionNotification } from "../../acp.js";
3
+ import type { BackendHooks, BackendTuning, ChatBackend, PromptFile } from "./backend.js";
4
+ import type { AuthStatus, ChatEvent, ConfigOption, ElicitationField, SessionFailure } from "./types.js";
5
+ /** The session failure a `session_info_update` carries, if it is one (AIR `sessionFailure` in `_meta`). */
6
+ export declare function failureOf(meta: unknown): SessionFailure | undefined;
7
+ /** The file change report a `session_info_update` carries, if it is one (AIR `agentFileChangeReport`). */
8
+ export declare function filesChangedOf(meta: unknown): Extract<ChatEvent, {
9
+ type: "files_changed";
10
+ }> | undefined;
11
+ /** The protocol's config options -> the chat's (select choices flattened, groups kept as a label). */
12
+ export declare function configOptions(options: SessionConfigOption[] | null | undefined): ConfigOption[];
13
+ /** The adapter's auth status notification -> the chat's. */
14
+ export declare function authStatusOf(params: unknown): AuthStatus | undefined;
15
+ /**
16
+ * A form elicitation's schema -> fields the UI can render. Claude's
17
+ * AskUserQuestion arrives as one select (oneOf) per question plus an
18
+ * "Other" text field flagged as its custom answer; MCP elicitations bring
19
+ * plain string/number/boolean/enum properties. Unknown property types
20
+ * degrade to text.
21
+ */
22
+ export declare function elicitationFields(schema: unknown): ElicitationField[];
23
+ /**
24
+ * One standard session update -> the chat event it means, or null for
25
+ * updates that carry no thread content (mode updates, ...).
26
+ * Updates from a session other than `root` come from a subagent and are
27
+ * tagged with that child's session id.
28
+ */
29
+ export declare function translateUpdate(root: string, params: SessionNotification): ChatEvent | null;
30
+ /** One extension update (subagent / async task lifecycle) -> its chat event. */
31
+ export declare function translateExtension(n: AcpExtensionNotification): ChatEvent | null;
32
+ /**
33
+ * A message with its files as ACP prompt blocks: images as image blocks
34
+ * (the agent sees the screenshot), text files as embedded resources, and
35
+ * anything else as a line naming the path — every file is on disk under
36
+ * the chat, so the agent can open it itself.
37
+ */
38
+ export declare function promptBlocks(text: string, files?: PromptFile[]): Promise<ContentBlock[]>;
39
+ /** The agent's own sessions in `cwd` (newest first), from a short-lived adapter. */
40
+ export declare function listAgentSessions(agent: AcpAgent, cwd: string): Promise<Array<{
41
+ sessionId: string;
42
+ title?: string;
43
+ updatedAt?: string;
44
+ cwd: string;
45
+ }>>;
46
+ /** Remove one of the agent's sessions for good (its transcript on disk), from a short-lived adapter. */
47
+ export declare function deleteAgentSession(agent: AcpAgent, cwd: string, sessionId: string): Promise<void>;
48
+ export declare function startAcpBackend(agent: AcpAgent, cwd: string, hooks: BackendHooks, tuning?: BackendTuning): Promise<ChatBackend>;