@netnodeag/kraftwerk 0.14.2 → 0.16.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 +10 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +35 -2
- package/dist/inspector/chat/sessions.d.ts +2 -0
- package/dist/inspector/chat/sessions.js +33 -3
- package/dist/inspector/server.js +16 -2
- package/inspector/dist/assets/index-Bo7R5E73.js +80 -0
- package/inspector/dist/assets/index-CSUBZW5F.css +1 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +25 -0
- package/inspector/dist/assets/index-B1Vm-R4m.css +0 -1
- package/inspector/dist/assets/index-BAmGK4eW.js +0 -80
package/README.md
CHANGED
|
@@ -96,6 +96,16 @@ directory, default `output/`), `knowledge:` (OKF knowledge-bundle root,
|
|
|
96
96
|
default `knowledge/`), and `agents:` (team agent-definition root, default
|
|
97
97
|
`agents/`).
|
|
98
98
|
|
|
99
|
+
`switcher:` links other kraftwerk workspaces from the inspector header —
|
|
100
|
+
the workspace name becomes a dropdown listing them:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
switcher:
|
|
104
|
+
- name: other space
|
|
105
|
+
url: https://localhost:1985
|
|
106
|
+
icon: "🛰" # optional
|
|
107
|
+
```
|
|
108
|
+
|
|
99
109
|
### Triggering from CI / cron / webhooks
|
|
100
110
|
|
|
101
111
|
`run --json` is the machine mode: non-interactive, one JSON result object
|
package/dist/config.d.ts
CHANGED
|
@@ -17,12 +17,25 @@
|
|
|
17
17
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
18
18
|
* agents: agents # team agent-definition root, relative to the file
|
|
19
19
|
* skills: skills # workspace skill root, relative to the file
|
|
20
|
+
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
21
|
+
* - name: other space
|
|
22
|
+
* url: https://localhost:1985
|
|
23
|
+
* icon: "🛰" # optional emoji shown next to the entry
|
|
20
24
|
*/
|
|
21
25
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
22
26
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
23
27
|
/** Stable, versionless URL of the kraftwerk.yml JSON schema (editor validation). */
|
|
24
28
|
export declare const CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/kraftwerk.schema.json";
|
|
25
29
|
export declare const CONFIG_FILENAMES: string[];
|
|
30
|
+
/** One entry of the workspace switcher: another kraftwerk instance to link to. */
|
|
31
|
+
export interface SwitcherEntry {
|
|
32
|
+
/** Display name of the other workspace. */
|
|
33
|
+
name: string;
|
|
34
|
+
/** URL the other workspace's inspector runs on, e.g. https://localhost:1985 */
|
|
35
|
+
url: string;
|
|
36
|
+
/** Optional emoji shown next to the entry. */
|
|
37
|
+
icon?: string;
|
|
38
|
+
}
|
|
26
39
|
export interface ProjectConfig {
|
|
27
40
|
/** Display name of the project ("environment"), shown in the inspector header. */
|
|
28
41
|
name?: string;
|
|
@@ -40,6 +53,8 @@ export interface ProjectConfig {
|
|
|
40
53
|
agents?: string;
|
|
41
54
|
/** Workspace skill root relative to the project root. Default: skills */
|
|
42
55
|
skills?: string;
|
|
56
|
+
/** Other kraftwerk workspaces, shown as a switcher dropdown in the inspector header. */
|
|
57
|
+
switcher?: SwitcherEntry[];
|
|
43
58
|
}
|
|
44
59
|
export interface Project {
|
|
45
60
|
/** Absolute project root the CLI operates on. */
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,10 @@ import { parse } from "yaml";
|
|
|
20
20
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
21
21
|
* agents: agents # team agent-definition root, relative to the file
|
|
22
22
|
* skills: skills # workspace skill root, relative to the file
|
|
23
|
+
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
24
|
+
* - name: other space
|
|
25
|
+
* url: https://localhost:1985
|
|
26
|
+
* icon: "🛰" # optional emoji shown next to the entry
|
|
23
27
|
*/
|
|
24
28
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
25
29
|
export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -89,10 +93,10 @@ async function loadConfig(configPath) {
|
|
|
89
93
|
if (raw === null || raw === undefined)
|
|
90
94
|
return {};
|
|
91
95
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
92
|
-
throw new Error(`${path.basename(configPath)}: expected a mapping (name, icon, port, workflows, output, knowledge, agents, skills)`);
|
|
96
|
+
throw new Error(`${path.basename(configPath)}: expected a mapping (name, icon, port, workflows, output, knowledge, agents, skills, switcher)`);
|
|
93
97
|
}
|
|
94
98
|
const config = raw;
|
|
95
|
-
const known = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills"];
|
|
99
|
+
const known = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher"];
|
|
96
100
|
for (const key of Object.keys(config)) {
|
|
97
101
|
if (!known.includes(key)) {
|
|
98
102
|
throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
|
|
@@ -102,9 +106,38 @@ async function loadConfig(configPath) {
|
|
|
102
106
|
throw new Error(`${path.basename(configPath)}: port must be an integer`);
|
|
103
107
|
}
|
|
104
108
|
}
|
|
109
|
+
else if (key === "switcher") {
|
|
110
|
+
validateSwitcher(configPath, config[key]);
|
|
111
|
+
}
|
|
105
112
|
else if (typeof config[key] !== "string") {
|
|
106
113
|
throw new Error(`${path.basename(configPath)}: ${key} must be a string`);
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
return config;
|
|
110
117
|
}
|
|
118
|
+
function validateSwitcher(configPath, value) {
|
|
119
|
+
const file = path.basename(configPath);
|
|
120
|
+
if (!Array.isArray(value)) {
|
|
121
|
+
throw new Error(`${file}: switcher must be a list of { name, url, icon? } entries`);
|
|
122
|
+
}
|
|
123
|
+
value.forEach((entry, i) => {
|
|
124
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
125
|
+
throw new Error(`${file}: switcher[${i}] must be a mapping with name and url`);
|
|
126
|
+
}
|
|
127
|
+
const e = entry;
|
|
128
|
+
for (const key of Object.keys(e)) {
|
|
129
|
+
if (!["name", "url", "icon"].includes(key)) {
|
|
130
|
+
throw new Error(`${file}: switcher[${i}]: unknown key "${key}" (allowed: name, url, icon)`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (typeof e.name !== "string" || !e.name.trim()) {
|
|
134
|
+
throw new Error(`${file}: switcher[${i}]: name must be a non-empty string`);
|
|
135
|
+
}
|
|
136
|
+
if (typeof e.url !== "string" || !/^https?:\/\//.test(e.url)) {
|
|
137
|
+
throw new Error(`${file}: switcher[${i}]: url must be an http(s) URL`);
|
|
138
|
+
}
|
|
139
|
+
if (e.icon !== undefined && typeof e.icon !== "string") {
|
|
140
|
+
throw new Error(`${file}: switcher[${i}]: icon must be a string`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ChatAgentId, ChatMeta, ChatScope, StoredChatEvent } from "./types.js";
|
|
2
|
+
/** Server shutdown: no agent subprocess may outlive the inspector. */
|
|
3
|
+
export declare function disposeAllBackends(): void;
|
|
2
4
|
export declare function createChat(opts: {
|
|
3
5
|
agent: ChatAgentId;
|
|
4
6
|
scope: ChatScope;
|
|
@@ -10,6 +10,32 @@ import { startAcpBackend } from "./acp.js";
|
|
|
10
10
|
import { startPiBackend } from "./pi.js";
|
|
11
11
|
import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
|
|
12
12
|
const states = new Map();
|
|
13
|
+
/** Kill a chat's agent subprocess; the next message spawns a fresh one. */
|
|
14
|
+
function dropBackend(state) {
|
|
15
|
+
state.backend?.dispose();
|
|
16
|
+
state.backend = null;
|
|
17
|
+
}
|
|
18
|
+
// Idle reaper: a finished chat must not keep its agent subprocess alive
|
|
19
|
+
// forever (one ACP adapter + one harness binary per chat adds up fast —
|
|
20
|
+
// hourly routines used to leak a process pair per tick). History is on
|
|
21
|
+
// disk and ensureBackend re-sends scope context, so reaping just costs
|
|
22
|
+
// the next message one respawn. busy=true covers the whole turn including
|
|
23
|
+
// pending permission prompts, so nothing is killed mid-work.
|
|
24
|
+
const IDLE_BACKEND_MS = 15 * 60_000;
|
|
25
|
+
const reaper = setInterval(() => {
|
|
26
|
+
const cutoff = Date.now() - IDLE_BACKEND_MS;
|
|
27
|
+
for (const state of states.values()) {
|
|
28
|
+
if (state.backend && !state.busy && Date.parse(state.meta.updatedAt) < cutoff) {
|
|
29
|
+
dropBackend(state);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, 60_000);
|
|
33
|
+
reaper.unref?.();
|
|
34
|
+
/** Server shutdown: no agent subprocess may outlive the inspector. */
|
|
35
|
+
export function disposeAllBackends() {
|
|
36
|
+
for (const state of states.values())
|
|
37
|
+
dropBackend(state);
|
|
38
|
+
}
|
|
13
39
|
async function loadState(id) {
|
|
14
40
|
const existing = states.get(id);
|
|
15
41
|
if (existing)
|
|
@@ -387,13 +413,17 @@ export async function postMessage(id, text) {
|
|
|
387
413
|
const promptText = context ? `<context>\n${context}\n</context>\n\n${body}` : body;
|
|
388
414
|
const stopReason = await backend.prompt(promptText);
|
|
389
415
|
emit(state, { type: "turn_end", stopReason });
|
|
416
|
+
// Routine sessions are one-shot and unattended: release the agent
|
|
417
|
+
// process as soon as the turn ends instead of waiting for the reaper.
|
|
418
|
+
const scope = state.meta.scope;
|
|
419
|
+
if (scope.kind === "team" && scope.routine)
|
|
420
|
+
dropBackend(state);
|
|
390
421
|
}
|
|
391
422
|
catch (err) {
|
|
392
423
|
emit(state, { type: "error", message: err.message });
|
|
393
424
|
// A failed turn may mean a dead subprocess; drop it so the next
|
|
394
425
|
// message spawns a fresh agent (thread history stays on disk).
|
|
395
|
-
state
|
|
396
|
-
state.backend = null;
|
|
426
|
+
dropBackend(state);
|
|
397
427
|
}
|
|
398
428
|
finally {
|
|
399
429
|
state.busy = false;
|
|
@@ -419,7 +449,7 @@ export async function deleteChat(id) {
|
|
|
419
449
|
return { error: "not found" };
|
|
420
450
|
for (const resolve of state.pendingPermissions.values())
|
|
421
451
|
resolve(null);
|
|
422
|
-
state
|
|
452
|
+
dropBackend(state);
|
|
423
453
|
states.delete(id);
|
|
424
454
|
await fs.rm(safeChatDir(id), { recursive: true, force: true });
|
|
425
455
|
return {};
|
package/dist/inspector/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveProject } from "../config.js";
|
|
|
6
6
|
import { listRuns, getRun, readRunFile } from "./runs.js";
|
|
7
7
|
import { listWorkflows, getWorkflow } from "./workflows.js";
|
|
8
8
|
import { dockerStatus, triggerRun, stopRun } from "./runner.js";
|
|
9
|
-
import { cancelChat, createChat, deleteChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
9
|
+
import { cancelChat, createChat, deleteChat, disposeAllBackends, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
10
10
|
import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
|
|
11
11
|
import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
|
|
12
12
|
import { getSkill, listSkills, skillsRoot } from "./skills.js";
|
|
@@ -76,7 +76,12 @@ async function handleApi(req, res, url) {
|
|
|
76
76
|
if (seg.length === 2 && seg[1] === "meta" && method === "GET") {
|
|
77
77
|
const project = await resolveProject(getProjectRoot()).catch(() => null);
|
|
78
78
|
const projectName = project?.config.name ?? (project ? path.basename(project.root) : "");
|
|
79
|
-
return json(res, {
|
|
79
|
+
return json(res, {
|
|
80
|
+
version: await getPkgVersion(),
|
|
81
|
+
projectName,
|
|
82
|
+
projectIcon: project?.config.icon ?? "",
|
|
83
|
+
switcher: project?.config.switcher ?? [],
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
// GET /api/runs
|
|
82
87
|
if (seg.length === 2 && seg[1] === "runs" && method === "GET") {
|
|
@@ -483,6 +488,15 @@ export function startInspector(opts) {
|
|
|
483
488
|
if (opts.projectRoot)
|
|
484
489
|
setProjectRoot(opts.projectRoot);
|
|
485
490
|
startRoutineScheduler();
|
|
491
|
+
// Chat agent subprocesses must die with the server — signals bypass
|
|
492
|
+
// "exit" handlers, so hook the signals themselves.
|
|
493
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
494
|
+
process.once(sig, () => {
|
|
495
|
+
disposeAllBackends();
|
|
496
|
+
process.exit(sig === "SIGINT" ? 130 : 143);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
process.once("exit", disposeAllBackends);
|
|
486
500
|
const server = http.createServer(async (req, res) => {
|
|
487
501
|
try {
|
|
488
502
|
const url = new URL(req.url ?? "/", "http://localhost");
|