@netnodeag/kraftwerk 0.35.0 → 0.36.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 +26 -19
- package/dist/cli/init.js +3 -3
- package/dist/cli/routines.js +9 -9
- package/dist/config.d.ts +2 -2
- package/dist/config.js +1 -1
- package/dist/inspector/{team.d.ts → agents.d.ts} +21 -12
- package/dist/inspector/{team.js → agents.js} +34 -19
- package/dist/inspector/chat/backend.d.ts +1 -1
- package/dist/inspector/chat/sessions.js +43 -43
- package/dist/inspector/chat/store.js +13 -1
- package/dist/inspector/chat/types.d.ts +2 -2
- package/dist/inspector/instances.d.ts +12 -0
- package/dist/inspector/instances.js +28 -0
- package/dist/inspector/routines.d.ts +4 -4
- package/dist/inspector/routines.js +11 -11
- package/dist/inspector/search.d.ts +25 -0
- package/dist/inspector/search.js +31 -0
- package/dist/inspector/server.js +34 -29
- package/dist/inspector/skills.d.ts +6 -6
- package/dist/inspector/skills.js +8 -8
- package/inspector/dist/assets/{editor-BABv2LmA.js → editor-7ImnDC3D.js} +3 -3
- package/inspector/dist/assets/{index-DbWUNbt-.js → index--2I7vpcS.js} +1 -1
- package/inspector/dist/assets/index-B2ThRS2S.js +217 -0
- package/inspector/dist/assets/{index-BXspF2KR.css → index-BA7nx4V5.css} +1 -1
- package/inspector/dist/assets/{index-CdwTmXTV.js → index-BC7aIJ-h.js} +1 -1
- package/inspector/dist/assets/{index-DuPp3MUc.js → index-BIXYBiw7.js} +1 -1
- package/inspector/dist/assets/{index-Bmm4zJNU.js → index-BZeYO5wT.js} +1 -1
- package/inspector/dist/assets/{index-sLxulPOm.js → index-B_CIhKa4.js} +1 -1
- package/inspector/dist/assets/{index-DCR7u4Xa.js → index-BuRajvjQ.js} +1 -1
- package/inspector/dist/assets/{index-Ca9Qs5LZ.js → index-C3Ob1O7W.js} +1 -1
- package/inspector/dist/assets/{index-HBdsdJhD.js → index-CIjv7yxE.js} +1 -1
- package/inspector/dist/assets/{index-wkYwz0Tl.js → index-CRAwmMfk.js} +1 -1
- package/inspector/dist/assets/{index-ByWpqzAI.js → index-Ct_jZSyD.js} +1 -1
- package/inspector/dist/assets/{index-hkVov8T4.js → index-DKrBjjGQ.js} +1 -1
- package/inspector/dist/assets/{index-Gx93brFq.js → index-DL_bJiOj.js} +1 -1
- package/inspector/dist/assets/{index-BbTer62d.js → index-DPBAv_kj.js} +1 -1
- package/inspector/dist/assets/{index-CCACy6uF.js → index-DbmSSHvr.js} +1 -1
- package/inspector/dist/assets/{index-DuwmzZAr.js → index-DjRBLY9N.js} +1 -1
- package/inspector/dist/assets/{index-CzPQygR7.js → index-YZtBJhWD.js} +1 -1
- package/inspector/dist/assets/{index-CjlJeW9z.js → index-fWUFhIGR.js} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/index-uEig4--d.js +0 -217
package/README.md
CHANGED
|
@@ -179,7 +179,7 @@ different host.
|
|
|
179
179
|
Optional, at the project root, and also the root marker for the walk-up. All
|
|
180
180
|
fields are optional. `workflows:` sets the workflows root, `output:` the
|
|
181
181
|
run-artifact directory (default `output/`), `knowledge:` the OKF bundle root
|
|
182
|
-
(default `knowledge/`), and `agents:` the
|
|
182
|
+
(default `knowledge/`), and `agents:` the agent-definition root (default
|
|
183
183
|
`agents/`).
|
|
184
184
|
|
|
185
185
|
`switcher:` links other kraftwerk workspaces from the inspector header. The
|
|
@@ -192,6 +192,13 @@ switcher:
|
|
|
192
192
|
icon: "🛰" # optional
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
**⌘K** (Ctrl K) anywhere in the inspector opens the agent palette: type a
|
|
196
|
+
few letters of an agent's name, description or workspace and hit enter to
|
|
197
|
+
jump to it. It lists the active agents of every workspace on this machine,
|
|
198
|
+
running or not (a stopped one is started on the way), read from the
|
|
199
|
+
project registry under `~/.kraftwerk/projects`, which every inspector keeps
|
|
200
|
+
current with its roster.
|
|
201
|
+
|
|
195
202
|
### Triggering from CI, cron, or webhooks
|
|
196
203
|
|
|
197
204
|
`run --json` is the machine mode. It runs non-interactively, prints one JSON
|
|
@@ -249,15 +256,15 @@ repo. Agent logins made inside the container persist in the `agent-home`
|
|
|
249
256
|
volume. This is a different image from the `kraftwerk-runner` sandbox
|
|
250
257
|
(`runner/Dockerfile`) used by `run --sandbox`.
|
|
251
258
|
|
|
252
|
-
## Persistent
|
|
259
|
+
## Persistent agents
|
|
253
260
|
|
|
254
|
-
The inspector's "
|
|
255
|
-
|
|
261
|
+
The inspector's "agents" screen turns chat agents into persistent teammates. An
|
|
262
|
+
agent is one folder under the project's `agents/` root:
|
|
256
263
|
|
|
257
264
|
```
|
|
258
265
|
agents/max/
|
|
259
266
|
agent.yml # name, emoji, description, harness, model, effort, workflows
|
|
260
|
-
system.md # the
|
|
267
|
+
system.md # the agent's system prompt (its role)
|
|
261
268
|
```
|
|
262
269
|
|
|
263
270
|
```yaml
|
|
@@ -269,43 +276,43 @@ harness: claude # claude | codex | pi (which chat backend runs it)
|
|
|
269
276
|
model: sonnet # optional; harness default when omitted
|
|
270
277
|
effort: medium # optional: low | medium | high | xhigh | max
|
|
271
278
|
workflows: [tagline, website-check]
|
|
272
|
-
knowledge: [customer-support] # OKF bundles the
|
|
279
|
+
knowledge: [customer-support] # OKF bundles the agent consults & maintains
|
|
273
280
|
skills: [report-html] # optional allowlist; omit = all skills, [] = none
|
|
274
281
|
```
|
|
275
282
|
|
|
276
|
-
Sessions with
|
|
277
|
-
listed per
|
|
283
|
+
Sessions with an agent are ordinary chats scoped `{ kind: "agent", slug }`,
|
|
284
|
+
listed per agent in a second sidebar. On the first message the agent gets
|
|
278
285
|
its role plus its connected workflows and knowledge bundles injected as
|
|
279
286
|
context. That context includes how to run workflows
|
|
280
287
|
(`KRAFTWERK_YES=1 npx kraftwerk run <workflow> "<request>"`) and how to read
|
|
281
288
|
and write knowledge through `kraftwerk knowledge`, with writes stamped with
|
|
282
|
-
the
|
|
289
|
+
the agent's own actor, `<slug>/<harness>`. So the agent triggers its own
|
|
283
290
|
workflows when a request matches, and keeps its bundles current.
|
|
284
291
|
|
|
285
292
|
Model and effort ride on backend-specific channels. The claude adapter takes
|
|
286
293
|
the model via ACP session options and the thinking budget via
|
|
287
294
|
`MAX_THINKING_TOKENS`. Codex gets a `CODEX_CONFIG` env override (`model`,
|
|
288
|
-
`model_reasoning_effort`). Pi gets `--model` and `--thinking` flags.
|
|
295
|
+
`model_reasoning_effort`). Pi gets `--model` and `--thinking` flags. Agents
|
|
289
296
|
are created and edited in the UI, or by editing the files, since the
|
|
290
297
|
definition is read fresh for each new session.
|
|
291
298
|
|
|
292
299
|
### Skills in chat
|
|
293
300
|
|
|
294
|
-
Chats, both general ones and
|
|
301
|
+
Chats, both general ones and agent sessions, can use skills. These are
|
|
295
302
|
Claude-style instruction packages, one folder per skill with a `SKILL.md`
|
|
296
303
|
holding YAML frontmatter (`name`, `description`) and then the instructions.
|
|
297
304
|
Four roots are discovered, each shadowing same-named skills in the roots below
|
|
298
305
|
it:
|
|
299
306
|
|
|
300
307
|
```
|
|
301
|
-
<project>/agents/<slug>/skills/<name>/SKILL.md # private to that one
|
|
308
|
+
<project>/agents/<slug>/skills/<name>/SKILL.md # private to that one agent
|
|
302
309
|
<project>/<skills root>/<name>/SKILL.md # workspace, git-tracked (kraftwerk.yml `skills`, default skills/)
|
|
303
310
|
<project>/.claude/skills/<name>/SKILL.md # git-tracked, per project
|
|
304
311
|
~/.claude/skills/<name>/SKILL.md # personal, per user
|
|
305
312
|
```
|
|
306
313
|
|
|
307
314
|
Agent skills are visible only to that agent's sessions and always apply. A
|
|
308
|
-
|
|
315
|
+
agent's `skills:` allowlist narrows the shared roots only. Manage them in the
|
|
309
316
|
agent profile under "own skills", or by editing the files.
|
|
310
317
|
|
|
311
318
|
Every chat lists its visible skills as context under "## Your skills", so the
|
|
@@ -313,16 +320,16 @@ agent reaches for one when the request matches. Typing `/` in the composer
|
|
|
313
320
|
opens an autocomplete over them. Sending `/<name> <args>` expands the skill's
|
|
314
321
|
SKILL.md into the prompt, which is what makes this work identically on claude,
|
|
315
322
|
codex, and pi. On top of that, claude discovers `.claude/skills` natively (a
|
|
316
|
-
|
|
317
|
-
loads each visible skill folder via `--skill`.
|
|
323
|
+
agent's `skills:` allowlist narrows that via ACP session options) and pi
|
|
324
|
+
loads each visible skill folder via `--skill`. Agents take an optional
|
|
318
325
|
`skills:` list in `agent.yml`. Omitted means all discovered skills, an empty
|
|
319
326
|
list means none, otherwise it is the allowlist. `GET /api/skills` returns
|
|
320
327
|
what's discovered.
|
|
321
328
|
|
|
322
329
|
### Routines, or scheduled prompts
|
|
323
330
|
|
|
324
|
-
|
|
325
|
-
orders for an employee. Definitions live next to the
|
|
331
|
+
An agent can have routines, cron-scheduled prompts that work like standing
|
|
332
|
+
orders for an employee. Definitions live next to the agent in
|
|
326
333
|
`agents/<slug>/routines.yml` and are git-tracked. Run state (last run, last
|
|
327
334
|
session, errors) lives in `<output>/routines-state.json`.
|
|
328
335
|
|
|
@@ -338,12 +345,12 @@ session, errors) lives in `<output>/routines-state.json`.
|
|
|
338
345
|
```
|
|
339
346
|
|
|
340
347
|
The inspector server runs the scheduler in-process, so there is no external
|
|
341
|
-
cron to set up. Every due routine opens a fresh session for the
|
|
348
|
+
cron to set up. Every due routine opens a fresh session for the agent, posts
|
|
342
349
|
the prompt, and shows up in the sessions sidebar titled "⏰ <name>". Routine
|
|
343
350
|
sessions run unattended, so tool-permission requests are auto-approved, with
|
|
344
351
|
the request and approval pair left in the thread as an audit trail. Give
|
|
345
352
|
routine prompts the same trust you would give a `KRAFTWERK_YES=1` workflow
|
|
346
|
-
run. Manage routines on the
|
|
353
|
+
run. Manage routines on the agent page, where you can create, edit and
|
|
347
354
|
delete them, toggle enabled, hit "run now", and jump to the last run's
|
|
348
355
|
session. Schedules missed while the server is down are skipped, not replayed.
|
|
349
356
|
|
package/dist/cli/init.js
CHANGED
|
@@ -6,7 +6,7 @@ import { initBundle, writeConcept } from "../okf.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* `kraftwerk init` — make any repository a kraftwerk consumer in one
|
|
8
8
|
* command: kraftwerk.yml (root marker + config), a kraftwerk-data/ tree with
|
|
9
|
-
* a runnable example workflow, a starter
|
|
9
|
+
* a runnable example workflow, a starter agent ("max"), a demo
|
|
10
10
|
* knowledge bundle, and an output ignore entry. Everything is idempotent:
|
|
11
11
|
* existing files are left untouched and reported.
|
|
12
12
|
*/
|
|
@@ -20,7 +20,7 @@ port: 1981 # port \`kraftwerk ui\` listens on
|
|
|
20
20
|
workflows: ${DATA_DIR}/workflows # where workflows live
|
|
21
21
|
output: ${DATA_DIR}/output # where run artifacts land (git-ignored)
|
|
22
22
|
knowledge: ${DATA_DIR}/knowledge # OKF knowledge bundles
|
|
23
|
-
agents: ${DATA_DIR}/agents #
|
|
23
|
+
agents: ${DATA_DIR}/agents # agent definitions
|
|
24
24
|
skills: ${DATA_DIR}/skills # workspace skills (shared instruction packages)
|
|
25
25
|
`;
|
|
26
26
|
const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
|
|
@@ -107,7 +107,7 @@ export async function runInit(cwd) {
|
|
|
107
107
|
await put(`${DATA_DIR}/workflows/hello/workflow.yml`, WORKFLOW_TEMPLATE);
|
|
108
108
|
await put(`${DATA_DIR}/workflows/hello/prompts/assistant.md`, PERSONA_TEMPLATE);
|
|
109
109
|
await put(`${DATA_DIR}/workflows/hello/prompts/answer.md`, PROMPT_TEMPLATE);
|
|
110
|
-
//
|
|
110
|
+
// Agent "max".
|
|
111
111
|
await put(`${DATA_DIR}/agents/max/agent.yml`, AGENT_YML_TEMPLATE);
|
|
112
112
|
await put(`${DATA_DIR}/agents/max/system.md`, AGENT_SYSTEM_TEMPLATE);
|
|
113
113
|
// Example workspace skill.
|
package/dist/cli/routines.js
CHANGED
|
@@ -4,22 +4,22 @@ import chalk from "chalk";
|
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
5
|
import { setOutputDir, setProjectRoot } from "../inspector/context.js";
|
|
6
6
|
import { deleteRoutine, listRoutines, routineStatuses, saveRoutine, } from "../inspector/routines.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getAgent, listAgents } from "../inspector/agents.js";
|
|
8
8
|
/**
|
|
9
9
|
* `kraftwerk routines` — CLI surface over per-agent scheduled prompts
|
|
10
10
|
* (agents/<slug>/routines.yml). Definition CRUD works directly on the
|
|
11
11
|
* files; `run` fires through the running inspector server, because the
|
|
12
12
|
* session (chat) and the scheduler live there.
|
|
13
13
|
*/
|
|
14
|
-
/** Point the inspector context at this project so
|
|
14
|
+
/** Point the inspector context at this project so agent/routine paths resolve. */
|
|
15
15
|
async function initContext() {
|
|
16
16
|
const project = await resolveProject(process.cwd());
|
|
17
17
|
setProjectRoot(project.root);
|
|
18
18
|
setOutputDir(project.outputDir);
|
|
19
19
|
return { port: project.config.port ?? 1981 };
|
|
20
20
|
}
|
|
21
|
-
async function
|
|
22
|
-
if (!(await
|
|
21
|
+
async function requireAgent(slug) {
|
|
22
|
+
if (!(await getAgent(slug))) {
|
|
23
23
|
console.error(chalk.red(`Agent "${slug}" not found (expected agents/${slug}/agent.yml).`));
|
|
24
24
|
process.exit(2);
|
|
25
25
|
}
|
|
@@ -56,9 +56,9 @@ export function registerRoutineCommands(program) {
|
|
|
56
56
|
.option("--json", "Machine-readable output")
|
|
57
57
|
.action(async (agent, opts) => {
|
|
58
58
|
await initContext();
|
|
59
|
-
const slugs = agent ? [agent] : (await
|
|
59
|
+
const slugs = agent ? [agent] : (await listAgents()).map((m) => m.slug);
|
|
60
60
|
if (agent)
|
|
61
|
-
await
|
|
61
|
+
await requireAgent(agent);
|
|
62
62
|
const all = {};
|
|
63
63
|
for (const slug of slugs)
|
|
64
64
|
all[slug] = await routineStatuses(slug);
|
|
@@ -89,7 +89,7 @@ export function registerRoutineCommands(program) {
|
|
|
89
89
|
.option("--disabled", "Create it switched off")
|
|
90
90
|
.action(async (agent, opts) => {
|
|
91
91
|
await initContext();
|
|
92
|
-
await
|
|
92
|
+
await requireAgent(agent);
|
|
93
93
|
const prompt = opts.prompt ??
|
|
94
94
|
(opts.file ? await readFile(path.resolve(opts.file), "utf8") : process.stdin.isTTY ? "" : await readStdin());
|
|
95
95
|
if (!prompt.trim()) {
|
|
@@ -124,7 +124,7 @@ export function registerRoutineCommands(program) {
|
|
|
124
124
|
.argument("<id>", "Routine id (see `kraftwerk routines <agent>`)")
|
|
125
125
|
.action(async (agent, id) => {
|
|
126
126
|
await initContext();
|
|
127
|
-
await
|
|
127
|
+
await requireAgent(agent);
|
|
128
128
|
if (!(await listRoutines(agent)).some((r) => r.id === id)) {
|
|
129
129
|
console.error(chalk.red(`Routine "${agent}/${id}" not found.`));
|
|
130
130
|
process.exit(2);
|
|
@@ -158,7 +158,7 @@ export function registerRoutineCommands(program) {
|
|
|
158
158
|
.action(async (agent, id, opts) => {
|
|
159
159
|
const { port } = await initContext();
|
|
160
160
|
const p = opts.port ? Number(opts.port) : port;
|
|
161
|
-
const url = `http://localhost:${p}/api/
|
|
161
|
+
const url = `http://localhost:${p}/api/agents/${encodeURIComponent(agent)}/routines/${encodeURIComponent(id)}/run`;
|
|
162
162
|
let body;
|
|
163
163
|
try {
|
|
164
164
|
body = (await (await fetch(url, { method: "POST" })).json());
|
package/dist/config.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* workflows: src/workflows # workflows root, relative to the file
|
|
16
16
|
* output: output # run-artifact directory, relative to the file
|
|
17
17
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
18
|
-
* agents: agents #
|
|
18
|
+
* agents: agents # agent-definition root, relative to the file
|
|
19
19
|
* skills: skills # workspace skill root, relative to the file
|
|
20
20
|
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
21
21
|
* - name: other space
|
|
@@ -71,7 +71,7 @@ export interface ProjectConfig {
|
|
|
71
71
|
output?: string;
|
|
72
72
|
/** OKF knowledge-bundle root relative to the project root. Default: knowledge */
|
|
73
73
|
knowledge?: string;
|
|
74
|
-
/**
|
|
74
|
+
/** Agent-definition root relative to the project root. Default: agents */
|
|
75
75
|
agents?: string;
|
|
76
76
|
/** Workspace skill root relative to the project root. Default: skills */
|
|
77
77
|
skills?: string;
|
package/dist/config.js
CHANGED
|
@@ -18,7 +18,7 @@ import { parse } from "yaml";
|
|
|
18
18
|
* workflows: src/workflows # workflows root, relative to the file
|
|
19
19
|
* output: output # run-artifact directory, relative to the file
|
|
20
20
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
21
|
-
* agents: agents #
|
|
21
|
+
* agents: agents # agent-definition root, relative to the file
|
|
22
22
|
* skills: skills # workspace skill root, relative to the file
|
|
23
23
|
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
24
24
|
* - name: other space
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { ChatAgentId } from "./chat/types.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Agents: persistent agents, defined on the filesystem. Each one
|
|
4
4
|
* is one folder under the project's agents/ root:
|
|
5
5
|
*
|
|
6
6
|
* agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
|
|
7
|
-
* agents/<slug>/system.md # the
|
|
7
|
+
* agents/<slug>/system.md # the agent's system prompt / role description
|
|
8
8
|
*
|
|
9
9
|
* Definitions are git-tracked project config (like workflows/), not run
|
|
10
10
|
* state — sessions with a member are ordinary chats scoped to it.
|
|
11
11
|
*/
|
|
12
12
|
export declare const HARNESSES: ChatAgentId[];
|
|
13
13
|
export declare const EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
14
|
-
export interface
|
|
14
|
+
export interface Agent {
|
|
15
15
|
slug: string;
|
|
16
16
|
name: string;
|
|
17
17
|
emoji: string;
|
|
@@ -33,17 +33,26 @@ export interface TeamMember {
|
|
|
33
33
|
/** Archived: hidden from the active roster, restorable any time. */
|
|
34
34
|
archived?: boolean;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** What the ⌘K palette needs to find and show an agent — kept in the project registry. */
|
|
37
|
+
export interface AgentSummary {
|
|
38
|
+
slug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
emoji: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
group?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare const toSummary: (a: Agent) => AgentSummary;
|
|
45
|
+
export interface AgentDetail extends Agent {
|
|
37
46
|
/** Contents of system.md — the member's system prompt. */
|
|
38
47
|
system: string;
|
|
39
48
|
}
|
|
40
|
-
export declare function
|
|
41
|
-
export declare function
|
|
49
|
+
export declare function agentsRoot(): Promise<string>;
|
|
50
|
+
export declare function safeAgentSlug(slug: string): string;
|
|
42
51
|
/** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
|
|
43
52
|
export declare function slugFromName(name: string): string;
|
|
44
|
-
export declare function
|
|
45
|
-
export declare function
|
|
46
|
-
export interface
|
|
53
|
+
export declare function listAgents(): Promise<Agent[]>;
|
|
54
|
+
export declare function getAgent(slug: string): Promise<AgentDetail | null>;
|
|
55
|
+
export interface SaveAgentInput {
|
|
47
56
|
slug?: string;
|
|
48
57
|
name: string;
|
|
49
58
|
emoji?: string;
|
|
@@ -59,7 +68,7 @@ export interface SaveMemberInput {
|
|
|
59
68
|
skills?: string[];
|
|
60
69
|
system?: string;
|
|
61
70
|
}
|
|
62
|
-
export declare function
|
|
71
|
+
export declare function saveAgent(input: SaveAgentInput): Promise<AgentDetail>;
|
|
63
72
|
/** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
|
|
64
|
-
export declare function
|
|
65
|
-
export declare function
|
|
73
|
+
export declare function setAgentArchived(slug: string, archived: boolean): Promise<AgentDetail>;
|
|
74
|
+
export declare function deleteAgent(slug: string): Promise<void>;
|
|
@@ -3,24 +3,32 @@ import path from "node:path";
|
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
5
|
import { getProjectRoot } from "./context.js";
|
|
6
|
+
import { syncProjectAgents } from "./instances.js";
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Agents: persistent agents, defined on the filesystem. Each one
|
|
8
9
|
* is one folder under the project's agents/ root:
|
|
9
10
|
*
|
|
10
11
|
* agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
|
|
11
|
-
* agents/<slug>/system.md # the
|
|
12
|
+
* agents/<slug>/system.md # the agent's system prompt / role description
|
|
12
13
|
*
|
|
13
14
|
* Definitions are git-tracked project config (like workflows/), not run
|
|
14
15
|
* state — sessions with a member are ordinary chats scoped to it.
|
|
15
16
|
*/
|
|
16
17
|
export const HARNESSES = ["claude", "codex", "pi"];
|
|
17
18
|
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
18
|
-
export
|
|
19
|
+
export const toSummary = (a) => ({
|
|
20
|
+
slug: a.slug,
|
|
21
|
+
name: a.name,
|
|
22
|
+
emoji: a.emoji,
|
|
23
|
+
...(a.description ? { description: a.description } : {}),
|
|
24
|
+
...(a.group ? { group: a.group } : {}),
|
|
25
|
+
});
|
|
26
|
+
export async function agentsRoot() {
|
|
19
27
|
const project = await resolveProject(getProjectRoot());
|
|
20
28
|
return path.resolve(project.root, project.config.agents ?? "agents");
|
|
21
29
|
}
|
|
22
30
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
|
|
23
|
-
export function
|
|
31
|
+
export function safeAgentSlug(slug) {
|
|
24
32
|
if (!SLUG_RE.test(slug))
|
|
25
33
|
throw new Error(`invalid agent slug "${slug}"`);
|
|
26
34
|
return slug;
|
|
@@ -55,8 +63,8 @@ function normalize(slug, raw) {
|
|
|
55
63
|
...(raw.archived === true ? { archived: true } : {}),
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
|
-
export async function
|
|
59
|
-
const root = await
|
|
66
|
+
export async function listAgents() {
|
|
67
|
+
const root = await agentsRoot();
|
|
60
68
|
let entries;
|
|
61
69
|
try {
|
|
62
70
|
entries = (await fs.readdir(root, { withFileTypes: true }))
|
|
@@ -66,7 +74,7 @@ export async function listMembers() {
|
|
|
66
74
|
catch {
|
|
67
75
|
return [];
|
|
68
76
|
}
|
|
69
|
-
const
|
|
77
|
+
const agents = await Promise.all(entries.map(async (slug) => {
|
|
70
78
|
try {
|
|
71
79
|
const raw = parse(await fs.readFile(path.join(root, slug, "agent.yml"), "utf8"));
|
|
72
80
|
return normalize(slug, (raw ?? {}));
|
|
@@ -75,10 +83,17 @@ export async function listMembers() {
|
|
|
75
83
|
return null;
|
|
76
84
|
}
|
|
77
85
|
}));
|
|
78
|
-
|
|
86
|
+
const list = agents.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
87
|
+
// Keep the registry's copy of the roster current for the other instances'
|
|
88
|
+
// palettes. Awaited so a read that follows a save sees the record updated;
|
|
89
|
+
// it only writes when the roster changed.
|
|
90
|
+
await resolveProject(getProjectRoot())
|
|
91
|
+
.then((p) => syncProjectAgents(p.root, list.filter((a) => !a.archived).map(toSummary)))
|
|
92
|
+
.catch(() => { });
|
|
93
|
+
return list;
|
|
79
94
|
}
|
|
80
|
-
export async function
|
|
81
|
-
const dir = path.join(await
|
|
95
|
+
export async function getAgent(slug) {
|
|
96
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
82
97
|
let raw;
|
|
83
98
|
try {
|
|
84
99
|
raw = parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8"));
|
|
@@ -89,7 +104,7 @@ export async function getMember(slug) {
|
|
|
89
104
|
const system = await fs.readFile(path.join(dir, "system.md"), "utf8").catch(() => "");
|
|
90
105
|
return { ...normalize(slug, (raw ?? {})), system: system.trim() };
|
|
91
106
|
}
|
|
92
|
-
export async function
|
|
107
|
+
export async function saveAgent(input) {
|
|
93
108
|
const name = input.name?.trim();
|
|
94
109
|
if (!name)
|
|
95
110
|
throw new Error("name is required");
|
|
@@ -99,8 +114,8 @@ export async function saveMember(input) {
|
|
|
99
114
|
if (input.effort && !EFFORTS.includes(input.effort)) {
|
|
100
115
|
throw new Error(`effort must be one of: ${EFFORTS.join(", ")}`);
|
|
101
116
|
}
|
|
102
|
-
const slug = input.slug ?
|
|
103
|
-
const dir = path.join(await
|
|
117
|
+
const slug = input.slug ? safeAgentSlug(input.slug) : slugFromName(name);
|
|
118
|
+
const dir = path.join(await agentsRoot(), slug);
|
|
104
119
|
await fs.mkdir(dir, { recursive: true });
|
|
105
120
|
// Profile edits must not silently unarchive: carry the flag over.
|
|
106
121
|
const existing = await fs
|
|
@@ -122,11 +137,11 @@ export async function saveMember(input) {
|
|
|
122
137
|
};
|
|
123
138
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
124
139
|
await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
|
|
125
|
-
return (await
|
|
140
|
+
return (await getAgent(slug));
|
|
126
141
|
}
|
|
127
142
|
/** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
|
|
128
|
-
export async function
|
|
129
|
-
const dir = path.join(await
|
|
143
|
+
export async function setAgentArchived(slug, archived) {
|
|
144
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
130
145
|
let raw;
|
|
131
146
|
try {
|
|
132
147
|
raw = (parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8")) ?? {});
|
|
@@ -140,9 +155,9 @@ export async function setMemberArchived(slug, archived) {
|
|
|
140
155
|
else
|
|
141
156
|
delete yml.archived;
|
|
142
157
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
143
|
-
return (await
|
|
158
|
+
return (await getAgent(slug));
|
|
144
159
|
}
|
|
145
|
-
export async function
|
|
146
|
-
const dir = path.join(await
|
|
160
|
+
export async function deleteAgent(slug) {
|
|
161
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
147
162
|
await fs.rm(dir, { recursive: true, force: true });
|
|
148
163
|
}
|
|
@@ -19,7 +19,7 @@ export interface BackendHooks {
|
|
|
19
19
|
}>): Promise<string | null>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Optional per-chat model/effort overrides (from
|
|
22
|
+
* Optional per-chat model/effort overrides (from an agent definition).
|
|
23
23
|
* Each backend maps them onto whatever its agent actually supports.
|
|
24
24
|
*/
|
|
25
25
|
export interface BackendTuning {
|
|
@@ -5,7 +5,7 @@ import { knowledgeIndex } from "../knowledge.js";
|
|
|
5
5
|
import { getRun, listRuns, safeRunDir } from "../runs.js";
|
|
6
6
|
import { listSkills, readSkill } from "../skills.js";
|
|
7
7
|
import { listWorkflows } from "../workflows.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getAgent, listAgents } from "../agents.js";
|
|
9
9
|
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";
|
|
@@ -74,19 +74,19 @@ function emit(state, ev) {
|
|
|
74
74
|
/* ---------- skills ---------- */
|
|
75
75
|
/**
|
|
76
76
|
* Skills visible to a chat: every discovered skill (workspace skills root
|
|
77
|
-
* + .claude/skills roots, plus the
|
|
78
|
-
* sessions), narrowed by the
|
|
77
|
+
* + .claude/skills roots, plus the agent's own agents/<slug>/skills for agent
|
|
78
|
+
* sessions), narrowed by the agent's allowlist when the chat is an agent
|
|
79
79
|
* session (absent allowlist = all, empty = none). The agent's own skills
|
|
80
80
|
* always apply — the allowlist narrows shared skills only.
|
|
81
81
|
*/
|
|
82
82
|
async function availableSkills(scope) {
|
|
83
|
-
const all = await listSkills(scope.kind === "
|
|
84
|
-
if (scope.kind !== "
|
|
83
|
+
const all = await listSkills(scope.kind === "agent" ? scope.slug : undefined).catch(() => []);
|
|
84
|
+
if (scope.kind !== "agent")
|
|
85
85
|
return all;
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
86
|
+
const def = await getAgent(scope.slug).catch(() => null);
|
|
87
|
+
if (!def || def.skills === undefined)
|
|
88
88
|
return all;
|
|
89
|
-
const allowed = new Set(
|
|
89
|
+
const allowed = new Set(def.skills.map((n) => n.toLowerCase()));
|
|
90
90
|
return all.filter((s) => s.source === "agent" || allowed.has(s.name.toLowerCase()));
|
|
91
91
|
}
|
|
92
92
|
/** "## Your skills" context block (empty when no skills are visible). */
|
|
@@ -122,12 +122,12 @@ async function expandSkillInvocation(scope, text) {
|
|
|
122
122
|
`Arguments the user passed to the skill: ${args || "(none)"}`);
|
|
123
123
|
}
|
|
124
124
|
/* ---------- scope context ---------- */
|
|
125
|
-
/** "## Your knowledge" block for a
|
|
126
|
-
async function
|
|
127
|
-
if (
|
|
125
|
+
/** "## Your knowledge" block for a agent's connected OKF bundles. */
|
|
126
|
+
async function agentKnowledgeContext(def) {
|
|
127
|
+
if (def.knowledge.length === 0)
|
|
128
128
|
return "";
|
|
129
129
|
const { bundles } = await knowledgeIndex().catch(() => ({ bundles: [] }));
|
|
130
|
-
const lines =
|
|
130
|
+
const lines = def.knowledge
|
|
131
131
|
.map((name) => {
|
|
132
132
|
const b = bundles.find((x) => x.name === name);
|
|
133
133
|
return b
|
|
@@ -139,30 +139,30 @@ async function teamKnowledgeContext(member) {
|
|
|
139
139
|
`part of your job. Consult them before answering questions in their domain, and keep them current ` +
|
|
140
140
|
`when you learn something durable:\n${lines}\n\n` +
|
|
141
141
|
`Read with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
142
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${
|
|
142
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${def.slug}/${def.harness}\` ` +
|
|
143
143
|
`(write the markdown to a temp file first; the CLI stamps provenance and maintains index.md/log.md). ` +
|
|
144
144
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
145
145
|
`click in the UI.\n\n`);
|
|
146
146
|
}
|
|
147
147
|
/** Base context per scope; scopeContext() appends the shared skills block. */
|
|
148
148
|
async function baseScopeContext(scope, agent) {
|
|
149
|
-
if (scope.kind === "
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
return `You were opened as
|
|
149
|
+
if (scope.kind === "agent") {
|
|
150
|
+
const def = await getAgent(scope.slug).catch(() => null);
|
|
151
|
+
if (!def) {
|
|
152
|
+
return `You were opened as agent "${scope.slug}", but its definition under agents/ is missing. Tell the user and ask them to recreate it.`;
|
|
153
153
|
}
|
|
154
154
|
const { workflows } = await listWorkflows().catch(() => ({ workflows: [] }));
|
|
155
|
-
const connected = workflows.filter((w) =>
|
|
156
|
-
const missing =
|
|
155
|
+
const connected = workflows.filter((w) => def.workflows.includes(w.slug));
|
|
156
|
+
const missing = def.workflows.filter((slug) => !connected.some((w) => w.slug === slug));
|
|
157
157
|
const wfLines = connected
|
|
158
158
|
.map((w) => `- ${w.slug}: ${w.description ?? w.name ?? ""} (${w.steps} steps)`)
|
|
159
159
|
.join("\n");
|
|
160
|
-
return (`You are ${
|
|
161
|
-
`(defined in agents/${
|
|
162
|
-
`is a conversation with the same ${
|
|
160
|
+
return (`You are ${def.emoji} ${def.name}, a persistent agent of this project ` +
|
|
161
|
+
`(defined in agents/${def.slug}/). The user works with you like with a colleague: every session ` +
|
|
162
|
+
`is a conversation with the same ${def.name}, so keep this role consistently. You are an AI agent, ` +
|
|
163
163
|
`not a human — never pretend otherwise, but do own your role.\n\n` +
|
|
164
|
-
`## Your role\n${
|
|
165
|
-
(
|
|
164
|
+
`## Your role\n${def.system || "(no system prompt written yet — ask the user what your job should be)"}\n\n` +
|
|
165
|
+
(def.workflows.length > 0
|
|
166
166
|
? `## Your workflows\nThese kraftwerk workflows are part of your job. When the user's request matches one, ` +
|
|
167
167
|
`run it yourself instead of doing the work by hand:\n${wfLines || "(none found)"}\n` +
|
|
168
168
|
(missing.length > 0
|
|
@@ -174,9 +174,9 @@ async function baseScopeContext(scope, agent) {
|
|
|
174
174
|
`run id and summarize the result. Confirm with the user before starting a long or expensive run ` +
|
|
175
175
|
`unless they clearly asked for it.\n\n`
|
|
176
176
|
: "") +
|
|
177
|
-
(await
|
|
177
|
+
(await agentKnowledgeContext(def)) +
|
|
178
178
|
`The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
|
|
179
|
-
`say so and suggest which
|
|
179
|
+
`say so and suggest which agent or tool fits better.` +
|
|
180
180
|
(scope.routine
|
|
181
181
|
? `\n\nThis session was started automatically by your scheduled routine "${scope.routine}" — ` +
|
|
182
182
|
`nobody may be watching live. Complete the task autonomously (tool permissions are ` +
|
|
@@ -212,11 +212,11 @@ async function baseScopeContext(scope, agent) {
|
|
|
212
212
|
`verification is the human's click in the UI. Ask the user what knowledge to capture, then author concise, well-typed concepts.`);
|
|
213
213
|
}
|
|
214
214
|
if (scope.kind === "kraftwerk") {
|
|
215
|
-
const [{ workflows }, runs, knowledge,
|
|
215
|
+
const [{ workflows }, runs, knowledge, agents] = await Promise.all([
|
|
216
216
|
listWorkflows(),
|
|
217
217
|
listRuns(),
|
|
218
218
|
knowledgeIndex().catch(() => ({ bundles: [] })),
|
|
219
|
-
|
|
219
|
+
listAgents().catch(() => []),
|
|
220
220
|
]);
|
|
221
221
|
const wfLines = workflows
|
|
222
222
|
.map((w) => `- ${w.slug}: ${w.description ?? w.name ?? ""} (${w.steps} steps, ${w.agents} agents)`)
|
|
@@ -228,7 +228,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
228
228
|
const bundleLines = knowledge.bundles
|
|
229
229
|
.map((b) => `- ${b.name} (${b.concepts} concepts${b.updatedAt ? `, updated ${b.updatedAt.slice(0, 10)}` : ""})`)
|
|
230
230
|
.join("\n");
|
|
231
|
-
const
|
|
231
|
+
const agentLines = agents
|
|
232
232
|
.map((m) => `- ${m.emoji} ${m.name} (${m.slug})${m.description ? `: ${m.description}` : ""}`)
|
|
233
233
|
.join("\n");
|
|
234
234
|
return (`You are the assistant inside the kraftwerk inspector, a UI for a workflow-as-code agent framework. ` +
|
|
@@ -244,8 +244,8 @@ async function baseScopeContext(scope, agent) {
|
|
|
244
244
|
`Read with \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
245
245
|
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor kraftwerk-chat/${agent}\` ` +
|
|
246
246
|
`(stamps provenance, maintains index.md/log.md — never hand-edit those).\n\n` +
|
|
247
|
-
`##
|
|
248
|
-
`These are persistent
|
|
247
|
+
`## Agents\n${agentLines || "(none)"}\n` +
|
|
248
|
+
`These are persistent agents (defined under agents/); the user talks to them on the ` +
|
|
249
249
|
`Agents screen. Point the user there when a request clearly belongs to one of them.\n\n` +
|
|
250
250
|
`Answer questions about workflows and runs by reading the files above. Do not modify run outputs unless asked.`);
|
|
251
251
|
}
|
|
@@ -285,18 +285,18 @@ async function scopeContext(scope, agent) {
|
|
|
285
285
|
return [base, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
286
286
|
}
|
|
287
287
|
/* ---------- backend lifecycle ---------- */
|
|
288
|
-
/**
|
|
288
|
+
/** Agent sessions carry the agent's model/effort; resolved live so edits apply to new backends. */
|
|
289
289
|
async function backendTuning(scope, agent) {
|
|
290
290
|
const tuning = {};
|
|
291
|
-
if (scope.kind === "
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
tuning.model =
|
|
295
|
-
if (
|
|
296
|
-
tuning.effort =
|
|
297
|
-
// Claude discovers skills natively;
|
|
298
|
-
if (agent === "claude" &&
|
|
299
|
-
tuning.skills =
|
|
291
|
+
if (scope.kind === "agent") {
|
|
292
|
+
const def = await getAgent(scope.slug).catch(() => null);
|
|
293
|
+
if (def?.model)
|
|
294
|
+
tuning.model = def.model;
|
|
295
|
+
if (def?.effort)
|
|
296
|
+
tuning.effort = def.effort;
|
|
297
|
+
// Claude discovers skills natively; an agentined allowlist narrows that.
|
|
298
|
+
if (agent === "claude" && def?.skills)
|
|
299
|
+
tuning.skills = def.skills;
|
|
300
300
|
}
|
|
301
301
|
// Run chats live in the run folder — grant claude the project root so
|
|
302
302
|
// project-level skills and files stay reachable.
|
|
@@ -319,7 +319,7 @@ async function ensureBackend(state) {
|
|
|
319
319
|
// Routine-triggered sessions run unattended: auto-approve, but keep
|
|
320
320
|
// the request/resolution pair in the thread as an audit trail.
|
|
321
321
|
const scope = state.meta.scope;
|
|
322
|
-
if (scope.kind === "
|
|
322
|
+
if (scope.kind === "agent" && scope.routine) {
|
|
323
323
|
const allow = options.find((o) => o.kind?.startsWith("allow")) ?? options[0];
|
|
324
324
|
const requestId = randomUUID();
|
|
325
325
|
emit(state, { type: "permission_request", requestId, title, options });
|
|
@@ -418,7 +418,7 @@ export async function postMessage(id, text) {
|
|
|
418
418
|
// Routine sessions are one-shot and unattended: release the agent
|
|
419
419
|
// process as soon as the turn ends instead of waiting for the reaper.
|
|
420
420
|
const scope = state.meta.scope;
|
|
421
|
-
if (scope.kind === "
|
|
421
|
+
if (scope.kind === "agent" && scope.routine)
|
|
422
422
|
dropBackend(state);
|
|
423
423
|
}
|
|
424
424
|
catch (err) {
|