@netnodeag/kraftwerk 0.4.0 → 0.4.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 +1 -1
- package/dist/cli/kraftwerk.js +2 -2
- package/dist/cli/ui.js +1 -1
- package/dist/inspector/chat/acp.d.ts +2 -0
- package/dist/inspector/chat/acp.js +122 -0
- package/dist/inspector/chat/backend.d.ts +28 -0
- package/dist/inspector/chat/backend.js +1 -0
- package/dist/inspector/chat/types.d.ts +70 -0
- package/dist/inspector/chat/types.js +8 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ kraftwerk list # table: workflows, steps, agents (with
|
|
|
56
56
|
kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
57
57
|
kraftwerk run # interactive: pick workflow, type the request
|
|
58
58
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
59
|
-
kraftwerk ui # inspector web UI on http://localhost:
|
|
59
|
+
kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
|
|
60
60
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
|
|
61
61
|
kraftwerk validate # all discovered — schema + semantics + files, exit 1 on failure
|
|
62
62
|
kraftwerk validate src/workflows/pitch # specific paths
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -20,7 +20,7 @@ import { runUi } from "./ui.js";
|
|
|
20
20
|
* kraftwerk list discover + list workflows (--json, --from)
|
|
21
21
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
22
22
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
23
|
-
* kraftwerk ui start the inspector web UI (localhost:
|
|
23
|
+
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
24
24
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
25
25
|
* kraftwerk validate [paths...] validate without executing
|
|
26
26
|
*
|
|
@@ -254,7 +254,7 @@ runs
|
|
|
254
254
|
program
|
|
255
255
|
.command("ui")
|
|
256
256
|
.description("Start the inspector web UI for this project's runs and workflows")
|
|
257
|
-
.option("--port <port>", "Port for the web UI", "
|
|
257
|
+
.option("--port <port>", "Port for the web UI", "1981")
|
|
258
258
|
.option("--output <dir>", "Output directory to inspect (default: the project's output dir)")
|
|
259
259
|
.action(async (opts) => {
|
|
260
260
|
await runUi(process.cwd(), opts);
|
package/dist/cli/ui.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function runUi(cwd, opts) {
|
|
|
45
45
|
const outputDir = opts.output
|
|
46
46
|
? path.resolve(cwd, opts.output)
|
|
47
47
|
: (await resolveProject(cwd)).outputDir;
|
|
48
|
-
const port = Number(opts.port ?? "
|
|
48
|
+
const port = Number(opts.port ?? "1981");
|
|
49
49
|
await startInspector({ outputDir, staticDir, port });
|
|
50
50
|
console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
|
|
51
51
|
chalk.dim(`(output: ${outputDir})`));
|
|
@@ -0,0 +1,122 @@
|
|
|
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, RequestError, } from "@agentclientprotocol/sdk";
|
|
5
|
+
/**
|
|
6
|
+
* ACP-backed chat: spawn an adapter (claude-agent-acp / codex-acp) as a
|
|
7
|
+
* subprocess, speak Agent Client Protocol over its stdio, and translate
|
|
8
|
+
* session/update notifications into chat events. One subprocess lives for
|
|
9
|
+
* the whole chat; the ACP session id carries the conversation.
|
|
10
|
+
*
|
|
11
|
+
* Auth rides on the local CLI logins (Claude Code / Codex) via the
|
|
12
|
+
* inherited environment — same story as the kraftwerk harnesses.
|
|
13
|
+
*/
|
|
14
|
+
const ADAPTERS = {
|
|
15
|
+
claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
|
|
16
|
+
codex: "@agentclientprotocol/codex-acp/dist/index.js",
|
|
17
|
+
};
|
|
18
|
+
function contentText(content) {
|
|
19
|
+
return content.type === "text" ? content.text : "";
|
|
20
|
+
}
|
|
21
|
+
export async function startAcpBackend(agent, cwd, hooks) {
|
|
22
|
+
const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
|
|
23
|
+
const child = spawn(process.execPath, [entry], {
|
|
24
|
+
cwd,
|
|
25
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
+
env: process.env,
|
|
27
|
+
});
|
|
28
|
+
let stderr = "";
|
|
29
|
+
child.stderr.on("data", (c) => {
|
|
30
|
+
stderr += c.toString("utf8");
|
|
31
|
+
if (stderr.length > 20_000)
|
|
32
|
+
stderr = stderr.slice(-20_000);
|
|
33
|
+
});
|
|
34
|
+
let dead = false;
|
|
35
|
+
child.on("close", (code) => {
|
|
36
|
+
dead = true;
|
|
37
|
+
if (code !== 0 && code !== null) {
|
|
38
|
+
hooks.emit({
|
|
39
|
+
type: "error",
|
|
40
|
+
message: `${agent} agent exited (code ${code})${stderr.trim() ? `: ${stderr.trim().slice(-500)}` : ""}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const client = {
|
|
45
|
+
sessionUpdate(params) {
|
|
46
|
+
const u = params.update;
|
|
47
|
+
switch (u.sessionUpdate) {
|
|
48
|
+
case "agent_message_chunk": {
|
|
49
|
+
const text = contentText(u.content);
|
|
50
|
+
if (text)
|
|
51
|
+
hooks.emit({ type: "text", text });
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case "agent_thought_chunk": {
|
|
55
|
+
const text = contentText(u.content);
|
|
56
|
+
if (text)
|
|
57
|
+
hooks.emit({ type: "thought", text });
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "tool_call":
|
|
61
|
+
hooks.emit({
|
|
62
|
+
type: "tool_call",
|
|
63
|
+
callId: u.toolCallId,
|
|
64
|
+
title: u.title,
|
|
65
|
+
kind: u.kind ?? undefined,
|
|
66
|
+
status: u.status ?? undefined,
|
|
67
|
+
});
|
|
68
|
+
break;
|
|
69
|
+
case "tool_call_update":
|
|
70
|
+
hooks.emit({
|
|
71
|
+
type: "tool_update",
|
|
72
|
+
callId: u.toolCallId,
|
|
73
|
+
title: u.title ?? undefined,
|
|
74
|
+
status: u.status ?? undefined,
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
// plans, mode/config updates etc. carry no thread content — skip.
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async requestPermission(params) {
|
|
81
|
+
const optionId = await hooks.askPermission(params.toolCall.title ?? params.toolCall.toolCallId, params.options.map((o) => ({ optionId: o.optionId, name: o.name, kind: o.kind })));
|
|
82
|
+
return optionId
|
|
83
|
+
? { outcome: { outcome: "selected", optionId } }
|
|
84
|
+
: { outcome: { outcome: "cancelled" } };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
88
|
+
const conn = new ClientSideConnection(() => client, stream);
|
|
89
|
+
await conn.initialize({
|
|
90
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
91
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
92
|
+
clientInfo: { name: "kraftwerk-inspector", version: "1.0.0" },
|
|
93
|
+
});
|
|
94
|
+
const session = await conn.newSession({ cwd, mcpServers: [] });
|
|
95
|
+
const sessionId = session.sessionId;
|
|
96
|
+
return {
|
|
97
|
+
async prompt(text) {
|
|
98
|
+
if (dead)
|
|
99
|
+
throw new Error(`${agent} agent process is gone — start a new chat`);
|
|
100
|
+
try {
|
|
101
|
+
const res = await conn.prompt({
|
|
102
|
+
sessionId,
|
|
103
|
+
prompt: [{ type: "text", text }],
|
|
104
|
+
});
|
|
105
|
+
return res.stopReason;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof RequestError)
|
|
109
|
+
throw new Error(`${agent} agent: ${err.message}`);
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
cancel() {
|
|
114
|
+
if (!dead)
|
|
115
|
+
void conn.cancel({ sessionId }).catch(() => { });
|
|
116
|
+
},
|
|
117
|
+
dispose() {
|
|
118
|
+
if (!dead)
|
|
119
|
+
child.kill("SIGTERM");
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ChatEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A chat backend is one live agent conversation: prompt() runs a full turn
|
|
4
|
+
* (resolves when the agent finishes), streaming intermediate activity
|
|
5
|
+
* through the hooks. ACP agents (claude, codex) keep a subprocess alive for
|
|
6
|
+
* the whole chat; pi respawns per message and resumes via --session-id.
|
|
7
|
+
*/
|
|
8
|
+
export interface BackendHooks {
|
|
9
|
+
/** Stream a thread event (text chunk, tool call, ...) to the session. */
|
|
10
|
+
emit(ev: ChatEvent): void;
|
|
11
|
+
/**
|
|
12
|
+
* Surface a permission request to the user; resolves with the chosen
|
|
13
|
+
* optionId, or null when the user (or a cancel) dismissed it.
|
|
14
|
+
*/
|
|
15
|
+
askPermission(title: string, options: Array<{
|
|
16
|
+
optionId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
kind?: string;
|
|
19
|
+
}>): Promise<string | null>;
|
|
20
|
+
}
|
|
21
|
+
export interface ChatBackend {
|
|
22
|
+
/** Send one user message; resolves with the stop reason at turn end. */
|
|
23
|
+
prompt(text: string): Promise<string>;
|
|
24
|
+
/** Interrupt the current turn (the running prompt() still resolves). */
|
|
25
|
+
cancel(): void;
|
|
26
|
+
/** Kill the agent subprocess, if any. */
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat data model shared across the chat backend modules. Every chat is a
|
|
3
|
+
* folder under <output>/chats/ holding meta.json plus events.jsonl — the
|
|
4
|
+
* same files-on-disk philosophy as runs. Events are the single source of
|
|
5
|
+
* truth: the UI replays them to render the thread, and the SSE stream just
|
|
6
|
+
* appends live ones.
|
|
7
|
+
*/
|
|
8
|
+
export type ChatAgentId = "claude" | "codex" | "pi";
|
|
9
|
+
export type ChatScope = {
|
|
10
|
+
kind: "general";
|
|
11
|
+
} | {
|
|
12
|
+
kind: "kraftwerk";
|
|
13
|
+
} | {
|
|
14
|
+
kind: "run";
|
|
15
|
+
runId: string;
|
|
16
|
+
};
|
|
17
|
+
export interface ChatMeta {
|
|
18
|
+
id: string;
|
|
19
|
+
agent: ChatAgentId;
|
|
20
|
+
title: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
scope: ChatScope;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
}
|
|
26
|
+
/** One thread event; `seq` orders them and drives SSE resume (?after=seq). */
|
|
27
|
+
export type ChatEvent = {
|
|
28
|
+
type: "user_message";
|
|
29
|
+
text: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "text";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: "thought";
|
|
35
|
+
text: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: "tool_call";
|
|
38
|
+
callId: string;
|
|
39
|
+
title: string;
|
|
40
|
+
kind?: string;
|
|
41
|
+
status?: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "tool_update";
|
|
44
|
+
callId: string;
|
|
45
|
+
title?: string;
|
|
46
|
+
status?: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: "permission_request";
|
|
49
|
+
requestId: string;
|
|
50
|
+
title: string;
|
|
51
|
+
options: Array<{
|
|
52
|
+
optionId: string;
|
|
53
|
+
name: string;
|
|
54
|
+
kind?: string;
|
|
55
|
+
}>;
|
|
56
|
+
} | {
|
|
57
|
+
type: "permission_resolved";
|
|
58
|
+
requestId: string;
|
|
59
|
+
optionId: string | null;
|
|
60
|
+
} | {
|
|
61
|
+
type: "turn_end";
|
|
62
|
+
stopReason: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: "error";
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
67
|
+
export type StoredChatEvent = ChatEvent & {
|
|
68
|
+
seq: number;
|
|
69
|
+
ts: string;
|
|
70
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat data model shared across the chat backend modules. Every chat is a
|
|
3
|
+
* folder under <output>/chats/ holding meta.json plus events.jsonl — the
|
|
4
|
+
* same files-on-disk philosophy as runs. Events are the single source of
|
|
5
|
+
* truth: the UI replays them to render the thread, and the SSE stream just
|
|
6
|
+
* appends live ones.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Deterministic workflow-as-code framework: agents (persona + model + tools + harness) run in bounded phases on headless CLI harnesses (claude -p, codex exec, pi); code owns the control flow, envelopes + gates judge the results",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
"typescript": "^7.0.2"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@agentclientprotocol/claude-agent-acp": "^0.70.0",
|
|
59
|
+
"@agentclientprotocol/codex-acp": "^1.6.2",
|
|
60
|
+
"@agentclientprotocol/sdk": "^1.4.0",
|
|
58
61
|
"@inquirer/prompts": "^8.5.2",
|
|
59
62
|
"ajv": "^8.20.0",
|
|
60
63
|
"chalk": "^6.0.0",
|