@netnodeag/kraftwerk 0.40.0 → 0.41.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.
Files changed (25) hide show
  1. package/README.md +43 -0
  2. package/dist/inspector/channels.d.ts +48 -0
  3. package/dist/inspector/channels.js +130 -0
  4. package/dist/inspector/chat/sessions.d.ts +17 -1
  5. package/dist/inspector/chat/sessions.js +280 -44
  6. package/dist/inspector/chat/types.d.ts +18 -0
  7. package/dist/inspector/notifications.js +2 -0
  8. package/dist/inspector/server.js +74 -2
  9. package/inspector/dist/assets/{dist-HuQTcyVK.js → dist-BO4gDbWB.js} +1 -1
  10. package/inspector/dist/assets/{dist-Bv3fOisq.js → dist-BiIrAs8U.js} +1 -1
  11. package/inspector/dist/assets/{dist-uQIhtGKj.js → dist-Bj7XitTo.js} +1 -1
  12. package/inspector/dist/assets/{dist-D9-MFS0x.js → dist-BvHfHbzl.js} +1 -1
  13. package/inspector/dist/assets/{dist-Dyv2EX0S.js → dist-CASrSR5E.js} +1 -1
  14. package/inspector/dist/assets/{dist-XL4xXfDo.js → dist-CoT4dTT9.js} +1 -1
  15. package/inspector/dist/assets/{dist-Cex4_N6v.js → dist-CqbReWDG.js} +1 -1
  16. package/inspector/dist/assets/{dist-DdbyO9w9.js → dist-DKds1beX.js} +1 -1
  17. package/inspector/dist/assets/dist-Dg2YcNTD.js +1 -0
  18. package/inspector/dist/assets/{dist-AXonJXol.js → dist-g0pveAaF.js} +1 -1
  19. package/inspector/dist/assets/{dist-BJLTdJRF.js → dist-h7tJ3T-Z.js} +1 -1
  20. package/inspector/dist/assets/{editor--4qv8VP5.js → editor-BiyVJ7Wz.js} +3 -3
  21. package/inspector/dist/assets/{index-E1E7-_NW.css → index-CHI0eOQa.css} +1 -1
  22. package/inspector/dist/assets/{index-BSqc9eg2.js → index-lwVGUk9H.js} +17 -17
  23. package/inspector/dist/index.html +2 -2
  24. package/package.json +1 -1
  25. package/inspector/dist/assets/dist-UTSke4yL.js +0 -1
package/README.md CHANGED
@@ -417,6 +417,49 @@ Manage routines on the agent page, where you can create, edit and
417
417
  delete them, toggle enabled, hit "run now", and jump to the last run's
418
418
  session. Schedules missed while the server is down are skipped, not replayed.
419
419
 
420
+ ## Channels
421
+
422
+ A channel is one conversation shared by several agents and humans — a Slack
423
+ channel where the coworkers are agents. The definition is git-tracked with
424
+ the workspace, the transcript is a chat like any other:
425
+
426
+ ```
427
+ channels/<slug>/channel.yml # name, purpose, members, responder, maxHops
428
+ output/chats/<chat-id>/ # the transcript (scope { kind: channel })
429
+ ```
430
+
431
+ ```yaml
432
+ name: Website relaunch
433
+ purpose: ship the new site by March
434
+ members: [researcher, writer, dev-ops] # agent slugs
435
+ responder: researcher # answers when nobody is @mentioned (optional)
436
+ maxHops: 3 # agent-to-agent handovers per human message
437
+ ```
438
+
439
+ Every member agent has its own seat in the channel: its own process, its
440
+ own persona, skills, model and permissions, exactly as in a direct session.
441
+ What it receives is not the whole transcript each time but the messages
442
+ since its last turn, each prefixed with the author (`[Lukas]:` a human,
443
+ `[@writer]:` an agent). Who answers:
444
+
445
+ - `@mention` an agent and it wakes; several mentions run in parallel and
446
+ their replies stream in as separate posts.
447
+ - No mention: the channel's responder answers, or nobody if none is set.
448
+ - Agents hand over by mentioning each other; `maxHops` bounds the chain
449
+ per human message so two agents never talk forever.
450
+ - Humans are never blocked. An agent that is busy when mentioned again runs
451
+ once more when its turn ends, with everything it missed.
452
+ - Permission questions from any agent show as cards in the channel and in
453
+ the bell; anyone present can answer.
454
+
455
+ Create channels on the channels screen, or from an existing agent session
456
+ with **add coworker**: the session becomes the channel's transcript, the
457
+ agent keeps its process and memory, and the agents you pick join. Your
458
+ messages carry the name set in the composer ("posting as"), stored per
459
+ browser. API: `GET/POST /api/channels`, `GET/PUT/DELETE /api/channels/:slug`,
460
+ `POST /api/channels/from-chat {chatId, name, members}`; messages go through
461
+ the chat endpoint with `from` for the poster's name.
462
+
420
463
  ## Context & Knowledge
421
464
 
422
465
  Alongside runs and chats, a project can keep curated knowledge as
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Channels: one transcript shared by several agents and humans — a Slack
3
+ * channel where the coworkers are agents. The definition is git-tracked
4
+ * with the workspace so the whole team sees the same channels:
5
+ *
6
+ * channels/<slug>/channel.yml # name, purpose, members (agent slugs), responder, maxHops
7
+ *
8
+ * The transcript itself is a chat (scope { kind: "channel", slug }) under
9
+ * <output>/chats/, run-state like every other chat. sessions.ts gives each
10
+ * member agent its own seat (process) in that chat and routes messages:
11
+ * an @mention wakes that agent, no mention wakes the channel's responder,
12
+ * an agent mentioning another hands over — bounded by maxHops per human
13
+ * message so two agents never talk forever.
14
+ */
15
+ export interface Channel {
16
+ slug: string;
17
+ name: string;
18
+ purpose?: string;
19
+ /** Agent slugs that sit in the channel. */
20
+ members: string[];
21
+ /** Who answers a message that mentions nobody; absent = nobody does. */
22
+ responder?: string;
23
+ /** Agent-to-agent handovers allowed per human message. */
24
+ maxHops: number;
25
+ }
26
+ export declare const DEFAULT_MAX_HOPS = 3;
27
+ export declare function channelsRoot(): Promise<string>;
28
+ export declare function safeChannelSlug(slug: string): string;
29
+ export declare function listChannels(): Promise<Channel[]>;
30
+ export declare function getChannel(slug: string): Promise<Channel | null>;
31
+ export interface SaveChannelInput {
32
+ slug?: string;
33
+ name?: string;
34
+ purpose?: string;
35
+ members?: string[];
36
+ /** Agent slug, or "" / null to clear. */
37
+ responder?: string | null;
38
+ maxHops?: number;
39
+ }
40
+ /** Create (no slug: derived from the name) or update a channel definition. */
41
+ export declare function saveChannel(input: SaveChannelInput): Promise<Channel>;
42
+ export declare function deleteChannel(slug: string): Promise<void>;
43
+ /**
44
+ * Who a channel message wakes. @mentions of members win (in text order, each
45
+ * once); a message that mentions nobody goes to the responder, or to nobody.
46
+ * `self` (the author, when an agent) never wakes itself.
47
+ */
48
+ export declare function mentionTargets(text: string, channel: Pick<Channel, "members" | "responder">, self?: string): string[];
@@ -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,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): Promise<{
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