@netnodeag/kraftwerk 0.41.0 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -3
- package/dist/acp.d.ts +43 -0
- package/dist/acp.js +76 -0
- package/dist/agent.d.ts +3 -1
- package/dist/cli/doctor.js +10 -2
- package/dist/cli/knowledge.js +3 -3
- package/dist/cli/kraftwerk.js +13 -7
- package/dist/harness.d.ts +6 -0
- package/dist/harnesses/acp.d.ts +20 -0
- package/dist/harnesses/acp.js +195 -0
- package/dist/harnesses/registry.d.ts +2 -2
- package/dist/harnesses/registry.js +8 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inspector/chat/acp.js +22 -72
- package/dist/inspector/chat/sessions.js +1 -1
- package/dist/inspector/knowledge.d.ts +1 -1
- package/dist/inspector/knowledge.js +1 -1
- package/dist/inspector/runs.d.ts +1 -0
- package/dist/inspector/runs.js +1 -0
- package/dist/inspector/server.js +2 -2
- package/dist/inspector/workflows.d.ts +4 -0
- package/dist/inspector/workflows.js +16 -1
- package/dist/okf.d.ts +1 -1
- package/dist/okf.js +1 -1
- package/dist/run.d.ts +1 -0
- package/dist/run.js +8 -4
- package/dist/stats.d.ts +2 -0
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +59 -34
- package/inspector/dist/assets/{dist-CASrSR5E.js → dist-BJxiusVh.js} +1 -1
- package/inspector/dist/assets/{dist-g0pveAaF.js → dist-BnxCrUQZ.js} +1 -1
- package/inspector/dist/assets/{dist-CqbReWDG.js → dist-BpgTFX4N.js} +1 -1
- package/inspector/dist/assets/dist-C6Q5FWM0.js +1 -0
- package/inspector/dist/assets/{dist-DKds1beX.js → dist-C8aIUi2c.js} +1 -1
- package/inspector/dist/assets/{dist-BvHfHbzl.js → dist-Cn-NK0yx.js} +1 -1
- package/inspector/dist/assets/{dist-CoT4dTT9.js → dist-DQbdG8Cf.js} +1 -1
- package/inspector/dist/assets/{dist-Bj7XitTo.js → dist-DkSZQh-S.js} +1 -1
- package/inspector/dist/assets/{dist-h7tJ3T-Z.js → dist-OsHgcj_x.js} +1 -1
- package/inspector/dist/assets/{dist-BiIrAs8U.js → dist-cZmo-9Se.js} +1 -1
- package/inspector/dist/assets/{dist-BO4gDbWB.js → dist-z049FsVf.js} +1 -1
- package/inspector/dist/assets/{editor-BiyVJ7Wz.js → editor-BQkS4m10.js} +3 -3
- package/inspector/dist/assets/{index-lwVGUk9H.js → index-DGlIITXq.js} +16 -16
- package/inspector/dist/assets/index-DuZ9U4BZ.css +1 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/workflow.schema.json +18 -4
- package/inspector/dist/assets/dist-Dg2YcNTD.js +0 -1
- package/inspector/dist/assets/index-CHI0eOQa.css +0 -1
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ kraftwerk list # table: workflows, steps, agents (with
|
|
|
148
148
|
kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
149
149
|
kraftwerk run # interactive: pick workflow, type the request
|
|
150
150
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
151
|
-
kraftwerk knowledge #
|
|
151
|
+
kraftwerk knowledge # Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
152
152
|
kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
|
|
153
153
|
kraftwerk projects # every workspace on this machine; projects start|stop|forget <ref>
|
|
154
154
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
|
|
@@ -460,7 +460,7 @@ browser. API: `GET/POST /api/channels`, `GET/PUT/DELETE /api/channels/:slug`,
|
|
|
460
460
|
`POST /api/channels/from-chat {chatId, name, members}`; messages go through
|
|
461
461
|
the chat endpoint with `from` for the poster's name.
|
|
462
462
|
|
|
463
|
-
##
|
|
463
|
+
## Knowledge
|
|
464
464
|
|
|
465
465
|
Alongside runs and chats, a project can keep curated knowledge as
|
|
466
466
|
[OKF v0.2](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
|
|
@@ -503,7 +503,7 @@ trust tier per concept from that:
|
|
|
503
503
|
- Verified by a non-human actor means machine-confirmed.
|
|
504
504
|
- Verified by a `human:` actor means human-reviewed.
|
|
505
505
|
|
|
506
|
-
The inspector's
|
|
506
|
+
The inspector's Knowledge screen shows bundles, concepts with
|
|
507
507
|
trust, status and staleness badges, sources, and the bundle log. The verify
|
|
508
508
|
button records a `human:user` verification. "Curate in chat" opens a
|
|
509
509
|
knowledge-scoped chat whose agent knows the OKF essentials and writes through
|
|
@@ -583,6 +583,31 @@ Prerequisites per harness:
|
|
|
583
583
|
models reuse the Claude subscription OAuth, other vendors need their key in
|
|
584
584
|
the env (check with `pi auth check --provider deepseek`).
|
|
585
585
|
|
|
586
|
+
### Agent protocol
|
|
587
|
+
|
|
588
|
+
Phases can also run over the [Agent Client Protocol](https://agentclientprotocol.com)
|
|
589
|
+
— the same adapters the inspector's chats use (`claude-agent-acp`,
|
|
590
|
+
`codex-acp`), bundled with kraftwerk, so nothing needs to be on the PATH.
|
|
591
|
+
Set `protocol: acp` at the top of a workflow (every agent) or on one agent;
|
|
592
|
+
`runs-on` still picks the harness (pi has no adapter). The adapter process
|
|
593
|
+
stays alive for the whole run, so phases on one harness share a session
|
|
594
|
+
like `--resume` does on the CLI. Persona and workspace context travel inside
|
|
595
|
+
each phase prompt. On claude the agent's `tools`, CLI grants and MCP servers
|
|
596
|
+
become the session's allowlist (settings sources off) and the session is
|
|
597
|
+
switched to `acceptEdits`; codex runs in its workspace-write mode. A permission request the harness
|
|
598
|
+
still raises is declined — nobody watches a workflow run, and kraftwerk
|
|
599
|
+
never answers for a human. A programmatic `Run` ends its adapters with
|
|
600
|
+
`disposeAcpSessions(runDir)` (the YAML runner does this itself).
|
|
601
|
+
|
|
602
|
+
```yaml
|
|
603
|
+
name: tagline
|
|
604
|
+
protocol: acp # all agents over ACP; per agent: protocol: cli | acp
|
|
605
|
+
agents:
|
|
606
|
+
writer:
|
|
607
|
+
runs-on: claude # or codex
|
|
608
|
+
model: sonnet
|
|
609
|
+
```
|
|
610
|
+
|
|
586
611
|
## YAML workflows
|
|
587
612
|
|
|
588
613
|
Linear workflows can be pure config, GitHub-Actions-flavored with `steps`,
|
package/dist/acp.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import { ClientSideConnection, type Client, type McpServer } from "@agentclientprotocol/sdk";
|
|
3
|
+
import type { McpServerConfig } from "./harness.js";
|
|
4
|
+
/**
|
|
5
|
+
* Agent Client Protocol plumbing shared by the inspector's chats and the
|
|
6
|
+
* workflow ACP harness: the adapter packages (bundled with kraftwerk, so no
|
|
7
|
+
* CLI on the PATH is needed), model/effort tuning, and spawning one adapter
|
|
8
|
+
* as a subprocess speaking ACP over its stdio.
|
|
9
|
+
*
|
|
10
|
+
* Auth rides on the local CLI logins (Claude Code / Codex) via the
|
|
11
|
+
* inherited environment — same story as the CLI harnesses.
|
|
12
|
+
*/
|
|
13
|
+
export type AcpAgent = "claude" | "codex";
|
|
14
|
+
/**
|
|
15
|
+
* Model/effort overrides ride on adapter-specific channels: the claude
|
|
16
|
+
* adapter takes the model via session `_meta.claudeCode.options` and the
|
|
17
|
+
* thinking budget via env; codex-acp merges a CODEX_CONFIG env JSON into
|
|
18
|
+
* the session config it hands to `codex app-server`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function adapterEnv(agent: AcpAgent, tuning: {
|
|
21
|
+
model?: string;
|
|
22
|
+
effort?: string;
|
|
23
|
+
}): NodeJS.ProcessEnv;
|
|
24
|
+
/** Kraftwerk's MCP config (stdio or url) -> the protocol's session mcpServers entries. */
|
|
25
|
+
export declare function acpMcpServers(servers?: Record<string, McpServerConfig>): McpServer[];
|
|
26
|
+
export interface AcpProcess {
|
|
27
|
+
child: ChildProcessWithoutNullStreams;
|
|
28
|
+
conn: ClientSideConnection;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Spawn the adapter for `agent` in `cwd`, wire the ACP connection to
|
|
32
|
+
* `client`, and run the initialize handshake. Process failures reach the
|
|
33
|
+
* caller through the callbacks (a failed spawn is otherwise an unhandled
|
|
34
|
+
* 'error' event that takes the host process down).
|
|
35
|
+
*/
|
|
36
|
+
export declare function connectAcp(agent: AcpAgent, opts: {
|
|
37
|
+
cwd: string;
|
|
38
|
+
env: NodeJS.ProcessEnv;
|
|
39
|
+
client: Client;
|
|
40
|
+
clientName: string;
|
|
41
|
+
onError(err: Error): void;
|
|
42
|
+
onClose(code: number | null, stderr: string): void;
|
|
43
|
+
}): Promise<AcpProcess>;
|
package/dist/acp.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
5
|
+
const ADAPTERS = {
|
|
6
|
+
claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
|
|
7
|
+
codex: "@agentclientprotocol/codex-acp/dist/index.js",
|
|
8
|
+
};
|
|
9
|
+
/** Effort tier -> Claude thinking budget (the adapter reads MAX_THINKING_TOKENS). */
|
|
10
|
+
const CLAUDE_THINKING_BUDGET = {
|
|
11
|
+
low: 2048,
|
|
12
|
+
medium: 8192,
|
|
13
|
+
high: 16384,
|
|
14
|
+
xhigh: 32000,
|
|
15
|
+
max: 63999,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Model/effort overrides ride on adapter-specific channels: the claude
|
|
19
|
+
* adapter takes the model via session `_meta.claudeCode.options` and the
|
|
20
|
+
* thinking budget via env; codex-acp merges a CODEX_CONFIG env JSON into
|
|
21
|
+
* the session config it hands to `codex app-server`.
|
|
22
|
+
*/
|
|
23
|
+
export function adapterEnv(agent, tuning) {
|
|
24
|
+
const env = { ...process.env };
|
|
25
|
+
if (agent === "claude" && tuning.effort && CLAUDE_THINKING_BUDGET[tuning.effort]) {
|
|
26
|
+
env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[tuning.effort]);
|
|
27
|
+
}
|
|
28
|
+
if (agent === "codex" && (tuning.model || tuning.effort)) {
|
|
29
|
+
env.CODEX_CONFIG = JSON.stringify({
|
|
30
|
+
...(tuning.model ? { model: tuning.model } : {}),
|
|
31
|
+
...(tuning.effort ? { model_reasoning_effort: tuning.effort } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return env;
|
|
35
|
+
}
|
|
36
|
+
/** Kraftwerk's MCP config (stdio or url) -> the protocol's session mcpServers entries. */
|
|
37
|
+
export function acpMcpServers(servers = {}) {
|
|
38
|
+
return Object.entries(servers).map(([name, cfg]) => "url" in cfg
|
|
39
|
+
? { type: "http", name, url: cfg.url, headers: [] }
|
|
40
|
+
: {
|
|
41
|
+
name,
|
|
42
|
+
command: cfg.command,
|
|
43
|
+
args: cfg.args ?? [],
|
|
44
|
+
env: Object.entries(cfg.env ?? {}).map(([n, value]) => ({ name: n, value })),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Spawn the adapter for `agent` in `cwd`, wire the ACP connection to
|
|
49
|
+
* `client`, and run the initialize handshake. Process failures reach the
|
|
50
|
+
* caller through the callbacks (a failed spawn is otherwise an unhandled
|
|
51
|
+
* 'error' event that takes the host process down).
|
|
52
|
+
*/
|
|
53
|
+
export async function connectAcp(agent, opts) {
|
|
54
|
+
const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
|
|
55
|
+
const child = spawn(process.execPath, [entry], {
|
|
56
|
+
cwd: opts.cwd,
|
|
57
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
58
|
+
env: opts.env,
|
|
59
|
+
});
|
|
60
|
+
let stderr = "";
|
|
61
|
+
child.stderr.on("data", (c) => {
|
|
62
|
+
stderr += c.toString("utf8");
|
|
63
|
+
if (stderr.length > 20_000)
|
|
64
|
+
stderr = stderr.slice(-20_000);
|
|
65
|
+
});
|
|
66
|
+
child.on("error", opts.onError);
|
|
67
|
+
child.on("close", (code) => opts.onClose(code, stderr.trim()));
|
|
68
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
69
|
+
const conn = new ClientSideConnection(() => opts.client, stream);
|
|
70
|
+
await conn.initialize({
|
|
71
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
72
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
73
|
+
clientInfo: { name: opts.clientName, version: "1.0.0" },
|
|
74
|
+
});
|
|
75
|
+
return { child, conn };
|
|
76
|
+
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HarnessId, McpServerConfig } from "./harness.js";
|
|
1
|
+
import type { AgentProtocol, HarnessId, McpServerConfig } from "./harness.js";
|
|
2
2
|
/**
|
|
3
3
|
* An agent is exactly four things:
|
|
4
4
|
*
|
|
@@ -45,5 +45,7 @@ export interface AgentDefinition {
|
|
|
45
45
|
mcp?: Record<string, McpServerConfig>;
|
|
46
46
|
/** Which runtime executes this agent. Default: "claude". */
|
|
47
47
|
harness?: HarnessId;
|
|
48
|
+
/** How that runtime is driven: headless CLI (default) or the Agent Client Protocol. */
|
|
49
|
+
protocol?: AgentProtocol;
|
|
48
50
|
}
|
|
49
51
|
export declare function defineAgent(agent: AgentDefinition): AgentDefinition;
|
package/dist/cli/doctor.js
CHANGED
|
@@ -137,11 +137,19 @@ export async function runDoctor(cwd) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
// Harnesses: hard requirement only if a discovered workflow runs on them.
|
|
140
|
+
// Over the agent protocol the bundled adapters bring their own runtime.
|
|
140
141
|
const needed = new Set();
|
|
142
|
+
let acp = false;
|
|
141
143
|
for (const e of found) {
|
|
142
|
-
for (const a of e.workflow?.meta.agents ?? [])
|
|
143
|
-
|
|
144
|
+
for (const a of e.workflow?.meta.agents ?? []) {
|
|
145
|
+
if (a.protocol === "acp")
|
|
146
|
+
acp = true;
|
|
147
|
+
else
|
|
148
|
+
needed.add(a.harness ?? "claude");
|
|
149
|
+
}
|
|
144
150
|
}
|
|
151
|
+
if (acp)
|
|
152
|
+
report("ok", "agent protocol adapters", "claude-agent-acp + codex-acp bundled with kraftwerk");
|
|
145
153
|
for (const harness of ["claude", "codex", "pi"]) {
|
|
146
154
|
const version = cliVersion(harness);
|
|
147
155
|
const isNeeded = needed.has(harness);
|
package/dist/cli/knowledge.js
CHANGED
|
@@ -4,8 +4,8 @@ import chalk from "chalk";
|
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
5
|
import { fsck, getConcept, initBundle, knowledgeRoot, listBundles, listConcepts, searchKnowledge, validateBundle, verifyConcept, writeConcept, } from "../okf.js";
|
|
6
6
|
/**
|
|
7
|
-
* `kraftwerk knowledge` — the CLI surface over OKF bundles (
|
|
8
|
-
* Knowledge). This is the enforced write path: `put` stamps provenance
|
|
7
|
+
* `kraftwerk knowledge` — the CLI surface over OKF bundles (the
|
|
8
|
+
* inspector's Knowledge screen). This is the enforced write path: `put` stamps provenance
|
|
9
9
|
* (`generated: { by, at }`), appends the bundle log, and regenerates
|
|
10
10
|
* index.md; direct file edits are healed by `fsck --fix`.
|
|
11
11
|
*
|
|
@@ -59,7 +59,7 @@ async function readStdin() {
|
|
|
59
59
|
export function registerKnowledgeCommands(program) {
|
|
60
60
|
const knowledge = program
|
|
61
61
|
.command("knowledge")
|
|
62
|
-
.description("
|
|
62
|
+
.description("Knowledge: OKF bundles under knowledge/ (read, write, verify)");
|
|
63
63
|
knowledge
|
|
64
64
|
.command("list", { isDefault: true })
|
|
65
65
|
.description("List bundles, or the concepts of one bundle")
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -25,7 +25,7 @@ import { runUi } from "./ui.js";
|
|
|
25
25
|
* kraftwerk list discover + list workflows (--json, --from)
|
|
26
26
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
27
27
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
28
|
-
* kraftwerk knowledge ...
|
|
28
|
+
* kraftwerk knowledge ... Knowledge: OKF bundles (list/get/put/verify/...)
|
|
29
29
|
* kraftwerk routines ... per-agent scheduled prompts (list/add/remove/enable/run)
|
|
30
30
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
31
31
|
* kraftwerk projects ... known projects on this machine (list/start/forget)
|
|
@@ -62,7 +62,10 @@ const program = new Command()
|
|
|
62
62
|
.version(pkg.version);
|
|
63
63
|
const agentLabel = (workflow) => workflow.meta.agents
|
|
64
64
|
.map((a) => {
|
|
65
|
-
const
|
|
65
|
+
const where = [a.harness && a.harness !== "claude" ? a.harness : "", a.protocol === "acp" ? "acp" : ""]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join("/");
|
|
68
|
+
const harness = where ? `${where}:` : "";
|
|
66
69
|
const effort = a.effort ? `, ${a.effort}` : "";
|
|
67
70
|
return `${a.id} ${chalk.dim(`(${harness}${a.model}${effort})`)}`;
|
|
68
71
|
})
|
|
@@ -88,6 +91,7 @@ program
|
|
|
88
91
|
id: a.id,
|
|
89
92
|
model: a.model,
|
|
90
93
|
harness: a.harness ?? "claude",
|
|
94
|
+
protocol: a.protocol ?? "cli",
|
|
91
95
|
})),
|
|
92
96
|
error: e.error,
|
|
93
97
|
})), null, 2));
|
|
@@ -166,13 +170,15 @@ program
|
|
|
166
170
|
})),
|
|
167
171
|
});
|
|
168
172
|
const workflow = entry.workflow;
|
|
173
|
+
// A workflow whose steps never read ${{ request }} runs without one.
|
|
169
174
|
let request = (requestParts ?? []).join(" ").trim();
|
|
170
|
-
if (!request &&
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
if (!request && workflow.meta.usesRequest) {
|
|
176
|
+
if (machine)
|
|
177
|
+
fail(2, "No request given (--json is non-interactive).");
|
|
173
178
|
request = (await input({ message: "Request (topic, URL, ...):" })).trim();
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
if (!request)
|
|
180
|
+
fail(2, "No request given.");
|
|
181
|
+
}
|
|
176
182
|
const missing = missingEnv(workflow.meta.requires);
|
|
177
183
|
if (missing.length > 0) {
|
|
178
184
|
fail(2, `Workflow "${workflow.name}" needs environment variables that are missing: ${missing.join(", ")}`);
|
package/dist/harness.d.ts
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* run directory (the workspace-context contract).
|
|
13
13
|
*/
|
|
14
14
|
export type HarnessId = "claude" | "codex" | "pi";
|
|
15
|
+
/**
|
|
16
|
+
* How the harness is driven: `cli` spawns the headless CLI per phase
|
|
17
|
+
* (claude -p, codex exec, pi); `acp` keeps one Agent Client Protocol
|
|
18
|
+
* adapter alive for the run (see harnesses/acp.ts). pi has no adapter.
|
|
19
|
+
*/
|
|
20
|
+
export type AgentProtocol = "cli" | "acp";
|
|
15
21
|
/**
|
|
16
22
|
* One MCP server an agent may use: either a local stdio server (spawned by
|
|
17
23
|
* the harness, e.g. `node multiply-server.ts`) or a remote streamable-HTTP
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentInvocation, Harness, HarnessId } from "../harness.js";
|
|
2
|
+
/** Claude Agent SDK options for a phase session: the CLI harness's flags, as session meta. */
|
|
3
|
+
export declare function claudeSessionOptions(inv: Pick<AgentInvocation, "model" | "tools" | "clis" | "mcpServers">): Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* What a tool call touched, from its input (path, url, command) or its
|
|
6
|
+
* first location. Undefined when the adapter has not said yet — a title
|
|
7
|
+
* alone may be a placeholder.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toolTarget(u: {
|
|
10
|
+
rawInput?: Record<string, unknown>;
|
|
11
|
+
locations?: Array<{
|
|
12
|
+
path?: string;
|
|
13
|
+
}>;
|
|
14
|
+
}): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Stop the adapter processes of one run (its run directory), or of every
|
|
17
|
+
* run in this process when no directory is given.
|
|
18
|
+
*/
|
|
19
|
+
export declare function disposeAcpSessions(cwd?: string): void;
|
|
20
|
+
export declare function acpHarness(id: HarnessId): Harness;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { acpMcpServers, adapterEnv, connectAcp } from "../acp.js";
|
|
3
|
+
import { declineOption, unattendedMode } from "../inspector/chat/permissions.js";
|
|
4
|
+
const live = new Map();
|
|
5
|
+
// The run process is the adapters' only owner: never leave one behind.
|
|
6
|
+
process.once("exit", () => disposeAcpSessions());
|
|
7
|
+
/** Claude Agent SDK options for a phase session: the CLI harness's flags, as session meta. */
|
|
8
|
+
export function claudeSessionOptions(inv) {
|
|
9
|
+
return {
|
|
10
|
+
model: inv.model,
|
|
11
|
+
// Keep the run hermetic: no user/project settings, hooks, or CLAUDE.md.
|
|
12
|
+
settingSources: [],
|
|
13
|
+
allowedTools: [
|
|
14
|
+
...inv.tools,
|
|
15
|
+
...(inv.clis ?? []).map((name) => `Bash(${name}:*)`),
|
|
16
|
+
...Object.keys(inv.mcpServers ?? {}).map((name) => `mcp__${name}`),
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What a tool call touched, from its input (path, url, command) or its
|
|
22
|
+
* first location. Undefined when the adapter has not said yet — a title
|
|
23
|
+
* alone may be a placeholder.
|
|
24
|
+
*/
|
|
25
|
+
export function toolTarget(u) {
|
|
26
|
+
const input = u.rawInput ?? {};
|
|
27
|
+
const raw = input.file_path ?? input.url ?? input.path ?? input.command ?? u.locations?.[0]?.path;
|
|
28
|
+
if (raw == null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const target = String(raw).replace(/\s+/g, " ").trim();
|
|
31
|
+
return target.length > 160 ? `${target.slice(0, 160)}…` : target;
|
|
32
|
+
}
|
|
33
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
34
|
+
async function open(agent, inv) {
|
|
35
|
+
let ended;
|
|
36
|
+
const closed = new Promise((r) => (ended = r));
|
|
37
|
+
const state = {
|
|
38
|
+
agent,
|
|
39
|
+
cwd: inv.cwd,
|
|
40
|
+
sessionId: "",
|
|
41
|
+
dead: false,
|
|
42
|
+
closed,
|
|
43
|
+
turn: null,
|
|
44
|
+
cost: 0,
|
|
45
|
+
};
|
|
46
|
+
const end = (why) => {
|
|
47
|
+
state.dead = true;
|
|
48
|
+
state.exit ??= why;
|
|
49
|
+
ended(state.exit);
|
|
50
|
+
};
|
|
51
|
+
const client = {
|
|
52
|
+
sessionUpdate(params) {
|
|
53
|
+
const u = params.update;
|
|
54
|
+
const turn = state.turn;
|
|
55
|
+
if (u.sessionUpdate === "agent_message_chunk" && u.content?.type === "text" && turn) {
|
|
56
|
+
turn.text += u.content.text ?? "";
|
|
57
|
+
}
|
|
58
|
+
else if (u.sessionUpdate === "tool_call" && turn) {
|
|
59
|
+
const kind = u.kind ?? "tool";
|
|
60
|
+
const target = toolTarget(u);
|
|
61
|
+
if (target)
|
|
62
|
+
turn.onToolUse?.(kind, target);
|
|
63
|
+
else
|
|
64
|
+
turn.pending.set(u.toolCallId ?? "", { kind, title: String(u.title ?? "") });
|
|
65
|
+
}
|
|
66
|
+
else if (u.sessionUpdate === "tool_call_update" && turn?.pending.has(u.toolCallId ?? "")) {
|
|
67
|
+
const target = toolTarget(u);
|
|
68
|
+
if (target) {
|
|
69
|
+
turn.onToolUse?.(turn.pending.get(u.toolCallId ?? "").kind, target);
|
|
70
|
+
turn.pending.delete(u.toolCallId ?? "");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (u.sessionUpdate === "usage_update" && typeof u.cost?.amount === "number") {
|
|
74
|
+
state.cost = u.cost.amount;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
async requestPermission(params) {
|
|
78
|
+
// The harness judged this worth asking; a workflow run has nobody to ask.
|
|
79
|
+
const optionId = declineOption(params.options);
|
|
80
|
+
console.log(` ⛔ ${params.toolCall.title ?? params.toolCall.toolCallId} — permission declined (unattended run)`);
|
|
81
|
+
return optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const { child, conn } = await connectAcp(agent, {
|
|
85
|
+
cwd: inv.cwd,
|
|
86
|
+
env: adapterEnv(agent, { model: inv.model, effort: inv.effort }),
|
|
87
|
+
client,
|
|
88
|
+
clientName: "kraftwerk",
|
|
89
|
+
onError: (err) => end(`could not start the ${agent} adapter: ${err.message}`),
|
|
90
|
+
onClose: (code, stderr) => end(`${agent} adapter exited (code ${code})${stderr ? `: ${stderr.slice(-500)}` : ""}`),
|
|
91
|
+
});
|
|
92
|
+
state.child = child;
|
|
93
|
+
state.conn = conn;
|
|
94
|
+
try {
|
|
95
|
+
const session = await conn.newSession({
|
|
96
|
+
cwd: inv.cwd,
|
|
97
|
+
mcpServers: acpMcpServers(inv.mcpServers),
|
|
98
|
+
...(agent === "claude" ? { _meta: { claudeCode: { options: claudeSessionOptions(inv) } } } : {}),
|
|
99
|
+
});
|
|
100
|
+
state.sessionId = session.sessionId;
|
|
101
|
+
const modeId = unattendedMode(agent, session.modes?.currentModeId);
|
|
102
|
+
if (modeId) {
|
|
103
|
+
await conn.setSessionMode({ sessionId: session.sessionId, modeId }).catch((err) => {
|
|
104
|
+
console.log(` ⚠ could not select the ${modeId} mode (${err.message}) — the ${agent} default applies`);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// A refused session leaves the adapter running; it must not outlive the failure.
|
|
110
|
+
if (!state.dead)
|
|
111
|
+
child.kill("SIGTERM");
|
|
112
|
+
throw await failure(state, agent, err);
|
|
113
|
+
}
|
|
114
|
+
live.set(state.sessionId, state);
|
|
115
|
+
return state;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The error to surface for a failed request. When the adapter died, the
|
|
119
|
+
* SDK rejects on stdout EOF before the process 'close' event carries the
|
|
120
|
+
* stderr tail — give that a moment and prefer it, it names the real cause.
|
|
121
|
+
*/
|
|
122
|
+
async function failure(state, agent, err) {
|
|
123
|
+
const closing = /connection closed|closed/i.test(err.message ?? "");
|
|
124
|
+
if (state.dead || closing)
|
|
125
|
+
await Promise.race([state.closed, wait(1000)]);
|
|
126
|
+
if (state.exit)
|
|
127
|
+
return new Error(state.exit);
|
|
128
|
+
if (err instanceof RequestError)
|
|
129
|
+
return new Error(`${agent} (acp): ${err.message}`);
|
|
130
|
+
return err;
|
|
131
|
+
}
|
|
132
|
+
async function invoke(agent, inv) {
|
|
133
|
+
let s = inv.resume ? live.get(inv.resume) : undefined;
|
|
134
|
+
if (s?.dead) {
|
|
135
|
+
live.delete(s.sessionId);
|
|
136
|
+
s = undefined;
|
|
137
|
+
}
|
|
138
|
+
s ??= await open(agent, inv);
|
|
139
|
+
const turn = { text: "", onToolUse: inv.onToolUse, pending: new Map() };
|
|
140
|
+
s.turn = turn;
|
|
141
|
+
const prevCost = s.cost;
|
|
142
|
+
const started = Date.now();
|
|
143
|
+
// No per-prompt system prompt over ACP: carry persona + context in the prompt.
|
|
144
|
+
const prompt = `# Role and workspace context for this phase\n\n${inv.systemPrompt}\n\n` + `# Task\n\n${inv.prompt}`;
|
|
145
|
+
let res;
|
|
146
|
+
try {
|
|
147
|
+
res = await s.conn.prompt({ sessionId: s.sessionId, prompt: [{ type: "text", text: prompt }] });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
s.turn = null;
|
|
151
|
+
throw await failure(s, agent, err);
|
|
152
|
+
}
|
|
153
|
+
s.turn = null;
|
|
154
|
+
// Calls the adapter never named (no update carried a target) still count.
|
|
155
|
+
for (const p of turn.pending.values())
|
|
156
|
+
inv.onToolUse?.(p.kind, p.title);
|
|
157
|
+
if (res.stopReason !== "end_turn")
|
|
158
|
+
throw new Error(`${agent} (acp) stopped with ${res.stopReason}`);
|
|
159
|
+
if (inv.onText && turn.text.trim())
|
|
160
|
+
inv.onText(turn.text.trim());
|
|
161
|
+
const result = {
|
|
162
|
+
sessionId: s.sessionId,
|
|
163
|
+
text: turn.text,
|
|
164
|
+
durationMs: Date.now() - started,
|
|
165
|
+
};
|
|
166
|
+
if (res.usage) {
|
|
167
|
+
result.usage = {
|
|
168
|
+
inputTokens: res.usage.inputTokens,
|
|
169
|
+
outputTokens: res.usage.outputTokens,
|
|
170
|
+
cacheReadTokens: res.usage.cachedReadTokens ?? 0,
|
|
171
|
+
cacheCreationTokens: res.usage.cachedWriteTokens ?? 0,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (s.cost > prevCost)
|
|
175
|
+
result.costUsd = s.cost - prevCost;
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Stop the adapter processes of one run (its run directory), or of every
|
|
180
|
+
* run in this process when no directory is given.
|
|
181
|
+
*/
|
|
182
|
+
export function disposeAcpSessions(cwd) {
|
|
183
|
+
for (const [id, s] of live) {
|
|
184
|
+
if (cwd && s.cwd !== cwd)
|
|
185
|
+
continue;
|
|
186
|
+
if (!s.dead)
|
|
187
|
+
s.child.kill("SIGTERM");
|
|
188
|
+
live.delete(id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export function acpHarness(id) {
|
|
192
|
+
if (id === "pi")
|
|
193
|
+
throw new Error('runs-on "pi" has no agent-protocol adapter — use claude or codex, or protocol: cli');
|
|
194
|
+
return { id, invoke: (inv) => invoke(id, inv) };
|
|
195
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Harness, HarnessId } from "../harness.js";
|
|
2
|
-
export declare function harnessFor(id?: HarnessId): Harness;
|
|
1
|
+
import type { AgentProtocol, Harness, HarnessId } from "../harness.js";
|
|
2
|
+
export declare function harnessFor(id?: HarnessId, protocol?: AgentProtocol): Harness;
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { acpHarness } from "./acp.js";
|
|
1
2
|
import { claudeHarness } from "./claude.js";
|
|
2
3
|
import { codexHarness } from "./codex.js";
|
|
3
4
|
import { piHarness } from "./pi.js";
|
|
4
5
|
/**
|
|
5
|
-
* Maps an agent's `harness`
|
|
6
|
-
* Default is claude — an agent without a harness runs on
|
|
6
|
+
* Maps an agent's `harness` (+ `protocol`) to the adapter that runs it.
|
|
7
|
+
* Default is claude over the CLI — an agent without a harness runs on
|
|
8
|
+
* `claude -p`; `protocol: acp` drives the same harness over the Agent
|
|
9
|
+
* Client Protocol instead.
|
|
7
10
|
*/
|
|
8
11
|
const HARNESSES = {
|
|
9
12
|
claude: claudeHarness,
|
|
10
13
|
codex: codexHarness,
|
|
11
14
|
pi: piHarness,
|
|
12
15
|
};
|
|
13
|
-
export function harnessFor(id = "claude") {
|
|
16
|
+
export function harnessFor(id = "claude", protocol = "cli") {
|
|
17
|
+
if (protocol === "acp")
|
|
18
|
+
return acpHarness(id);
|
|
14
19
|
const harness = HARNESSES[id];
|
|
15
20
|
if (!harness) {
|
|
16
21
|
throw new Error(`harness "${id}" is not registered (available: ${Object.keys(HARNESSES).join(", ")})`);
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export { defineAgent, type AgentDefinition, type EffortLevel } from "./agent.js";
|
|
11
11
|
export { type AgentInvocation, type AgentResult, type Harness, type HarnessId, type McpServerConfig, type TokenUsage, } from "./harness.js";
|
|
12
12
|
export { harnessFor } from "./harnesses/registry.js";
|
|
13
|
+
export { disposeAcpSessions } from "./harnesses/acp.js";
|
|
13
14
|
export { Run, type RunOptions } from "./run.js";
|
|
14
15
|
export { correctionPrompt, envelopeContract, parseEnvelope, type Envelope, } from "./envelope.js";
|
|
15
16
|
export { containsText, fileNonEmpty, slotsFilled, type Gate } from "./gates.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export { defineAgent } from "./agent.js";
|
|
11
11
|
export { harnessFor } from "./harnesses/registry.js";
|
|
12
|
+
export { disposeAcpSessions } from "./harnesses/acp.js";
|
|
12
13
|
export { Run } from "./run.js";
|
|
13
14
|
export { correctionPrompt, envelopeContract, parseEnvelope, } from "./envelope.js";
|
|
14
15
|
export { containsText, fileNonEmpty, slotsFilled } from "./gates.js";
|