@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 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:4499; --port, --output
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
@@ -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:4499)
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", "4499")
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 ?? "4499");
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,2 @@
1
+ import type { BackendHooks, ChatBackend } from "./backend.js";
2
+ export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks): Promise<ChatBackend>;
@@ -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.0",
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",