@netnodeag/kraftwerk 0.43.0 → 0.44.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 +48 -0
- package/dist/acp.d.ts +131 -1
- package/dist/acp.js +163 -2
- package/dist/harnesses/acp.js +9 -5
- package/dist/inspector/chat/acp.d.ts +48 -2
- package/dist/inspector/chat/acp.js +471 -47
- package/dist/inspector/chat/backend.d.ts +30 -3
- package/dist/inspector/chat/pi.js +4 -1
- package/dist/inspector/chat/sessions.d.ts +60 -2
- package/dist/inspector/chat/sessions.js +274 -16
- package/dist/inspector/chat/store.d.ts +12 -0
- package/dist/inspector/chat/store.js +46 -0
- package/dist/inspector/chat/types.d.ts +185 -2
- package/dist/inspector/server.js +116 -5
- package/inspector/dist/assets/{dist-2tkXRwU5.js → dist-BBzscIhY.js} +1 -1
- package/inspector/dist/assets/{dist-Ck7zPi0Z.js → dist-BDk9auN5.js} +1 -1
- package/inspector/dist/assets/dist-BktrTMXT.js +1 -0
- package/inspector/dist/assets/{dist-LuoAT8wo.js → dist-BprKOWfC.js} +1 -1
- package/inspector/dist/assets/{dist-BoDJE1PN.js → dist-CLig8JSu.js} +1 -1
- package/inspector/dist/assets/{dist-CypKz49n.js → dist-Cq-0-mqe.js} +1 -1
- package/inspector/dist/assets/{dist-Rc-i9obk.js → dist-DVe7MUnQ.js} +1 -1
- package/inspector/dist/assets/{dist-C-Yu0i8M.js → dist-DqfkVJaW.js} +1 -1
- package/inspector/dist/assets/{dist-NNs7Vtl3.js → dist-S-LgiFkD.js} +1 -1
- package/inspector/dist/assets/{dist-FHTVbzBi.js → dist-SBBqiARl.js} +1 -1
- package/inspector/dist/assets/{dist-BX5KQfrx.js → dist-wCqvkQEl.js} +1 -1
- package/inspector/dist/assets/{editor-BI5xRg8C.js → editor-D__mCVTd.js} +3 -3
- package/inspector/dist/assets/index-DPdYC76U.css +1 -0
- package/inspector/dist/assets/index-De7hHAxt.js +219 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +2 -2
- package/inspector/dist/assets/dist-Q3GFwcs6.js +0 -1
- package/inspector/dist/assets/index-BUp72TiG.js +0 -218
- package/inspector/dist/assets/index-h5Vh1ZaF.css +0 -1
package/README.md
CHANGED
|
@@ -290,6 +290,46 @@ and write knowledge through `kraftwerk knowledge`, with writes stamped with
|
|
|
290
290
|
the agent's own actor, `<slug>/<harness>`. So the agent triggers its own
|
|
291
291
|
workflows when a request matches, and keeps its bundles current.
|
|
292
292
|
|
|
293
|
+
A chat keeps the agent's own session. The ACP session id is stored with the
|
|
294
|
+
chat (`sessions` in `meta.json`), and the next process — after an inspector
|
|
295
|
+
restart, or once the idle reaper released the agent — resumes it over
|
|
296
|
+
`session/resume`, so the agent continues with its memory rather than with
|
|
297
|
+
the transcript as a summary. A session that cannot be resumed (its
|
|
298
|
+
transcript is gone) falls back to a fresh one and says so in the thread.
|
|
299
|
+
"Fork" branches a chat: a new chat with the same transcript whose agent
|
|
300
|
+
continues from a copy of the session (`session/fork`, claude), leaving the
|
|
301
|
+
original as it is. Chats over ACP negotiate claude's native subagent and
|
|
302
|
+
async task streams: a delegated subagent shows as its own card with its
|
|
303
|
+
stream folded underneath, background work (backgrounded shells, monitors)
|
|
304
|
+
as task cards that outlive the tool call, and a context compaction as a
|
|
305
|
+
"compact" card with the token counts. Session failures the harness reports
|
|
306
|
+
(a rate limit, an expired login, a provider outage) arrive structured, not
|
|
307
|
+
as prose: a card names the category and the action the harness recommends,
|
|
308
|
+
retry, sign in again, or start a fresh session
|
|
309
|
+
(`POST /api/chats/:id/reset-session` forgets the stored session id).
|
|
310
|
+
|
|
311
|
+
The rest of what the adapters announce is in the chat too. The agent's
|
|
312
|
+
plan is a checklist card that updates in place. Its questions (Claude's
|
|
313
|
+
AskUserQuestion, MCP elicitations) are form cards answered in the thread —
|
|
314
|
+
unattended sessions skip an unanswered question after the same deadline as
|
|
315
|
+
permissions. A message sent while the agent works is steered into the
|
|
316
|
+
running turn instead of waiting (`POST /api/chats/:id/steer`). The header
|
|
317
|
+
shows who the agent is signed in as, and in expert mode its context use
|
|
318
|
+
and cost plus the model and thinking settings it lets you change live
|
|
319
|
+
(`POST /api/chats/:id/config`). The agent's slash commands join the
|
|
320
|
+
skills in the `/` menu. Each turn ends with the files the agent says it
|
|
321
|
+
changed. Background tasks that can be stopped have a stop button
|
|
322
|
+
(`POST /api/chats/:id/task-stop`). Deleting a chat deletes the agent's own
|
|
323
|
+
sessions with it, and "continue a session" on the new-chat screen lists
|
|
324
|
+
the agent's sessions in the project (`GET /api/agent-sessions`) to pick
|
|
325
|
+
one up as a chat.
|
|
326
|
+
|
|
327
|
+
Files dropped, pasted or attached in the composer go with the message
|
|
328
|
+
(`POST /api/chats/:id/attachments`, then `attachments` on the message):
|
|
329
|
+
they are stored under the chat's folder, images reach the agent as image
|
|
330
|
+
blocks (a screenshot is seen, not described), text files as embedded
|
|
331
|
+
resources, everything else by path.
|
|
332
|
+
|
|
293
333
|
Model and effort ride on backend-specific channels. The claude adapter takes
|
|
294
334
|
the model via ACP session options and the thinking budget via
|
|
295
335
|
`MAX_THINKING_TOKENS`. Codex gets a `CODEX_CONFIG` env override (`model`,
|
|
@@ -461,6 +501,14 @@ browser. API: `GET/POST /api/channels`, `GET/PUT/DELETE /api/channels/:slug`,
|
|
|
461
501
|
`POST /api/channels/from-chat {chatId, name, members}`; messages go through
|
|
462
502
|
the chat endpoint with `from` for the poster's name.
|
|
463
503
|
|
|
504
|
+
Each member chip in a channel shows what that agent is on: the message
|
|
505
|
+
that last addressed it, and while it works, its current step (the tool it
|
|
506
|
+
is using, else the start of its reply). Clicking a chip opens the agent's
|
|
507
|
+
own session — its stream alone, humans' messages for context, tool
|
|
508
|
+
activity always visible — and a stop button interrupts just that agent
|
|
509
|
+
(`POST /api/chats/:id/cancel {agent}`); "stop all" in the header
|
|
510
|
+
interrupts every agent working in the channel.
|
|
511
|
+
|
|
464
512
|
## Knowledge
|
|
465
513
|
|
|
466
514
|
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
|
-
|
|
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: {
|
|
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
|
+
}
|
package/dist/harnesses/acp.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
2
|
-
|
|
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>;
|