@netnodeag/kraftwerk 0.40.0 → 0.42.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 +68 -0
- 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/kraftwerk.js +12 -6
- 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/channels.d.ts +48 -0
- package/dist/inspector/channels.js +130 -0
- package/dist/inspector/chat/acp.js +22 -72
- package/dist/inspector/chat/sessions.d.ts +17 -1
- package/dist/inspector/chat/sessions.js +280 -44
- package/dist/inspector/chat/types.d.ts +18 -0
- package/dist/inspector/notifications.js +2 -0
- package/dist/inspector/runs.d.ts +1 -0
- package/dist/inspector/runs.js +1 -0
- package/dist/inspector/server.js +76 -4
- package/dist/inspector/workflows.d.ts +4 -0
- package/dist/inspector/workflows.js +16 -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-AXonJXol.js → dist-5tG7aS9B.js} +1 -1
- package/inspector/dist/assets/{dist-DdbyO9w9.js → dist-C3XpBw6W.js} +1 -1
- package/inspector/dist/assets/{dist-Dyv2EX0S.js → dist-CE4xSQoL.js} +1 -1
- package/inspector/dist/assets/{dist-HuQTcyVK.js → dist-CvxuLI-i.js} +1 -1
- package/inspector/dist/assets/{dist-BJLTdJRF.js → dist-CxiuM_jC.js} +1 -1
- package/inspector/dist/assets/{dist-uQIhtGKj.js → dist-D8KGprRA.js} +1 -1
- package/inspector/dist/assets/{dist-XL4xXfDo.js → dist-DAeASF1A.js} +1 -1
- package/inspector/dist/assets/{dist-D9-MFS0x.js → dist-DmP7Dxbp.js} +1 -1
- package/inspector/dist/assets/{dist-Bv3fOisq.js → dist-DzY0J5Mz.js} +1 -1
- package/inspector/dist/assets/dist-IEe8XFs2.js +1 -0
- package/inspector/dist/assets/{dist-Cex4_N6v.js → dist-W68I5dk2.js} +1 -1
- package/inspector/dist/assets/{editor--4qv8VP5.js → editor-CyEovn1g.js} +3 -3
- package/inspector/dist/assets/{index-BSqc9eg2.js → index-B2d-nOUv.js} +17 -17
- package/inspector/dist/assets/index-BRw2ED9p.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-UTSke4yL.js +0 -1
- package/inspector/dist/assets/index-E1E7-_NW.css +0 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
import { getProjectRoot } from "./context.js";
|
|
5
|
+
import { resolveProject } from "../config.js";
|
|
6
|
+
import { listAgents, safeAgentSlug, slugFromName } from "./agents.js";
|
|
7
|
+
export const DEFAULT_MAX_HOPS = 3;
|
|
8
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
|
|
9
|
+
export async function channelsRoot() {
|
|
10
|
+
const project = await resolveProject(getProjectRoot());
|
|
11
|
+
return path.resolve(project.root, "channels");
|
|
12
|
+
}
|
|
13
|
+
export function safeChannelSlug(slug) {
|
|
14
|
+
if (!SLUG_RE.test(slug))
|
|
15
|
+
throw new Error(`invalid channel slug "${slug}"`);
|
|
16
|
+
return slug;
|
|
17
|
+
}
|
|
18
|
+
function normalize(slug, raw) {
|
|
19
|
+
const members = Array.isArray(raw.members) ? [...new Set(raw.members.map(String))] : [];
|
|
20
|
+
const responder = typeof raw.responder === "string" && members.includes(raw.responder) ? raw.responder : undefined;
|
|
21
|
+
const hops = typeof raw.maxHops === "number" && Number.isInteger(raw.maxHops) && raw.maxHops >= 0 ? raw.maxHops : DEFAULT_MAX_HOPS;
|
|
22
|
+
return {
|
|
23
|
+
slug,
|
|
24
|
+
name: String(raw.name ?? slug),
|
|
25
|
+
...(raw.purpose ? { purpose: String(raw.purpose) } : {}),
|
|
26
|
+
members,
|
|
27
|
+
...(responder ? { responder } : {}),
|
|
28
|
+
maxHops: hops,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function listChannels() {
|
|
32
|
+
const root = await channelsRoot();
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = (await fs.readdir(root, { withFileTypes: true }))
|
|
36
|
+
.filter((e) => e.isDirectory() && SLUG_RE.test(e.name))
|
|
37
|
+
.map((e) => e.name);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const channels = await Promise.all(entries.map((slug) => getChannel(slug)));
|
|
43
|
+
return channels.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
45
|
+
export async function getChannel(slug) {
|
|
46
|
+
const file = path.join(await channelsRoot(), safeChannelSlug(slug), "channel.yml");
|
|
47
|
+
try {
|
|
48
|
+
const raw = parse(await fs.readFile(file, "utf8"));
|
|
49
|
+
return normalize(slug, (raw ?? {}));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Members must be existing, non-archived agents; returns them in the order given. */
|
|
56
|
+
async function checkMembers(slugs) {
|
|
57
|
+
const agents = await listAgents();
|
|
58
|
+
return slugs.map((slug) => {
|
|
59
|
+
const a = agents.find((m) => m.slug === safeAgentSlug(slug));
|
|
60
|
+
if (!a)
|
|
61
|
+
throw new Error(`agent "${slug}" not found`);
|
|
62
|
+
if (a.archived)
|
|
63
|
+
throw new Error(`agent "${slug}" is archived`);
|
|
64
|
+
return a;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Create (no slug: derived from the name) or update a channel definition. */
|
|
68
|
+
export async function saveChannel(input) {
|
|
69
|
+
const existing = input.slug ? await getChannel(safeChannelSlug(input.slug)) : null;
|
|
70
|
+
const name = (input.name ?? existing?.name ?? "").trim();
|
|
71
|
+
if (!name)
|
|
72
|
+
throw new Error("name is required");
|
|
73
|
+
const slug = input.slug ? safeChannelSlug(input.slug) : slugFromName(name);
|
|
74
|
+
if (!input.slug && (await getChannel(slug)))
|
|
75
|
+
throw new Error(`channel "${slug}" exists already`);
|
|
76
|
+
const members = input.members !== undefined ? [...new Set(input.members.map(String))] : existing?.members ?? [];
|
|
77
|
+
if (members.length === 0)
|
|
78
|
+
throw new Error("a channel needs at least one agent");
|
|
79
|
+
await checkMembers(members);
|
|
80
|
+
let responder = existing?.responder;
|
|
81
|
+
if (input.responder !== undefined)
|
|
82
|
+
responder = input.responder ? input.responder : undefined;
|
|
83
|
+
if (responder && !members.includes(responder))
|
|
84
|
+
responder = undefined;
|
|
85
|
+
const maxHops = input.maxHops !== undefined
|
|
86
|
+
? Number.isInteger(input.maxHops) && input.maxHops >= 0
|
|
87
|
+
? input.maxHops
|
|
88
|
+
: (() => {
|
|
89
|
+
throw new Error("maxHops must be a whole number");
|
|
90
|
+
})()
|
|
91
|
+
: existing?.maxHops ?? DEFAULT_MAX_HOPS;
|
|
92
|
+
const channel = {
|
|
93
|
+
slug,
|
|
94
|
+
name,
|
|
95
|
+
...(input.purpose !== undefined
|
|
96
|
+
? input.purpose.trim()
|
|
97
|
+
? { purpose: input.purpose.trim() }
|
|
98
|
+
: {}
|
|
99
|
+
: existing?.purpose
|
|
100
|
+
? { purpose: existing.purpose }
|
|
101
|
+
: {}),
|
|
102
|
+
members,
|
|
103
|
+
...(responder ? { responder } : {}),
|
|
104
|
+
maxHops,
|
|
105
|
+
};
|
|
106
|
+
const dir = path.join(await channelsRoot(), slug);
|
|
107
|
+
await fs.mkdir(dir, { recursive: true });
|
|
108
|
+
const { slug: _s, ...body } = channel;
|
|
109
|
+
await fs.writeFile(path.join(dir, "channel.yml"), stringify(body));
|
|
110
|
+
return channel;
|
|
111
|
+
}
|
|
112
|
+
export async function deleteChannel(slug) {
|
|
113
|
+
await fs.rm(path.join(await channelsRoot(), safeChannelSlug(slug)), { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Who a channel message wakes. @mentions of members win (in text order, each
|
|
117
|
+
* once); a message that mentions nobody goes to the responder, or to nobody.
|
|
118
|
+
* `self` (the author, when an agent) never wakes itself.
|
|
119
|
+
*/
|
|
120
|
+
export function mentionTargets(text, channel, self) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const m of text.matchAll(/(^|[^\w@])@([a-z0-9][a-z0-9-]*)/g)) {
|
|
123
|
+
const slug = m[2];
|
|
124
|
+
if (channel.members.includes(slug) && slug !== self && !out.includes(slug))
|
|
125
|
+
out.push(slug);
|
|
126
|
+
}
|
|
127
|
+
if (out.length === 0 && channel.responder && channel.responder !== self && !self)
|
|
128
|
+
return [channel.responder];
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
@@ -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,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatAgentId, ChatMeta, ChatScope, StoredChatEvent } from "./types.js";
|
|
2
|
+
import { type Channel } from "../channels.js";
|
|
2
3
|
/** Server shutdown: no agent subprocess may outlive the inspector. */
|
|
3
4
|
export declare function disposeAllBackends(): void;
|
|
4
5
|
/**
|
|
@@ -32,9 +33,24 @@ export declare function getChat(id: string): Promise<{
|
|
|
32
33
|
events: StoredChatEvent[];
|
|
33
34
|
busy: boolean;
|
|
34
35
|
} | null>;
|
|
35
|
-
export declare function postMessage(id: string, text: string
|
|
36
|
+
export declare function postMessage(id: string, text: string, opts?: {
|
|
37
|
+
from?: string;
|
|
38
|
+
}): Promise<{
|
|
36
39
|
error?: string;
|
|
37
40
|
}>;
|
|
41
|
+
/** The chat that holds a channel's transcript; created on first use. */
|
|
42
|
+
export declare function ensureChannelChat(channel: Channel): Promise<ChatMeta>;
|
|
43
|
+
/**
|
|
44
|
+
* "Add a coworker" to an agent session: the chat becomes the channel's
|
|
45
|
+
* transcript. The agent keeps its process and memory — it has seen every
|
|
46
|
+
* message so far — and learns the channel rules with its next prompt.
|
|
47
|
+
*/
|
|
48
|
+
export declare function convertChatToChannel(chatId: string, channel: Channel): Promise<{
|
|
49
|
+
error?: string;
|
|
50
|
+
meta?: ChatMeta;
|
|
51
|
+
}>;
|
|
52
|
+
/** Members left the channel: their processes go, the transcript stays. */
|
|
53
|
+
export declare function dropChannelSeats(chatId: string, keep: string[]): Promise<void>;
|
|
38
54
|
/**
|
|
39
55
|
* Open a vibeable in the chat (or close it with null). The agent's cwd
|
|
40
56
|
* becomes the app folder, so the running subprocess is dropped and the next
|