@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
|
@@ -1,80 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
|
|
1
|
+
import { RequestError } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { adapterEnv, connectAcp } from "../../acp.js";
|
|
5
3
|
import { unattendedMode } from "./permissions.js";
|
|
6
4
|
/**
|
|
7
5
|
* ACP-backed chat: spawn an adapter (claude-agent-acp / codex-acp) as a
|
|
8
6
|
* subprocess, speak Agent Client Protocol over its stdio, and translate
|
|
9
7
|
* session/update notifications into chat events. One subprocess lives for
|
|
10
|
-
* the whole chat; the ACP session id carries the conversation.
|
|
11
|
-
*
|
|
12
|
-
* Auth rides on the local CLI logins (Claude Code / Codex) via the
|
|
13
|
-
* inherited environment — same story as the kraftwerk harnesses.
|
|
8
|
+
* the whole chat; the ACP session id carries the conversation. The
|
|
9
|
+
* adapter plumbing is shared with the workflow ACP harness (src/acp.ts).
|
|
14
10
|
*/
|
|
15
|
-
const ADAPTERS = {
|
|
16
|
-
claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
|
|
17
|
-
codex: "@agentclientprotocol/codex-acp/dist/index.js",
|
|
18
|
-
};
|
|
19
|
-
/** Effort tier -> Claude thinking budget (the adapter reads MAX_THINKING_TOKENS). */
|
|
20
|
-
const CLAUDE_THINKING_BUDGET = {
|
|
21
|
-
low: 2048,
|
|
22
|
-
medium: 8192,
|
|
23
|
-
high: 16384,
|
|
24
|
-
xhigh: 32000,
|
|
25
|
-
max: 63999,
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Model/effort overrides ride on adapter-specific channels: the claude
|
|
29
|
-
* adapter takes the model via session `_meta.claudeCode.options` and the
|
|
30
|
-
* thinking budget via env; codex-acp merges a CODEX_CONFIG env JSON into
|
|
31
|
-
* the session config it hands to `codex app-server`.
|
|
32
|
-
*/
|
|
33
|
-
function tuningEnv(agent, tuning) {
|
|
34
|
-
const env = { ...process.env };
|
|
35
|
-
if (agent === "claude" && tuning.effort && CLAUDE_THINKING_BUDGET[tuning.effort]) {
|
|
36
|
-
env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[tuning.effort]);
|
|
37
|
-
}
|
|
38
|
-
if (agent === "codex" && (tuning.model || tuning.effort)) {
|
|
39
|
-
env.CODEX_CONFIG = JSON.stringify({
|
|
40
|
-
...(tuning.model ? { model: tuning.model } : {}),
|
|
41
|
-
...(tuning.effort ? { model_reasoning_effort: tuning.effort } : {}),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return env;
|
|
45
|
-
}
|
|
46
11
|
function contentText(content) {
|
|
47
12
|
return content.type === "text" ? content.text : "";
|
|
48
13
|
}
|
|
49
14
|
export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
50
|
-
const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
|
|
51
|
-
const child = spawn(process.execPath, [entry], {
|
|
52
|
-
cwd,
|
|
53
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
54
|
-
env: tuningEnv(agent, tuning),
|
|
55
|
-
});
|
|
56
|
-
let stderr = "";
|
|
57
|
-
child.stderr.on("data", (c) => {
|
|
58
|
-
stderr += c.toString("utf8");
|
|
59
|
-
if (stderr.length > 20_000)
|
|
60
|
-
stderr = stderr.slice(-20_000);
|
|
61
|
-
});
|
|
62
15
|
let dead = false;
|
|
63
|
-
// Without a listener a failed spawn (cwd gone, node missing) is an
|
|
64
|
-
// unhandled 'error' event that takes the whole inspector down.
|
|
65
|
-
child.on("error", (err) => {
|
|
66
|
-
dead = true;
|
|
67
|
-
hooks.emit({ type: "error", message: `could not start the ${agent} agent: ${err.message}` });
|
|
68
|
-
});
|
|
69
|
-
child.on("close", (code) => {
|
|
70
|
-
dead = true;
|
|
71
|
-
if (code !== 0 && code !== null) {
|
|
72
|
-
hooks.emit({
|
|
73
|
-
type: "error",
|
|
74
|
-
message: `${agent} agent exited (code ${code})${stderr.trim() ? `: ${stderr.trim().slice(-500)}` : ""}`,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
16
|
const client = {
|
|
79
17
|
sessionUpdate(params) {
|
|
80
18
|
const u = params.update;
|
|
@@ -118,12 +56,24 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
|
118
56
|
: { outcome: { outcome: "cancelled" } };
|
|
119
57
|
},
|
|
120
58
|
};
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
59
|
+
const { child, conn } = await connectAcp(agent, {
|
|
60
|
+
cwd,
|
|
61
|
+
env: adapterEnv(agent, tuning),
|
|
62
|
+
client,
|
|
63
|
+
clientName: "kraftwerk-inspector",
|
|
64
|
+
onError: (err) => {
|
|
65
|
+
dead = true;
|
|
66
|
+
hooks.emit({ type: "error", message: `could not start the ${agent} agent: ${err.message}` });
|
|
67
|
+
},
|
|
68
|
+
onClose: (code, stderr) => {
|
|
69
|
+
dead = true;
|
|
70
|
+
if (code !== 0 && code !== null) {
|
|
71
|
+
hooks.emit({
|
|
72
|
+
type: "error",
|
|
73
|
+
message: `${agent} agent exited (code ${code})${stderr ? `: ${stderr.slice(-500)}` : ""}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
127
77
|
});
|
|
128
78
|
// Claude-only session options ride on _meta.claudeCode.options (the
|
|
129
79
|
// adapter spreads them into the Agent SDK options): model override,
|
|
@@ -333,7 +333,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
333
333
|
const bundleLines = bundles
|
|
334
334
|
.map((b) => `- ${b.name} (${b.concepts} concepts${b.updatedAt ? `, updated ${b.updatedAt.slice(0, 10)}` : ""})`)
|
|
335
335
|
.join("\n");
|
|
336
|
-
return (`You are the knowledge curator inside the kraftwerk inspector ("
|
|
336
|
+
return (`You are the knowledge curator inside the kraftwerk inspector ("Knowledge"). ` +
|
|
337
337
|
`This project keeps knowledge as OKF v0.2 bundles (Open Knowledge Format): directories of markdown ` +
|
|
338
338
|
`files with YAML frontmatter under ${root || "knowledge/"}. Each direct subdirectory is one bundle; each .md file ` +
|
|
339
339
|
`(except the reserved index.md and log.md) is one concept.\n\n` +
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BundleInfo, type ConceptDetail, type ConceptInfo } from "../okf.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Knowledge for the inspector: a thin API layer over the OKF
|
|
4
4
|
* core (src/okf.ts). Writes from the UI are stamped as `human:user` — the
|
|
5
5
|
* browser user is the human in the loop; agents write through the CLI (or
|
|
6
6
|
* chat) with their own actor.
|
|
@@ -4,7 +4,7 @@ import { resolveProject } from "../config.js";
|
|
|
4
4
|
import { getConcept, initBundle, knowledgeRoot, listBundles, listConcepts, safeBundleName, verifyConcept, writeConcept, } from "../okf.js";
|
|
5
5
|
import { getProjectRoot } from "./context.js";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Knowledge for the inspector: a thin API layer over the OKF
|
|
8
8
|
* core (src/okf.ts). Writes from the UI are stamped as `human:user` — the
|
|
9
9
|
* browser user is the human in the loop; agents write through the CLI (or
|
|
10
10
|
* chat) with their own actor.
|
package/dist/inspector/runs.d.ts
CHANGED
package/dist/inspector/runs.js
CHANGED
package/dist/inspector/server.js
CHANGED
|
@@ -645,8 +645,8 @@ async function handleApi(req, res, url) {
|
|
|
645
645
|
return json(res, { error: "invalid JSON body" }, 400);
|
|
646
646
|
}
|
|
647
647
|
const request = (body.request ?? "").trim();
|
|
648
|
-
if (!request)
|
|
649
|
-
return json(res, { error: "
|
|
648
|
+
if (!request && wf.usesRequest)
|
|
649
|
+
return json(res, { error: "this workflow needs a request" }, 400);
|
|
650
650
|
try {
|
|
651
651
|
const { runId } = triggerRun({
|
|
652
652
|
workflowName: wf.name,
|
|
@@ -3,6 +3,7 @@ export interface AgentInfo {
|
|
|
3
3
|
name?: string;
|
|
4
4
|
model?: string;
|
|
5
5
|
harness?: string;
|
|
6
|
+
protocol?: string;
|
|
6
7
|
effort?: string;
|
|
7
8
|
tools: string[];
|
|
8
9
|
persona?: string;
|
|
@@ -25,6 +26,8 @@ export interface WorkflowSummary {
|
|
|
25
26
|
description?: string;
|
|
26
27
|
agents: number;
|
|
27
28
|
steps: number;
|
|
29
|
+
/** False when no step reads `${{ request }}` — the ▶ button runs it straight away. */
|
|
30
|
+
usesRequest: boolean;
|
|
28
31
|
error?: string;
|
|
29
32
|
}
|
|
30
33
|
export interface WorkflowDetail {
|
|
@@ -36,6 +39,7 @@ export interface WorkflowDetail {
|
|
|
36
39
|
agents: AgentInfo[];
|
|
37
40
|
steps: StepInfo[];
|
|
38
41
|
files: string[];
|
|
42
|
+
usesRequest: boolean;
|
|
39
43
|
error?: string;
|
|
40
44
|
}
|
|
41
45
|
export declare function listWorkflows(): Promise<{
|
|
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
|
+
import { referencesRequest } from "../yaml.js";
|
|
5
6
|
import { getProjectRoot } from "./context.js";
|
|
6
7
|
/**
|
|
7
8
|
* Workflow discovery + parsing for the inspector. Uses the same root
|
|
@@ -80,6 +81,16 @@ async function parseRaw(l) {
|
|
|
80
81
|
const text = await fs.readFile(l.yamlPath, "utf8");
|
|
81
82
|
return parseYaml(text);
|
|
82
83
|
}
|
|
84
|
+
/** Does any step read the request (prompt or script, inline or via a file in the folder)? */
|
|
85
|
+
async function stepsUseRequest(raw, baseDir) {
|
|
86
|
+
for (const s of raw?.steps ?? []) {
|
|
87
|
+
const script = s?.run !== undefined;
|
|
88
|
+
const { text } = await resolveText(script ? s.run : s?.prompt, baseDir);
|
|
89
|
+
if (text && referencesRequest(text, script ? "script" : "agent"))
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
83
94
|
export async function listWorkflows() {
|
|
84
95
|
const root = await workflowsRoot();
|
|
85
96
|
const located = await locate();
|
|
@@ -92,10 +103,11 @@ export async function listWorkflows() {
|
|
|
92
103
|
description: raw?.description,
|
|
93
104
|
agents: Object.keys(raw?.agents ?? {}).length,
|
|
94
105
|
steps: (raw?.steps ?? []).length,
|
|
106
|
+
usesRequest: await stepsUseRequest(raw, l.baseDir),
|
|
95
107
|
};
|
|
96
108
|
}
|
|
97
109
|
catch (err) {
|
|
98
|
-
return { slug: l.slug, agents: 0, steps: 0, error: err.message };
|
|
110
|
+
return { slug: l.slug, agents: 0, steps: 0, usesRequest: true, error: err.message };
|
|
99
111
|
}
|
|
100
112
|
}));
|
|
101
113
|
return { root, workflows };
|
|
@@ -127,6 +139,7 @@ export async function getWorkflow(slug) {
|
|
|
127
139
|
agents: [],
|
|
128
140
|
steps: [],
|
|
129
141
|
files: [],
|
|
142
|
+
usesRequest: true,
|
|
130
143
|
error: err.message,
|
|
131
144
|
};
|
|
132
145
|
}
|
|
@@ -138,6 +151,7 @@ export async function getWorkflow(slug) {
|
|
|
138
151
|
name: a?.name,
|
|
139
152
|
model: a?.model,
|
|
140
153
|
harness: a?.harness,
|
|
154
|
+
protocol: a?.protocol ?? raw?.protocol,
|
|
141
155
|
effort: a?.effort,
|
|
142
156
|
tools: a?.tools ?? [],
|
|
143
157
|
persona: persona.text,
|
|
@@ -178,5 +192,6 @@ export async function getWorkflow(slug) {
|
|
|
178
192
|
agents,
|
|
179
193
|
steps,
|
|
180
194
|
files: l.baseDir ? await listDirFiles(l.baseDir) : [path.basename(l.yamlPath)],
|
|
195
|
+
usesRequest: await stepsUseRequest(raw, l.baseDir),
|
|
181
196
|
};
|
|
182
197
|
}
|
package/dist/okf.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OKF (Open Knowledge Format) core — kraftwerk's "
|
|
2
|
+
* OKF (Open Knowledge Format) core — kraftwerk's "Knowledge"
|
|
3
3
|
* feature. Implements OKF v0.2 (github.com/GoogleCloudPlatform/knowledge-catalog):
|
|
4
4
|
* a knowledge bundle is a directory of markdown files with YAML frontmatter,
|
|
5
5
|
* one concept per file, plus the reserved index.md / log.md.
|
package/dist/okf.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml, parseDocument } from "yaml";
|
|
4
4
|
/**
|
|
5
|
-
* OKF (Open Knowledge Format) core — kraftwerk's "
|
|
5
|
+
* OKF (Open Knowledge Format) core — kraftwerk's "Knowledge"
|
|
6
6
|
* feature. Implements OKF v0.2 (github.com/GoogleCloudPlatform/knowledge-catalog):
|
|
7
7
|
* a knowledge bundle is a directory of markdown files with YAML frontmatter,
|
|
8
8
|
* one concept per file, plus the reserved index.md / log.md.
|
package/dist/run.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface RunOptions {
|
|
|
30
30
|
maxGateRetries?: number;
|
|
31
31
|
}
|
|
32
32
|
export declare class Run {
|
|
33
|
+
/** Session per harness AND protocol: a CLI session id means nothing to the ACP adapter and vice versa. */
|
|
33
34
|
private readonly sessions;
|
|
34
35
|
/** Per-phase stats in execution order; repeated phases appear once per execution. */
|
|
35
36
|
readonly stats: PhaseStats[];
|
package/dist/run.js
CHANGED
|
@@ -5,6 +5,7 @@ import { harnessFor } from "./harnesses/registry.js";
|
|
|
5
5
|
import { correctionPrompt, parseEnvelope } from "./envelope.js";
|
|
6
6
|
import { fmtDuration, phaseStatsLine, summaryTable } from "./stats.js";
|
|
7
7
|
export class Run {
|
|
8
|
+
/** Session per harness AND protocol: a CLI session id means nothing to the ACP adapter and vice versa. */
|
|
8
9
|
sessions = new Map();
|
|
9
10
|
/** Per-phase stats in execution order; repeated phases appear once per execution. */
|
|
10
11
|
stats = [];
|
|
@@ -23,7 +24,8 @@ export class Run {
|
|
|
23
24
|
}
|
|
24
25
|
async agentPhase(params) {
|
|
25
26
|
const { agent } = params;
|
|
26
|
-
const harness = harnessFor(agent.harness);
|
|
27
|
+
const harness = harnessFor(agent.harness, agent.protocol);
|
|
28
|
+
const sessionKey = `${harness.id}/${agent.protocol ?? "cli"}`;
|
|
27
29
|
const modelName = harness.id === "claude" ? agent.model : `${harness.id}:${agent.model}`;
|
|
28
30
|
const modelLabel = agent.effort ? `${modelName}, effort ${agent.effort}` : modelName;
|
|
29
31
|
console.log(`\n▸ Phase "${params.name}" — ${agent.name} [${agent.id}] (${modelLabel})`);
|
|
@@ -32,17 +34,19 @@ export class Run {
|
|
|
32
34
|
kind: "agent",
|
|
33
35
|
agent: agent.id,
|
|
34
36
|
harness: harness.id,
|
|
37
|
+
protocol: agent.protocol ?? "cli",
|
|
35
38
|
model: agent.model,
|
|
36
39
|
effort: agent.effort ?? null,
|
|
37
40
|
clis: agent.clis ? Object.keys(agent.clis) : [],
|
|
38
41
|
mcp: agent.mcp ? Object.keys(agent.mcp) : [],
|
|
39
|
-
resume: this.sessions.get(
|
|
42
|
+
resume: this.sessions.get(sessionKey) ?? null,
|
|
40
43
|
});
|
|
41
44
|
const phaseStats = {
|
|
42
45
|
phase: params.name,
|
|
43
46
|
kind: "agent",
|
|
44
47
|
agent: agent.id,
|
|
45
48
|
harness: harness.id,
|
|
49
|
+
...(agent.protocol === "acp" ? { protocol: "acp" } : {}),
|
|
46
50
|
model: agent.model,
|
|
47
51
|
effort: agent.effort,
|
|
48
52
|
attempts: 0,
|
|
@@ -74,7 +78,7 @@ export class Run {
|
|
|
74
78
|
tools: agent.tools,
|
|
75
79
|
clis: cliEntries.map(([name]) => name),
|
|
76
80
|
mcpServers: agent.mcp,
|
|
77
|
-
resume: this.sessions.get(
|
|
81
|
+
resume: this.sessions.get(sessionKey),
|
|
78
82
|
onToolUse: (tool, target) => {
|
|
79
83
|
console.log(` ⚙ ${tool} ${target}`.trimEnd());
|
|
80
84
|
void this.trace("tool_use", { phase: params.name, attempt, tool, target });
|
|
@@ -82,7 +86,7 @@ export class Run {
|
|
|
82
86
|
onText: this.verbose ? (text) => console.log(` 💬 ${text}`) : undefined,
|
|
83
87
|
});
|
|
84
88
|
if (result.sessionId)
|
|
85
|
-
this.sessions.set(
|
|
89
|
+
this.sessions.set(sessionKey, result.sessionId);
|
|
86
90
|
phaseStats.attempts = attempt + 1;
|
|
87
91
|
phaseStats.durationMs = Date.now() - started;
|
|
88
92
|
phaseStats.costUsd += result.costUsd ?? 0;
|
package/dist/stats.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface PhaseStats {
|
|
|
9
9
|
agent?: string;
|
|
10
10
|
/** Harness id, for agent phases. */
|
|
11
11
|
harness?: string;
|
|
12
|
+
/** "acp" when the phase ran over the Agent Client Protocol (absent = CLI). */
|
|
13
|
+
protocol?: string;
|
|
12
14
|
/** Model id, for agent phases. */
|
|
13
15
|
model?: string;
|
|
14
16
|
effort?: string;
|
package/dist/yaml.d.ts
CHANGED
|
@@ -7,9 +7,21 @@ export interface LoadedWorkflow extends WorkflowDefinition {
|
|
|
7
7
|
steps: string[];
|
|
8
8
|
/** Environment variables the workflow declares under `requires:`. */
|
|
9
9
|
requires: string[];
|
|
10
|
+
/** False when no step reads `${{ request }}` — such a workflow runs without a request. */
|
|
11
|
+
usesRequest: boolean;
|
|
10
12
|
};
|
|
11
13
|
run(opts: Parameters<WorkflowDefinition["run"]>[0]): Promise<RunResult>;
|
|
12
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Does a prompt or script read the request? Prompts through `${{ request }}`;
|
|
17
|
+
* scripts through that or the REQUEST env var — matched as the bare word,
|
|
18
|
+
* because a script may read it from bash (`$REQUEST`), Python
|
|
19
|
+
* (`os.environ.get("REQUEST")`) or anything else. A mention in a comment
|
|
20
|
+
* counts too: better one dialog too many than a run without its input.
|
|
21
|
+
* A workflow whose steps never read it runs without one (`kraftwerk run
|
|
22
|
+
* <name>`, the ▶ in the UI) — the request is an argument, not a ritual.
|
|
23
|
+
*/
|
|
24
|
+
export declare const referencesRequest: (text: string, kind: "agent" | "script") => boolean;
|
|
13
25
|
/** Names from `requires:` that are missing/empty in the current environment. */
|
|
14
26
|
export declare const missingEnv: (requires: string[]) => string[];
|
|
15
27
|
/**
|
package/dist/yaml.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Ajv } from "ajv";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
|
+
import { disposeAcpSessions } from "./harnesses/acp.js";
|
|
5
6
|
import { envelopeContract } from "./envelope.js";
|
|
6
7
|
import { checkScript, containsText, fileNonEmpty, slotsFilled } from "./gates.js";
|
|
7
8
|
import { Run } from "./run.js";
|
|
@@ -77,6 +78,16 @@ function formatAjvErrors(errors, file) {
|
|
|
77
78
|
const cliHint = errors.some((e) => e.instancePath.startsWith("/clis")) ? `\n${CLI_HINT}` : "";
|
|
78
79
|
return `${file}: schema validation failed\n${lines.map((l) => ` - ${l}`).join("\n")}${gateHint}${stepHint}${mcpHint}${cliHint}`;
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Does a prompt or script read the request? Prompts through `${{ request }}`;
|
|
83
|
+
* scripts through that or the REQUEST env var — matched as the bare word,
|
|
84
|
+
* because a script may read it from bash (`$REQUEST`), Python
|
|
85
|
+
* (`os.environ.get("REQUEST")`) or anything else. A mention in a comment
|
|
86
|
+
* counts too: better one dialog too many than a run without its input.
|
|
87
|
+
* A workflow whose steps never read it runs without one (`kraftwerk run
|
|
88
|
+
* <name>`, the ▶ in the UI) — the request is an argument, not a ritual.
|
|
89
|
+
*/
|
|
90
|
+
export const referencesRequest = (text, kind) => /\$\{\{\s*request\s*\}\}/.test(text) || (kind === "script" && /\bREQUEST\b/.test(text));
|
|
80
91
|
/** Names from `requires:` that are missing/empty in the current environment. */
|
|
81
92
|
export const missingEnv = (requires) => requires.filter((name) => !process.env[name]);
|
|
82
93
|
/**
|
|
@@ -176,8 +187,15 @@ export async function loadWorkflow(givenPath) {
|
|
|
176
187
|
// CLIs available to agents: command prefix -> one-line usage hint (may be
|
|
177
188
|
// empty). Purely declarative; agents opt in below.
|
|
178
189
|
const cliDefs = new Map(Object.entries(raw.clis ?? {}).map(([name, hint]) => [name, String(hint ?? "")]));
|
|
190
|
+
// `protocol: acp` drives a harness over the Agent Client Protocol instead
|
|
191
|
+
// of its CLI — workflow-wide, or per agent. pi has no adapter.
|
|
192
|
+
const protocolDefault = raw.protocol ?? "cli";
|
|
179
193
|
const agents = new Map();
|
|
180
194
|
for (const [id, a] of Object.entries(raw.agents ?? {})) {
|
|
195
|
+
const protocol = a.protocol ?? protocolDefault;
|
|
196
|
+
if (protocol === "acp" && a["runs-on"] === "pi") {
|
|
197
|
+
fail(`agents.${id}: runs-on "pi" has no agent-protocol adapter — use claude or codex, or protocol: cli`);
|
|
198
|
+
}
|
|
181
199
|
const mcp = {};
|
|
182
200
|
for (const serverName of (a.mcp ?? [])) {
|
|
183
201
|
const def = mcpDefs.get(serverName);
|
|
@@ -206,6 +224,7 @@ export async function loadWorkflow(givenPath) {
|
|
|
206
224
|
tools: a.tools,
|
|
207
225
|
persona: await resolveText(a.persona, `agents.${id}.persona`),
|
|
208
226
|
harness: a["runs-on"],
|
|
227
|
+
...(protocol === "acp" ? { protocol } : {}),
|
|
209
228
|
...(Object.keys(clis).length > 0 ? { clis } : {}),
|
|
210
229
|
...(Object.keys(mcp).length > 0 ? { mcp } : {}),
|
|
211
230
|
});
|
|
@@ -255,6 +274,7 @@ export async function loadWorkflow(givenPath) {
|
|
|
255
274
|
agents: [...agents.values()],
|
|
256
275
|
steps: steps.map((s) => s.name),
|
|
257
276
|
requires,
|
|
277
|
+
usesRequest: steps.some((s) => referencesRequest(s.kind === "script" ? s.script : s.prompt, s.kind)),
|
|
258
278
|
},
|
|
259
279
|
async run({ request, verbose }) {
|
|
260
280
|
const missing = missingEnv(requires);
|
|
@@ -286,48 +306,53 @@ export async function loadWorkflow(givenPath) {
|
|
|
286
306
|
? { name: s.name, kind: "script" }
|
|
287
307
|
: { name: s.name, kind: "agent", agent: s.agent.id, model: s.agent.model }),
|
|
288
308
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
309
|
+
try {
|
|
310
|
+
for (const step of steps) {
|
|
311
|
+
// `if:` preconditions — deterministic checks on run-dir files. Any
|
|
312
|
+
// unmet precondition SKIPS the step (the run continues); this is how
|
|
313
|
+
// a workflow avoids spawning an agent when there is nothing to do.
|
|
314
|
+
if (step.preconditions.length > 0) {
|
|
315
|
+
const unmet = [];
|
|
316
|
+
for (const pre of step.preconditions) {
|
|
317
|
+
const failure = await pre.check(runDir);
|
|
318
|
+
if (failure)
|
|
319
|
+
unmet.push(`${pre.name}: ${failure}`);
|
|
320
|
+
}
|
|
321
|
+
if (unmet.length > 0) {
|
|
322
|
+
console.log(`\n▸ Phase "${step.name}" — skipped (if: not met)`);
|
|
323
|
+
for (const reason of unmet)
|
|
324
|
+
console.log(` ↷ ${reason}`);
|
|
325
|
+
await run.trace("phase_skipped", { phase: step.name, unmet });
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
299
328
|
}
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
329
|
+
if (step.kind === "script") {
|
|
330
|
+
await run.scriptPhase({
|
|
331
|
+
name: step.name,
|
|
332
|
+
script: step.script.replace(/\$\{\{\s*request\s*\}\}/g, request),
|
|
333
|
+
gates: step.gates,
|
|
334
|
+
env: {
|
|
335
|
+
REQUEST: request,
|
|
336
|
+
...(baseDir ? { WORKFLOW_DIR: path.resolve(baseDir) } : {}),
|
|
337
|
+
},
|
|
338
|
+
});
|
|
305
339
|
continue;
|
|
306
340
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
341
|
+
const prompt = step.prompt
|
|
342
|
+
.replace(/\$\{\{\s*request\s*\}\}/g, request)
|
|
343
|
+
.replace(/\$\{\{\s*agent\s*\}\}/g, step.agent.id);
|
|
344
|
+
await run.agentPhase({
|
|
310
345
|
name: step.name,
|
|
311
|
-
|
|
346
|
+
agent: step.agent,
|
|
347
|
+
prompt: `${prompt}\n\n${envelopeContract(step.name)}`,
|
|
312
348
|
gates: step.gates,
|
|
313
|
-
env: {
|
|
314
|
-
REQUEST: request,
|
|
315
|
-
...(baseDir ? { WORKFLOW_DIR: path.resolve(baseDir) } : {}),
|
|
316
|
-
},
|
|
317
349
|
});
|
|
318
|
-
continue;
|
|
319
350
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
name: step.name,
|
|
325
|
-
agent: step.agent,
|
|
326
|
-
prompt: `${prompt}\n\n${envelopeContract(step.name)}`,
|
|
327
|
-
gates: step.gates,
|
|
328
|
-
});
|
|
351
|
+
await run.printSummary();
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
disposeAcpSessions(runDir);
|
|
329
355
|
}
|
|
330
|
-
await run.printSummary();
|
|
331
356
|
console.log(`\nArtifacts: ${runDir}`);
|
|
332
357
|
return { runDir, phases: run.stats, total: summaryTable(run.stats).total };
|
|
333
358
|
},
|