@netnodeag/kraftwerk 0.4.1 → 0.7.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 +192 -6
- package/dist/cli/doctor.js +77 -2
- package/dist/cli/init.js +65 -15
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +213 -0
- package/dist/cli/kraftwerk.js +12 -10
- package/dist/cli/runs.js +13 -6
- package/dist/cli/ui.js +3 -4
- package/dist/config.d.ts +9 -0
- package/dist/config.js +5 -2
- package/dist/gates.d.ts +7 -0
- package/dist/gates.js +27 -0
- package/dist/harnesses/claude.js +0 -1
- package/dist/inspector/chat/acp.d.ts +2 -2
- package/dist/inspector/chat/acp.js +44 -3
- package/dist/inspector/chat/backend.d.ts +14 -0
- package/dist/inspector/chat/pi.d.ts +2 -0
- package/dist/inspector/chat/pi.js +106 -0
- package/dist/inspector/chat/sessions.d.ts +33 -0
- package/dist/inspector/chat/sessions.js +407 -0
- package/dist/inspector/chat/store.d.ts +8 -0
- package/dist/inspector/chat/store.js +69 -0
- package/dist/inspector/chat/types.d.ts +7 -0
- package/dist/inspector/context.d.ts +7 -2
- package/dist/inspector/context.js +11 -3
- package/dist/inspector/knowledge.d.ts +24 -0
- package/dist/inspector/knowledge.js +41 -0
- package/dist/inspector/routines.d.ts +56 -0
- package/dist/inspector/routines.js +266 -0
- package/dist/inspector/runner.d.ts +10 -0
- package/dist/inspector/runner.js +6 -8
- package/dist/inspector/runs.d.ts +2 -0
- package/dist/inspector/runs.js +17 -12
- package/dist/inspector/server.d.ts +2 -0
- package/dist/inspector/server.js +311 -1
- package/dist/inspector/skills.d.ts +26 -0
- package/dist/inspector/skills.js +65 -0
- package/dist/inspector/team.d.ts +57 -0
- package/dist/inspector/team.js +121 -0
- package/dist/okf.d.ts +118 -0
- package/dist/okf.js +398 -0
- package/dist/runner/docker.d.ts +3 -1
- package/dist/runner/docker.js +5 -5
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +19 -0
- package/dist/yaml.js +43 -10
- package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
- package/inspector/dist/assets/index-G3eFm1X4.js +13 -0
- package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/workflow.schema.json +166 -31
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/inspector/dist/assets/index-BCSWkslF.css +0 -1
- package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -11,15 +11,17 @@ import { loadWorkflow, missingEnv } from "../yaml.js";
|
|
|
11
11
|
import { renderCreateBrief } from "./create-brief.js";
|
|
12
12
|
import { runDoctor } from "./doctor.js";
|
|
13
13
|
import { runInit } from "./init.js";
|
|
14
|
+
import { registerKnowledgeCommands } from "./knowledge.js";
|
|
14
15
|
import { listRuns, showRun } from "./runs.js";
|
|
15
16
|
import { runUi } from "./ui.js";
|
|
16
17
|
/**
|
|
17
18
|
* kraftwerk — the kraftwerk CLI.
|
|
18
19
|
*
|
|
19
|
-
* kraftwerk init scaffold kraftwerk.yml +
|
|
20
|
+
* kraftwerk init scaffold kraftwerk.yml + kraftwerk-data/ (workflow, agent, knowledge)
|
|
20
21
|
* kraftwerk list discover + list workflows (--json, --from)
|
|
21
22
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
22
23
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
24
|
+
* kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
|
|
23
25
|
* kraftwerk ui start the inspector web UI (localhost:1981)
|
|
24
26
|
* kraftwerk doctor preflight: harness CLIs, docker, workflows, env
|
|
25
27
|
* kraftwerk validate [paths...] validate without executing
|
|
@@ -127,7 +129,7 @@ program
|
|
|
127
129
|
.option("--from <source>", "Workflows from a git remote: github:org/repo[@ref] or git URL")
|
|
128
130
|
.option("--sandbox", "Run inside the Docker sandbox container (image: kraftwerk-runner)")
|
|
129
131
|
.option("--ssh", "Forward the SSH agent + known_hosts into the sandbox (only with --sandbox)")
|
|
130
|
-
.option("--run-id <id>", "Pin the run folder name (output/<id>), e.g. for external triggers")
|
|
132
|
+
.option("--run-id <id>", "Pin the run folder name (output/runs/<id>), e.g. for external triggers")
|
|
131
133
|
.action(async (name, requestParts, opts) => {
|
|
132
134
|
const machine = !!opts.json;
|
|
133
135
|
const fail = (code, message) => {
|
|
@@ -170,8 +172,10 @@ program
|
|
|
170
172
|
}
|
|
171
173
|
if (opts.sandbox) {
|
|
172
174
|
const { runSandboxed } = await import("../runner/docker.js");
|
|
175
|
+
const { resolveProject } = await import("../config.js");
|
|
173
176
|
const handle = await runSandboxed({
|
|
174
177
|
projectRoot: baseDir,
|
|
178
|
+
outputDir: opts.from ? undefined : (await resolveProject(baseDir)).outputDir,
|
|
175
179
|
workflowPath: entry.path,
|
|
176
180
|
workflowName: workflow.name,
|
|
177
181
|
request,
|
|
@@ -182,17 +186,14 @@ program
|
|
|
182
186
|
console.log(chalk.dim(`sandbox ${handle.containerName} → ${handle.runDir}`));
|
|
183
187
|
process.exit(await handle.finished);
|
|
184
188
|
}
|
|
185
|
-
if (opts.runId) {
|
|
186
|
-
process.env.KRAFTWERK_RUN_DIR = path.resolve("output", opts.runId);
|
|
187
|
-
}
|
|
188
|
-
else if (!process.env.KRAFTWERK_RUN_DIR) {
|
|
189
|
+
if (opts.runId || !process.env.KRAFTWERK_RUN_DIR) {
|
|
189
190
|
// Root the run dir explicitly: at the project's output dir (honors
|
|
190
191
|
// kraftwerk.yml `output:` and works from subdirs); for remote runs at
|
|
191
192
|
// the caller's cwd — never inside the clone cache.
|
|
192
|
-
const {
|
|
193
|
+
const { newRunId, runDirFor } = await import("../workflow.js");
|
|
193
194
|
const { resolveProject } = await import("../config.js");
|
|
194
195
|
const outBase = opts.from ? path.resolve("output") : (await resolveProject(baseDir)).outputDir;
|
|
195
|
-
process.env.KRAFTWERK_RUN_DIR =
|
|
196
|
+
process.env.KRAFTWERK_RUN_DIR = runDirFor(outBase, opts.runId ?? newRunId(workflow.name));
|
|
196
197
|
}
|
|
197
198
|
// Machine/quiet mode: workflow narration must not pollute stdout — the
|
|
198
199
|
// engine logs via console.log, so reroute it for the duration of the run.
|
|
@@ -223,7 +224,7 @@ program
|
|
|
223
224
|
});
|
|
224
225
|
program
|
|
225
226
|
.command("init")
|
|
226
|
-
.description("Scaffold the project: kraftwerk.yml,
|
|
227
|
+
.description("Scaffold the project: kraftwerk.yml, kraftwerk-data/ (example workflow, agent max, demo knowledge), .gitignore")
|
|
227
228
|
.action(async () => {
|
|
228
229
|
await runInit(process.cwd());
|
|
229
230
|
});
|
|
@@ -246,11 +247,12 @@ runs
|
|
|
246
247
|
runs
|
|
247
248
|
.command("show")
|
|
248
249
|
.description("Show one run in detail: phases, gates, cost")
|
|
249
|
-
.argument("<runId>", "Folder name under output/, see `kraftwerk runs`")
|
|
250
|
+
.argument("<runId>", "Folder name under output/runs/, see `kraftwerk runs`")
|
|
250
251
|
.option("--json", "Machine-readable output (all trace events)")
|
|
251
252
|
.action(async (runId, opts) => {
|
|
252
253
|
await showRun(process.cwd(), runId, opts);
|
|
253
254
|
});
|
|
255
|
+
registerKnowledgeCommands(program);
|
|
254
256
|
program
|
|
255
257
|
.command("ui")
|
|
256
258
|
.description("Start the inspector web UI for this project's runs and workflows")
|
package/dist/cli/runs.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from "chalk";
|
|
|
4
4
|
import Table from "cli-table3";
|
|
5
5
|
import { resolveProject } from "../config.js";
|
|
6
6
|
import { fmtDuration, fmtTokens } from "../stats.js";
|
|
7
|
+
import { runDirFor } from "../workflow.js";
|
|
7
8
|
async function readRun(dir) {
|
|
8
9
|
const tracePath = path.join(dir, "trace.jsonl");
|
|
9
10
|
const raw = await readFile(tracePath, "utf8").catch(() => null);
|
|
@@ -37,12 +38,18 @@ async function readRun(dir) {
|
|
|
37
38
|
}
|
|
38
39
|
async function collectRuns(cwd) {
|
|
39
40
|
const { outputDir } = await resolveProject(cwd);
|
|
40
|
-
|
|
41
|
+
// New runs live under <output>/runs/; legacy run-* folders sit at the root.
|
|
42
|
+
const dirs = [
|
|
43
|
+
...(await readdir(path.join(outputDir, "runs"), { withFileTypes: true }).catch(() => []))
|
|
44
|
+
.filter((e) => e.isDirectory())
|
|
45
|
+
.map((e) => path.join(outputDir, "runs", e.name)),
|
|
46
|
+
...(await readdir(outputDir, { withFileTypes: true }).catch(() => []))
|
|
47
|
+
.filter((e) => e.isDirectory() && e.name.startsWith("run-"))
|
|
48
|
+
.map((e) => path.join(outputDir, e.name)),
|
|
49
|
+
];
|
|
41
50
|
const runs = [];
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
continue;
|
|
45
|
-
const info = await readRun(path.join(outputDir, entry.name));
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
const info = await readRun(dir);
|
|
46
53
|
if (info)
|
|
47
54
|
runs.push(info);
|
|
48
55
|
}
|
|
@@ -83,7 +90,7 @@ export async function listRuns(cwd, opts = {}) {
|
|
|
83
90
|
}
|
|
84
91
|
export async function showRun(cwd, id, opts = {}) {
|
|
85
92
|
const { outputDir } = await resolveProject(cwd);
|
|
86
|
-
const dir =
|
|
93
|
+
const dir = runDirFor(outputDir, id);
|
|
87
94
|
if (!(await stat(dir).catch(() => null))) {
|
|
88
95
|
console.error(chalk.red(`Run "${id}" not found under ${outputDir}.`));
|
|
89
96
|
process.exit(2);
|
package/dist/cli/ui.js
CHANGED
|
@@ -42,11 +42,10 @@ function ensureBuilt() {
|
|
|
42
42
|
}
|
|
43
43
|
export async function runUi(cwd, opts) {
|
|
44
44
|
const staticDir = ensureBuilt();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
: (await resolveProject(cwd)).outputDir;
|
|
45
|
+
const project = await resolveProject(cwd);
|
|
46
|
+
const outputDir = opts.output ? path.resolve(cwd, opts.output) : project.outputDir;
|
|
48
47
|
const port = Number(opts.port ?? "1981");
|
|
49
|
-
await startInspector({ outputDir, staticDir, port });
|
|
48
|
+
await startInspector({ outputDir, staticDir, port, projectRoot: project.root });
|
|
50
49
|
console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
|
|
51
50
|
chalk.dim(`(output: ${outputDir})`));
|
|
52
51
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -9,17 +9,26 @@
|
|
|
9
9
|
*
|
|
10
10
|
* All fields are optional — a valid kraftwerk.yml may be empty:
|
|
11
11
|
*
|
|
12
|
+
* name: my-project # display name (inspector header, "environment")
|
|
12
13
|
* workflows: src/workflows # workflows root, relative to the file
|
|
13
14
|
* output: output # run-artifact directory, relative to the file
|
|
15
|
+
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
16
|
+
* agents: agents # team agent-definition root, relative to the file
|
|
14
17
|
*/
|
|
15
18
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
16
19
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
17
20
|
export declare const CONFIG_FILENAMES: string[];
|
|
18
21
|
export interface ProjectConfig {
|
|
22
|
+
/** Display name of the project ("environment"), shown in the inspector header. */
|
|
23
|
+
name?: string;
|
|
19
24
|
/** Workflows root relative to the project root. */
|
|
20
25
|
workflows?: string;
|
|
21
26
|
/** Run-artifact directory relative to the project root. Default: output */
|
|
22
27
|
output?: string;
|
|
28
|
+
/** OKF knowledge-bundle root relative to the project root. Default: knowledge */
|
|
29
|
+
knowledge?: string;
|
|
30
|
+
/** Team agent-definition root relative to the project root. Default: agents */
|
|
31
|
+
agents?: string;
|
|
23
32
|
}
|
|
24
33
|
export interface Project {
|
|
25
34
|
/** Absolute project root the CLI operates on. */
|
package/dist/config.js
CHANGED
|
@@ -12,8 +12,11 @@ import { parse } from "yaml";
|
|
|
12
12
|
*
|
|
13
13
|
* All fields are optional — a valid kraftwerk.yml may be empty:
|
|
14
14
|
*
|
|
15
|
+
* name: my-project # display name (inspector header, "environment")
|
|
15
16
|
* workflows: src/workflows # workflows root, relative to the file
|
|
16
17
|
* output: output # run-artifact directory, relative to the file
|
|
18
|
+
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
19
|
+
* agents: agents # team agent-definition root, relative to the file
|
|
17
20
|
*/
|
|
18
21
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
19
22
|
export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -81,10 +84,10 @@ async function loadConfig(configPath) {
|
|
|
81
84
|
if (raw === null || raw === undefined)
|
|
82
85
|
return {};
|
|
83
86
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
84
|
-
throw new Error(`${path.basename(configPath)}: expected a mapping (workflows, output)`);
|
|
87
|
+
throw new Error(`${path.basename(configPath)}: expected a mapping (name, workflows, output, knowledge, agents)`);
|
|
85
88
|
}
|
|
86
89
|
const config = raw;
|
|
87
|
-
const known = ["workflows", "output"];
|
|
90
|
+
const known = ["name", "workflows", "output", "knowledge", "agents"];
|
|
88
91
|
for (const key of Object.keys(config)) {
|
|
89
92
|
if (!known.includes(key)) {
|
|
90
93
|
throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
|
package/dist/gates.d.ts
CHANGED
|
@@ -14,5 +14,12 @@ export interface Gate {
|
|
|
14
14
|
export declare const fileNonEmpty: (file: string) => Gate;
|
|
15
15
|
/** No {{...}} template slots may survive a fill step. */
|
|
16
16
|
export declare const slotsFilled: (file: string) => Gate;
|
|
17
|
+
/**
|
|
18
|
+
* Workflow-specific validation as a bash script: exit 0 = passed, non-zero =
|
|
19
|
+
* failed and everything the script printed becomes the failure message — so
|
|
20
|
+
* precise per-violation output turns directly into a precise correction
|
|
21
|
+
* prompt. The script runs in the run directory (env: RUN_DIR).
|
|
22
|
+
*/
|
|
23
|
+
export declare const checkScript: (script: string, label: string) => Gate;
|
|
17
24
|
/** Fixed template parts chosen by code must survive byte-identically. */
|
|
18
25
|
export declare const containsText: (file: string, needle: string, label: string) => Gate;
|
package/dist/gates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
export const fileNonEmpty = (file) => ({
|
|
@@ -22,6 +23,32 @@ export const slotsFilled = (file) => ({
|
|
|
22
23
|
: null;
|
|
23
24
|
},
|
|
24
25
|
});
|
|
26
|
+
/**
|
|
27
|
+
* Workflow-specific validation as a bash script: exit 0 = passed, non-zero =
|
|
28
|
+
* failed and everything the script printed becomes the failure message — so
|
|
29
|
+
* precise per-violation output turns directly into a precise correction
|
|
30
|
+
* prompt. The script runs in the run directory (env: RUN_DIR).
|
|
31
|
+
*/
|
|
32
|
+
export const checkScript = (script, label) => ({
|
|
33
|
+
name: `check(${label})`,
|
|
34
|
+
async check(runDir) {
|
|
35
|
+
const result = await new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn("bash", ["-c", script], {
|
|
37
|
+
cwd: runDir,
|
|
38
|
+
env: { ...process.env, RUN_DIR: runDir },
|
|
39
|
+
});
|
|
40
|
+
let output = "";
|
|
41
|
+
child.stdout.on("data", (chunk) => (output += String(chunk)));
|
|
42
|
+
child.stderr.on("data", (chunk) => (output += String(chunk)));
|
|
43
|
+
child.on("error", reject);
|
|
44
|
+
child.on("close", (code) => resolve({ code: code ?? 1, output }));
|
|
45
|
+
});
|
|
46
|
+
if (result.code === 0)
|
|
47
|
+
return null;
|
|
48
|
+
const tail = result.output.trim().split("\n").slice(-12).join("\n");
|
|
49
|
+
return tail || `check script failed with exit code ${result.code}`;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
25
52
|
/** Fixed template parts chosen by code must survive byte-identically. */
|
|
26
53
|
export const containsText = (file, needle, label) => ({
|
|
27
54
|
name: `contains(${file}, ${label})`,
|
package/dist/harnesses/claude.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { BackendHooks, ChatBackend } from "./backend.js";
|
|
2
|
-
export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks): Promise<ChatBackend>;
|
|
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>;
|
|
@@ -15,15 +15,42 @@ const ADAPTERS = {
|
|
|
15
15
|
claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
|
|
16
16
|
codex: "@agentclientprotocol/codex-acp/dist/index.js",
|
|
17
17
|
};
|
|
18
|
+
/** Effort tier -> Claude thinking budget (the adapter reads MAX_THINKING_TOKENS). */
|
|
19
|
+
const CLAUDE_THINKING_BUDGET = {
|
|
20
|
+
low: 2048,
|
|
21
|
+
medium: 8192,
|
|
22
|
+
high: 16384,
|
|
23
|
+
xhigh: 32000,
|
|
24
|
+
max: 63999,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Model/effort overrides ride on adapter-specific channels: the claude
|
|
28
|
+
* adapter takes the model via session `_meta.claudeCode.options` and the
|
|
29
|
+
* thinking budget via env; codex-acp merges a CODEX_CONFIG env JSON into
|
|
30
|
+
* the session config it hands to `codex app-server`.
|
|
31
|
+
*/
|
|
32
|
+
function tuningEnv(agent, tuning) {
|
|
33
|
+
const env = { ...process.env };
|
|
34
|
+
if (agent === "claude" && tuning.effort && CLAUDE_THINKING_BUDGET[tuning.effort]) {
|
|
35
|
+
env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[tuning.effort]);
|
|
36
|
+
}
|
|
37
|
+
if (agent === "codex" && (tuning.model || tuning.effort)) {
|
|
38
|
+
env.CODEX_CONFIG = JSON.stringify({
|
|
39
|
+
...(tuning.model ? { model: tuning.model } : {}),
|
|
40
|
+
...(tuning.effort ? { model_reasoning_effort: tuning.effort } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
18
45
|
function contentText(content) {
|
|
19
46
|
return content.type === "text" ? content.text : "";
|
|
20
47
|
}
|
|
21
|
-
export async function startAcpBackend(agent, cwd, hooks) {
|
|
48
|
+
export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
22
49
|
const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
|
|
23
50
|
const child = spawn(process.execPath, [entry], {
|
|
24
51
|
cwd,
|
|
25
52
|
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
-
env:
|
|
53
|
+
env: tuningEnv(agent, tuning),
|
|
27
54
|
});
|
|
28
55
|
let stderr = "";
|
|
29
56
|
child.stderr.on("data", (c) => {
|
|
@@ -91,7 +118,21 @@ export async function startAcpBackend(agent, cwd, hooks) {
|
|
|
91
118
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
92
119
|
clientInfo: { name: "kraftwerk-inspector", version: "1.0.0" },
|
|
93
120
|
});
|
|
94
|
-
|
|
121
|
+
// Claude-only session options ride on _meta.claudeCode.options (the
|
|
122
|
+
// adapter spreads them into the Agent SDK options): model override,
|
|
123
|
+
// skill allowlist (undefined = all discovered skills), extra work dirs.
|
|
124
|
+
const claudeOptions = {
|
|
125
|
+
...(tuning.model ? { model: tuning.model } : {}),
|
|
126
|
+
...(tuning.skills ? { skills: tuning.skills } : {}),
|
|
127
|
+
...(tuning.addDirs?.length ? { additionalDirectories: tuning.addDirs } : {}),
|
|
128
|
+
};
|
|
129
|
+
const session = await conn.newSession({
|
|
130
|
+
cwd,
|
|
131
|
+
mcpServers: [],
|
|
132
|
+
...(agent === "claude" && Object.keys(claudeOptions).length
|
|
133
|
+
? { _meta: { claudeCode: { options: claudeOptions } } }
|
|
134
|
+
: {}),
|
|
135
|
+
});
|
|
95
136
|
const sessionId = session.sessionId;
|
|
96
137
|
return {
|
|
97
138
|
async prompt(text) {
|
|
@@ -18,6 +18,20 @@ export interface BackendHooks {
|
|
|
18
18
|
kind?: string;
|
|
19
19
|
}>): Promise<string | null>;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Optional per-chat model/effort overrides (from a team member definition).
|
|
23
|
+
* Each backend maps them onto whatever its agent actually supports.
|
|
24
|
+
*/
|
|
25
|
+
export interface BackendTuning {
|
|
26
|
+
model?: string;
|
|
27
|
+
effort?: string;
|
|
28
|
+
/** claude: restrict native skill discovery to these names (undefined = all). */
|
|
29
|
+
skills?: string[];
|
|
30
|
+
/** pi: skill folders to load explicitly via --skill. */
|
|
31
|
+
skillDirs?: string[];
|
|
32
|
+
/** claude: extra directories the session may work in (e.g. project root for run chats). */
|
|
33
|
+
addDirs?: string[];
|
|
34
|
+
}
|
|
21
35
|
export interface ChatBackend {
|
|
22
36
|
/** Send one user message; resolves with the stop reason at turn end. */
|
|
23
37
|
prompt(text: string): Promise<string>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
const clip = (raw) => {
|
|
4
|
+
const oneLine = String(raw ?? "").replace(/\s+/g, " ").trim();
|
|
5
|
+
return oneLine.length > 120 ? `${oneLine.slice(0, 120)}…` : oneLine;
|
|
6
|
+
};
|
|
7
|
+
export function startPiBackend(cwd, hooks, tuning = {}) {
|
|
8
|
+
const sessionId = randomUUID();
|
|
9
|
+
let child = null;
|
|
10
|
+
let cancelled = false;
|
|
11
|
+
const tuningArgs = [
|
|
12
|
+
...(tuning.model ? ["--model", tuning.model] : []),
|
|
13
|
+
...(tuning.effort ? ["--thinking", tuning.effort] : []),
|
|
14
|
+
...(tuning.skillDirs ?? []).flatMap((dir) => ["--skill", dir]),
|
|
15
|
+
];
|
|
16
|
+
return {
|
|
17
|
+
prompt(text) {
|
|
18
|
+
cancelled = false;
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const proc = spawn("pi", ["-p", "--mode", "json", "--session-id", sessionId, ...tuningArgs], {
|
|
21
|
+
cwd,
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
env: process.env,
|
|
24
|
+
});
|
|
25
|
+
child = proc;
|
|
26
|
+
proc.stdin.write(text);
|
|
27
|
+
proc.stdin.end();
|
|
28
|
+
let sawAssistant = false;
|
|
29
|
+
let failure;
|
|
30
|
+
let stderr = "";
|
|
31
|
+
let buffer = "";
|
|
32
|
+
let toolSeq = 0;
|
|
33
|
+
const handleLine = (line) => {
|
|
34
|
+
if (!line.trim())
|
|
35
|
+
return;
|
|
36
|
+
let event;
|
|
37
|
+
try {
|
|
38
|
+
event = JSON.parse(line);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return; // non-JSON noise on stdout
|
|
42
|
+
}
|
|
43
|
+
if (event.type === "tool_execution_start" && event.toolName) {
|
|
44
|
+
const a = event.args ?? {};
|
|
45
|
+
const target = clip(a.path ?? a.file_path ?? a.command ?? a.pattern ?? "");
|
|
46
|
+
hooks.emit({
|
|
47
|
+
type: "tool_call",
|
|
48
|
+
callId: `pi-${sessionId}-${toolSeq++}`,
|
|
49
|
+
title: target ? `${event.toolName} ${target}` : event.toolName,
|
|
50
|
+
status: "completed",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
54
|
+
sawAssistant = true;
|
|
55
|
+
for (const block of event.message.content ?? []) {
|
|
56
|
+
if (block?.type === "text" && block.text?.trim()) {
|
|
57
|
+
hooks.emit({ type: "text", text: block.text });
|
|
58
|
+
}
|
|
59
|
+
if (block?.type === "thinking" && block.text?.trim()) {
|
|
60
|
+
hooks.emit({ type: "thought", text: block.text });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (event.type === "error") {
|
|
65
|
+
failure = typeof event.error === "string" ? event.error : line;
|
|
66
|
+
}
|
|
67
|
+
// API failures don't exit non-zero: they ride on turn_end as
|
|
68
|
+
// stopReason "error" with an empty assistant message.
|
|
69
|
+
if (event.type === "turn_end" && event.message?.stopReason === "error") {
|
|
70
|
+
failure = clip(event.message.errorMessage ?? "unknown pi error");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
proc.stdout.on("data", (chunk) => {
|
|
74
|
+
buffer += chunk.toString("utf8");
|
|
75
|
+
const lines = buffer.split("\n");
|
|
76
|
+
buffer = lines.pop() ?? "";
|
|
77
|
+
lines.forEach(handleLine);
|
|
78
|
+
});
|
|
79
|
+
proc.stderr.on("data", (chunk) => {
|
|
80
|
+
stderr += chunk.toString("utf8");
|
|
81
|
+
});
|
|
82
|
+
proc.on("error", (err) => reject(new Error(`could not spawn pi: ${err.message}`)));
|
|
83
|
+
proc.on("close", (code) => {
|
|
84
|
+
child = null;
|
|
85
|
+
handleLine(buffer);
|
|
86
|
+
if (cancelled)
|
|
87
|
+
return resolve("cancelled");
|
|
88
|
+
if (failure || code !== 0 || !sawAssistant) {
|
|
89
|
+
reject(new Error(`pi failed (exit ${code})` +
|
|
90
|
+
(failure ? `: ${failure}` : "") +
|
|
91
|
+
(stderr.trim() ? `\nstderr: ${stderr.trim().slice(-1000)}` : "")));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
resolve("end_turn");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
cancel() {
|
|
99
|
+
cancelled = true;
|
|
100
|
+
child?.kill("SIGTERM");
|
|
101
|
+
},
|
|
102
|
+
dispose() {
|
|
103
|
+
child?.kill("SIGTERM");
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ChatAgentId, ChatMeta, ChatScope, StoredChatEvent } from "./types.js";
|
|
2
|
+
export declare function createChat(opts: {
|
|
3
|
+
agent: ChatAgentId;
|
|
4
|
+
scope: ChatScope;
|
|
5
|
+
/** Preset title (e.g. routine runs); otherwise the first message names the chat. */
|
|
6
|
+
title?: string;
|
|
7
|
+
}): Promise<ChatMeta>;
|
|
8
|
+
export declare function listChats(): Promise<Array<ChatMeta & {
|
|
9
|
+
busy: boolean;
|
|
10
|
+
}>>;
|
|
11
|
+
export declare function getChat(id: string): Promise<{
|
|
12
|
+
meta: ChatMeta;
|
|
13
|
+
events: StoredChatEvent[];
|
|
14
|
+
busy: boolean;
|
|
15
|
+
} | null>;
|
|
16
|
+
export declare function postMessage(id: string, text: string): Promise<{
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
|
|
20
|
+
error?: string;
|
|
21
|
+
}>;
|
|
22
|
+
/** Delete a chat: kill its backend, drop the live state, remove its folder. */
|
|
23
|
+
export declare function deleteChat(id: string): Promise<{
|
|
24
|
+
error?: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function cancelChat(id: string): Promise<{
|
|
27
|
+
error?: string;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* SSE subscription: replays events after `afterSeq`, then streams new ones.
|
|
31
|
+
* Returns an unsubscribe function.
|
|
32
|
+
*/
|
|
33
|
+
export declare function subscribeChat(id: string, afterSeq: number, fn: (ev: StoredChatEvent) => void): Promise<(() => void) | null>;
|