@netnodeag/kraftwerk 0.57.3 → 0.58.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 +85 -2
- package/dist/acp.js +2 -1
- package/dist/cli/journal.d.ts +10 -0
- package/dist/cli/journal.js +36 -0
- package/dist/cli/knowledge.js +17 -0
- package/dist/cli/kraftwerk.js +6 -0
- package/dist/cli/projects.js +3 -3
- package/dist/cli/repos.js +1 -1
- package/dist/cli/trash.d.ts +2 -0
- package/dist/cli/trash.js +70 -0
- package/dist/cli/vibeables.js +3 -3
- package/dist/inspector/agent-status.d.ts +21 -0
- package/dist/inspector/agent-status.js +25 -0
- package/dist/inspector/agents.d.ts +5 -0
- package/dist/inspector/agents.js +10 -1
- package/dist/inspector/attention.d.ts +48 -0
- package/dist/inspector/attention.js +66 -0
- package/dist/inspector/channels.js +2 -1
- package/dist/inspector/chat/sessions.d.ts +52 -1
- package/dist/inspector/chat/sessions.js +160 -27
- package/dist/inspector/chat/store.d.ts +2 -0
- package/dist/inspector/chat/store.js +5 -1
- package/dist/inspector/files.d.ts +87 -0
- package/dist/inspector/files.js +316 -0
- package/dist/inspector/git.js +3 -1
- package/dist/inspector/journal.d.ts +31 -0
- package/dist/inspector/journal.js +72 -0
- package/dist/inspector/knowledge.d.ts +2 -0
- package/dist/inspector/knowledge.js +7 -0
- package/dist/inspector/projects.d.ts +1 -1
- package/dist/inspector/projects.js +5 -2
- package/dist/inspector/repos.js +2 -1
- package/dist/inspector/runs.d.ts +1 -1
- package/dist/inspector/runs.js +3 -2
- package/dist/inspector/server.js +111 -1
- package/dist/inspector/skills.d.ts +1 -1
- package/dist/inspector/skills.js +3 -2
- package/dist/inspector/status-line.d.ts +14 -0
- package/dist/inspector/status-line.js +44 -0
- package/dist/inspector/trash.d.ts +37 -0
- package/dist/inspector/trash.js +148 -0
- package/dist/inspector/vibeables.d.ts +1 -1
- package/dist/inspector/vibeables.js +3 -2
- package/inspector/dist/assets/dist-B2o7tVdJ.js +23 -0
- package/inspector/dist/assets/{dist-Ceimhh7q.js → dist-BA7UaMHM.js} +1 -1
- package/inspector/dist/assets/dist-BXQtebmM.js +1 -0
- package/inspector/dist/assets/dist-BYDYk38f.js +1 -0
- package/inspector/dist/assets/{dist-CPlSMtdY.js → dist-BdZ5cNRp.js} +1 -1
- package/inspector/dist/assets/{dist-DGoW6poO.js → dist-BgakrfmB.js} +1 -1
- package/inspector/dist/assets/{dist-CprubYd5.js → dist-BpUghI2_.js} +1 -1
- package/inspector/dist/assets/{dist-BdimISeT.js → dist-BpYRdsio.js} +1 -1
- package/inspector/dist/assets/{dist-C9wdXHwY.js → dist-CHSqB1rM.js} +1 -1
- package/inspector/dist/assets/{dist-B1xH7Mxz.js → dist-CVpPKkOb.js} +1 -1
- package/inspector/dist/assets/{dist-H3rerbMg.js → dist-CYzOYk_e.js} +9 -9
- package/inspector/dist/assets/dist-CzkTIkYD.js +1 -0
- package/inspector/dist/assets/{dist-rE2oRtJp.js → dist-D7O3CloR.js} +1 -1
- package/inspector/dist/assets/dist-DJzkq2t-.js +1 -0
- package/inspector/dist/assets/dist-Dctj7Mr3.js +1 -0
- package/inspector/dist/assets/{dist-BA9oX2Qo.js → dist-DtEBn0Pj.js} +1 -1
- package/inspector/dist/assets/{dist-CmUexHfa.js → dist-DwpQHmP7.js} +1 -1
- package/inspector/dist/assets/{dist-CF7An11X.js → dist-KG4lrraG.js} +1 -1
- package/inspector/dist/assets/{dist-5wk4daHl.js → dist-Ko3fuaif.js} +1 -1
- package/inspector/dist/assets/{dist-ZMc9InB9.js → dist-f7v359XP.js} +1 -1
- package/inspector/dist/assets/{dist-yFKhRrHN.js → dist-itDgDZqI.js} +1 -1
- package/inspector/dist/assets/{dist-CXS6W3P9.js → dist-p_qW90fR.js} +1 -1
- package/inspector/dist/assets/{dist-DNBDudvB.js → dist-xi1nJU9x.js} +1 -1
- package/inspector/dist/assets/editor-BkGiMzB_.js +120 -0
- package/inspector/dist/assets/editor-CZMrhTTL.css +1 -0
- package/inspector/dist/assets/index-Bb27CwDb.css +2 -0
- package/inspector/dist/assets/index-D4h-VE-4.js +229 -0
- package/inspector/dist/assets/material-symbols-rounded-Dni8jOUD.woff2 +0 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/dist-9zOw_mLO.js +0 -1
- package/inspector/dist/assets/dist-D3a-4Le2.js +0 -1
- package/inspector/dist/assets/dist-D8nmL5S9.js +0 -1
- package/inspector/dist/assets/dist-DN5yqrtf.js +0 -1
- package/inspector/dist/assets/dist-DaG9QYNU.js +0 -1
- package/inspector/dist/assets/dist-_9tJHb24.js +0 -23
- package/inspector/dist/assets/editor-B1Jq9YIq.css +0 -1
- package/inspector/dist/assets/editor-DtVtHids.js +0 -117
- package/inspector/dist/assets/index-B-sri0As.js +0 -225
- package/inspector/dist/assets/index-Da4dQf-y.css +0 -1
- package/inspector/dist/assets/material-symbols-rounded-DzDTtRam.woff2 +0 -0
|
@@ -2,6 +2,23 @@ import type { Attachment, ChatAgentId, ChatMeta, ChatScope, ElicitationAnswer, S
|
|
|
2
2
|
import { type Channel } from "../channels.js";
|
|
3
3
|
/** Server shutdown: no agent subprocess may outlive the inspector. */
|
|
4
4
|
export declare function disposeAllBackends(): void;
|
|
5
|
+
/**
|
|
6
|
+
* "## Your journal" block: the recent part of agents/<slug>/journal.md and
|
|
7
|
+
* how to add to it. This is what makes the agent the same colleague from
|
|
8
|
+
* one session to the next.
|
|
9
|
+
*/
|
|
10
|
+
export declare function agentJournalContext(def: {
|
|
11
|
+
slug: string;
|
|
12
|
+
harness: ChatAgentId;
|
|
13
|
+
}): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* "## Files" block for an agent: the workspace's files and the folders in
|
|
16
|
+
* it linked to the agent. An agent owns no files; in a project it works
|
|
17
|
+
* with the project's (the project block says where).
|
|
18
|
+
*/
|
|
19
|
+
export declare function agentFilesContext(def: {
|
|
20
|
+
files?: string[];
|
|
21
|
+
}): Promise<string>;
|
|
5
22
|
/**
|
|
6
23
|
* "## Your vibeables" block: the apps an agent builds and maintains, linked
|
|
7
24
|
* in agent.yml like knowledge. Empty when nothing is linked. When the
|
|
@@ -36,6 +53,34 @@ export declare function createChat(opts: {
|
|
|
36
53
|
}): Promise<ChatMeta>;
|
|
37
54
|
/** A permission request is waiting for a human in this chat (live state only — a restart drops it with the backend). */
|
|
38
55
|
export declare function chatAwaitingApproval(id: string): boolean;
|
|
56
|
+
/** A request in a chat that waits for a human: an approval or a question, with the seat (agent) that raised it. */
|
|
57
|
+
export interface WaitingRequest {
|
|
58
|
+
requestId: string;
|
|
59
|
+
kind: "approval" | "question";
|
|
60
|
+
title: string;
|
|
61
|
+
since: string;
|
|
62
|
+
meta: ChatMeta;
|
|
63
|
+
/** The seat that asked: MAIN in an ordinary chat, the agent slug in a channel. */
|
|
64
|
+
seat: string;
|
|
65
|
+
}
|
|
66
|
+
/** Every request waiting for a human, across the chats in memory. */
|
|
67
|
+
export declare function waitingRequests(): WaitingRequest[];
|
|
68
|
+
/** One chat an agent is in right now: mid-turn, or waiting on a human. */
|
|
69
|
+
export interface AgentActivity {
|
|
70
|
+
chatId: string;
|
|
71
|
+
title: string;
|
|
72
|
+
since?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Live work per agent slug, from the chats in memory: where its seat is
|
|
76
|
+
* mid-turn (working) and where it raised a permission request or a
|
|
77
|
+
* question nobody answered yet (waiting). Agent sessions and channel seats
|
|
78
|
+
* alike; plain and project chats have no agent and are left out.
|
|
79
|
+
*/
|
|
80
|
+
export declare function agentActivity(): Map<string, {
|
|
81
|
+
working: AgentActivity[];
|
|
82
|
+
waiting: AgentActivity[];
|
|
83
|
+
}>;
|
|
39
84
|
export declare function listChats(): Promise<Array<ChatMeta & {
|
|
40
85
|
busy: boolean;
|
|
41
86
|
awaitingApproval: boolean;
|
|
@@ -132,10 +177,16 @@ export declare function listAgentSessionsFor(agent: ChatAgentId): Promise<Array<
|
|
|
132
177
|
export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
|
|
133
178
|
error?: string;
|
|
134
179
|
}>;
|
|
135
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Delete a chat: kill its backend, drop the live state, move its folder to
|
|
182
|
+
* the trash. The agents' own transcripts stay until it is deleted from the
|
|
183
|
+
* trash (purgeChatTranscripts), so a restored chat can resume.
|
|
184
|
+
*/
|
|
136
185
|
export declare function deleteChat(id: string): Promise<{
|
|
137
186
|
error?: string;
|
|
138
187
|
}>;
|
|
188
|
+
/** The agents' own transcripts of a chat folder (one in the trash), deleted for good. Best effort. */
|
|
189
|
+
export declare function purgeChatTranscripts(dir: string): Promise<void>;
|
|
139
190
|
/**
|
|
140
191
|
* Interrupt the agent's turn — in a channel, one agent's (`agent` = its
|
|
141
192
|
* slug) or every agent's. Whatever the agent was waiting on from a human
|
|
@@ -14,7 +14,10 @@ import { startPiBackend } from "./pi.js";
|
|
|
14
14
|
import { UNATTENDED_PERMISSION_TIMEOUT_MS, declineOption, unattendedTimeoutLabel } from "./permissions.js";
|
|
15
15
|
import { chatHref, dismissKey, pushNotification } from "../notifications.js";
|
|
16
16
|
import { getChannel, mentionTargets } from "../channels.js";
|
|
17
|
-
import {
|
|
17
|
+
import { moveToTrash } from "../trash.js";
|
|
18
|
+
import { JOURNAL_FILE, journalExcerpt, readJournal } from "../journal.js";
|
|
19
|
+
import { filesSummary } from "../files.js";
|
|
20
|
+
import { appendEvent, attachmentPath, copyChatFiles, listChatMetas, newChatId, readEvents, readMeta, readMetaAt, safeChatDir, writeMeta, } from "./store.js";
|
|
18
21
|
const MAIN = "main";
|
|
19
22
|
const states = new Map();
|
|
20
23
|
const newSeat = (key, harness) => ({
|
|
@@ -55,6 +58,28 @@ function chatLabel(meta) {
|
|
|
55
58
|
return meta.title;
|
|
56
59
|
return meta.scope.kind === "agent" ? meta.scope.slug : "chat";
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Whose a chat is, owner first, for a notification: "📁 Relaunch · 🦊 Lisa",
|
|
63
|
+
* "🦊 Lisa", "#launch · 🐻 Max", "Ralv · <chat title>". The project or
|
|
64
|
+
* agent comes before the chat because that is where you go.
|
|
65
|
+
*/
|
|
66
|
+
async function ownerLabel(meta, seatKey) {
|
|
67
|
+
const agentName = async (slug) => {
|
|
68
|
+
const a = await getAgent(slug).catch(() => null);
|
|
69
|
+
return a ? `${a.emoji ? `${a.emoji} ` : ""}${a.name}` : slug;
|
|
70
|
+
};
|
|
71
|
+
const projectName = async (slug) => `📁 ${(await getProject(slug).catch(() => null))?.title ?? slug}`;
|
|
72
|
+
const { scope } = meta;
|
|
73
|
+
if (scope.kind === "agent")
|
|
74
|
+
return agentName(scope.slug);
|
|
75
|
+
if (scope.kind === "project")
|
|
76
|
+
return projectName(scope.slug);
|
|
77
|
+
if (scope.kind === "channel") {
|
|
78
|
+
const parts = [meta.project ? await projectName(meta.project) : `#${scope.slug}`, await agentName(seatKey)];
|
|
79
|
+
return parts.join(" · ");
|
|
80
|
+
}
|
|
81
|
+
return `Ralv${meta.title ? ` · ${meta.title}` : ""}`;
|
|
82
|
+
}
|
|
58
83
|
/** Text of the agent's final message (or last error) after `afterSeq`, for a notification body. */
|
|
59
84
|
function lastEventText(state, afterSeq, type) {
|
|
60
85
|
const parts = [];
|
|
@@ -133,6 +158,7 @@ async function loadState(id) {
|
|
|
133
158
|
pendingPermissions: new Map(),
|
|
134
159
|
pendingElicitations: new Map(),
|
|
135
160
|
pendingSeat: new Map(),
|
|
161
|
+
pendingInfo: new Map(),
|
|
136
162
|
writeChain: Promise.resolve(),
|
|
137
163
|
};
|
|
138
164
|
// Two racing loads: keep whichever registered first.
|
|
@@ -290,6 +316,50 @@ async function agentKnowledgeContext(def) {
|
|
|
290
316
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
291
317
|
`click in the UI.\n\n`);
|
|
292
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* "## Your journal" block: the recent part of agents/<slug>/journal.md and
|
|
321
|
+
* how to add to it. This is what makes the agent the same colleague from
|
|
322
|
+
* one session to the next.
|
|
323
|
+
*/
|
|
324
|
+
export async function agentJournalContext(def) {
|
|
325
|
+
const { text, truncated } = journalExcerpt(await readJournal(def.slug).catch(() => ""));
|
|
326
|
+
return (`## Your journal
|
|
327
|
+
Your own notes from earlier sessions, newest first (agents/${def.slug}/${JOURNAL_FILE}). ` +
|
|
328
|
+
`They are your memory between sessions: build on them, keep what you promised, and check facts that may have changed since.
|
|
329
|
+
|
|
330
|
+
` +
|
|
331
|
+
(text ? `${text}
|
|
332
|
+
` : `(empty: nothing recorded yet)
|
|
333
|
+
`) +
|
|
334
|
+
(truncated ? `(older days are in the file; read it when you need them)
|
|
335
|
+
` : "") +
|
|
336
|
+
`
|
|
337
|
+
Add to it when something should outlive this session, one line each:
|
|
338
|
+
` +
|
|
339
|
+
` npx kraftwerk journal ${def.slug} "<one line>" --kind learned|decided|promised|done
|
|
340
|
+
` +
|
|
341
|
+
`learned: a durable fact about the user, the work or the workspace. decided: a decision and its reason. ` +
|
|
342
|
+
`promised: something you said you would do. done: a promise kept or a piece of work finished. ` +
|
|
343
|
+
`Write a few lines at the end of a session that changed something, not a transcript; no secrets. ` +
|
|
344
|
+
`Never edit ${JOURNAL_FILE} by hand.
|
|
345
|
+
|
|
346
|
+
`);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* "## Files" block for an agent: the workspace's files and the folders in
|
|
350
|
+
* it linked to the agent. An agent owns no files; in a project it works
|
|
351
|
+
* with the project's (the project block says where).
|
|
352
|
+
*/
|
|
353
|
+
export async function agentFilesContext(def) {
|
|
354
|
+
const ws = await filesSummary("workspace").catch(() => null);
|
|
355
|
+
if (!ws)
|
|
356
|
+
return "";
|
|
357
|
+
const linked = (def.files ?? []).map((f) => `- ${ws.root}/${f}/`).join("\n");
|
|
358
|
+
return (`## Files\nThe workspace's shared files (any format: brand assets, templates, contracts, …) are in ${ws.root}/ (${ws.count} file${ws.count === 1 ? "" : "s"}). ` +
|
|
359
|
+
(linked ? `The folders that are part of your job:\n${linked}\n` : "") +
|
|
360
|
+
`Work you do for a project belongs in that project's files folder, not here. What you produce in a direct session goes to ${ws.root}/inbox/. ` +
|
|
361
|
+
`Delete nothing there unless asked.\n\n`);
|
|
362
|
+
}
|
|
293
363
|
/**
|
|
294
364
|
* "## Your vibeables" block: the apps an agent builds and maintains, linked
|
|
295
365
|
* in agent.yml like knowledge. Empty when nothing is linked. When the
|
|
@@ -425,6 +495,8 @@ async function baseScopeContext(scope, agent) {
|
|
|
425
495
|
: "") +
|
|
426
496
|
(await agentKnowledgeContext(def)) +
|
|
427
497
|
(await agentVibeablesContext(def)) +
|
|
498
|
+
(await agentJournalContext(def)) +
|
|
499
|
+
(await agentFilesContext(def)) +
|
|
428
500
|
`The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
|
|
429
501
|
`say so and suggest which agent or tool fits better.` +
|
|
430
502
|
(scope.routine
|
|
@@ -508,6 +580,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
508
580
|
`## Agents\n${agentLines || "(none)"}\n` +
|
|
509
581
|
`These are persistent agents (defined under agents/); the user talks to them on the ` +
|
|
510
582
|
`Agents screen. Point the user there when a request clearly belongs to one of them.\n\n` +
|
|
583
|
+
(await agentFilesContext({})) +
|
|
511
584
|
`Answer questions about workflows and runs by reading the files above. Do not modify run outputs unless asked.`);
|
|
512
585
|
}
|
|
513
586
|
if (scope.kind === "run") {
|
|
@@ -645,23 +718,32 @@ async function ensureBackend(state, seat) {
|
|
|
645
718
|
const notifyKey = `approval:${state.meta.id}:${requestId}`;
|
|
646
719
|
let deadline;
|
|
647
720
|
state.pendingSeat.set(requestId, seat.key);
|
|
721
|
+
state.pendingInfo.set(requestId, { kind: "approval", title, since: new Date().toISOString() });
|
|
648
722
|
state.pendingPermissions.set(requestId, (optionId) => {
|
|
649
723
|
if (deadline)
|
|
650
724
|
clearTimeout(deadline);
|
|
651
725
|
state.pendingPermissions.delete(requestId);
|
|
652
726
|
state.pendingSeat.delete(requestId);
|
|
727
|
+
state.pendingInfo.delete(requestId);
|
|
653
728
|
emit(state, { type: "permission_resolved", requestId, optionId }, me);
|
|
654
729
|
void dismissKey(notifyKey);
|
|
655
730
|
resolve(optionId);
|
|
656
731
|
});
|
|
657
732
|
emit(state, { type: "permission_request", requestId, title, options }, me);
|
|
658
733
|
// The bell: a question is waiting. Answered -> the item goes away again.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
734
|
+
// The owner lookup is async: answered meanwhile, there is nothing to notify about (and a late item is taken back).
|
|
735
|
+
void ownerLabel(state.meta, seat.key).then(async (who) => {
|
|
736
|
+
if (!state.pendingInfo.has(requestId))
|
|
737
|
+
return;
|
|
738
|
+
await pushNotification({
|
|
739
|
+
kind: "approval",
|
|
740
|
+
key: notifyKey,
|
|
741
|
+
title: `${who} needs approval`,
|
|
742
|
+
body: title,
|
|
743
|
+
href: `${chatHref(state.meta)}?focus=${encodeURIComponent(requestId)}`,
|
|
744
|
+
});
|
|
745
|
+
if (!state.pendingInfo.has(requestId))
|
|
746
|
+
await dismissKey(notifyKey);
|
|
665
747
|
});
|
|
666
748
|
if (isUnattended(state.meta.scope)) {
|
|
667
749
|
deadline = setTimeout(() => {
|
|
@@ -688,22 +770,31 @@ async function ensureBackend(state, seat) {
|
|
|
688
770
|
const notifyKey = `question:${state.meta.id}:${requestId}`;
|
|
689
771
|
let deadline;
|
|
690
772
|
state.pendingSeat.set(requestId, seat.key);
|
|
773
|
+
state.pendingInfo.set(requestId, { kind: "question", title: message, since: new Date().toISOString() });
|
|
691
774
|
state.pendingElicitations.set(requestId, (answer) => {
|
|
692
775
|
if (deadline)
|
|
693
776
|
clearTimeout(deadline);
|
|
694
777
|
state.pendingElicitations.delete(requestId);
|
|
695
778
|
state.pendingSeat.delete(requestId);
|
|
779
|
+
state.pendingInfo.delete(requestId);
|
|
696
780
|
emit(state, { type: "elicitation_resolved", requestId, ...answer }, me);
|
|
697
781
|
void dismissKey(notifyKey);
|
|
698
782
|
resolve(answer);
|
|
699
783
|
});
|
|
700
784
|
emit(state, { type: "elicitation_request", requestId, message, fields }, me);
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
785
|
+
// The owner lookup is async: answered meanwhile, there is nothing to notify about (and a late item is taken back).
|
|
786
|
+
void ownerLabel(state.meta, seat.key).then(async (who) => {
|
|
787
|
+
if (!state.pendingInfo.has(requestId))
|
|
788
|
+
return;
|
|
789
|
+
await pushNotification({
|
|
790
|
+
kind: "approval",
|
|
791
|
+
key: notifyKey,
|
|
792
|
+
title: `${who} has a question`,
|
|
793
|
+
body: message,
|
|
794
|
+
href: `${chatHref(state.meta)}?focus=${encodeURIComponent(requestId)}`,
|
|
795
|
+
});
|
|
796
|
+
if (!state.pendingInfo.has(requestId))
|
|
797
|
+
await dismissKey(notifyKey);
|
|
707
798
|
});
|
|
708
799
|
if (isUnattended(state.meta.scope)) {
|
|
709
800
|
deadline = setTimeout(() => {
|
|
@@ -775,13 +866,48 @@ export async function createChat(opts) {
|
|
|
775
866
|
pendingPermissions: new Map(),
|
|
776
867
|
pendingElicitations: new Map(),
|
|
777
868
|
pendingSeat: new Map(),
|
|
869
|
+
pendingInfo: new Map(),
|
|
778
870
|
writeChain: Promise.resolve(),
|
|
779
871
|
});
|
|
780
872
|
return meta;
|
|
781
873
|
}
|
|
782
874
|
/** A permission request is waiting for a human in this chat (live state only — a restart drops it with the backend). */
|
|
783
875
|
export function chatAwaitingApproval(id) {
|
|
784
|
-
return (states.get(id)?.
|
|
876
|
+
return (states.get(id)?.pendingInfo.size ?? 0) > 0;
|
|
877
|
+
}
|
|
878
|
+
/** Every request waiting for a human, across the chats in memory. */
|
|
879
|
+
export function waitingRequests() {
|
|
880
|
+
const out = [];
|
|
881
|
+
for (const state of states.values()) {
|
|
882
|
+
for (const [requestId, info] of state.pendingInfo) {
|
|
883
|
+
out.push({ requestId, ...info, meta: state.meta, seat: state.pendingSeat.get(requestId) ?? MAIN });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return out;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Live work per agent slug, from the chats in memory: where its seat is
|
|
890
|
+
* mid-turn (working) and where it raised a permission request or a
|
|
891
|
+
* question nobody answered yet (waiting). Agent sessions and channel seats
|
|
892
|
+
* alike; plain and project chats have no agent and are left out.
|
|
893
|
+
*/
|
|
894
|
+
export function agentActivity() {
|
|
895
|
+
const out = new Map();
|
|
896
|
+
const of = (slug) => out.get(slug) ?? out.set(slug, { working: [], waiting: [] }).get(slug);
|
|
897
|
+
for (const state of states.values()) {
|
|
898
|
+
const { meta } = state;
|
|
899
|
+
for (const seat of state.seats.values()) {
|
|
900
|
+
const slug = meta.scope.kind === "agent" ? meta.scope.slug : meta.scope.kind === "channel" ? seat.key : undefined;
|
|
901
|
+
if (!slug)
|
|
902
|
+
continue;
|
|
903
|
+
const item = { chatId: meta.id, title: meta.title };
|
|
904
|
+
if (seat.busy)
|
|
905
|
+
of(slug).working.push({ ...item, ...(seat.busySince ? { since: seat.busySince } : {}) });
|
|
906
|
+
if ([...state.pendingSeat.values()].includes(seat.key))
|
|
907
|
+
of(slug).waiting.push(item);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
785
911
|
}
|
|
786
912
|
export async function listChats() {
|
|
787
913
|
const metas = await listChatMetas();
|
|
@@ -810,6 +936,7 @@ export async function postMessage(id, text, opts = {}) {
|
|
|
810
936
|
if (seat.busy)
|
|
811
937
|
return { error: "agent is still working — wait for the turn to finish" };
|
|
812
938
|
seat.busy = true;
|
|
939
|
+
seat.busySince = new Date().toISOString();
|
|
813
940
|
const isFirst = !state.events.some((e) => e.type === "user_message");
|
|
814
941
|
if (isFirst && !state.meta.title) {
|
|
815
942
|
state.meta.title = text.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
@@ -841,7 +968,7 @@ export async function postMessage(id, text, opts = {}) {
|
|
|
841
968
|
const failed = state.events.some((e) => e.seq > turnStart && (e.type === "error" || (e.type === "failure" && e.severity === "error")));
|
|
842
969
|
void pushNotification({
|
|
843
970
|
kind: failed ? "routine_failed" : "routine_done",
|
|
844
|
-
title: `${chatLabel(state.meta)} ${failed ? "ended with an error" : "finished"}`,
|
|
971
|
+
title: `${await ownerLabel(state.meta, MAIN)} · ${chatLabel(state.meta)} ${failed ? "ended with an error" : "finished"}`,
|
|
845
972
|
body: failed ? lastEventText(state, turnStart, "error") : lastEventText(state, turnStart, "text"),
|
|
846
973
|
href: chatHref(state.meta),
|
|
847
974
|
...(failed ? { diagnose: routineRef(state.meta) } : {}),
|
|
@@ -853,7 +980,7 @@ export async function postMessage(id, text, opts = {}) {
|
|
|
853
980
|
if (isUnattended(state.meta.scope)) {
|
|
854
981
|
void pushNotification({
|
|
855
982
|
kind: "routine_failed",
|
|
856
|
-
title: `${chatLabel(state.meta)} failed`,
|
|
983
|
+
title: `${await ownerLabel(state.meta, MAIN)} · ${chatLabel(state.meta)} failed`,
|
|
857
984
|
body: err.message,
|
|
858
985
|
href: chatHref(state.meta),
|
|
859
986
|
diagnose: routineRef(state.meta),
|
|
@@ -950,6 +1077,7 @@ function seatTextSince(state, afterSeq, slug) {
|
|
|
950
1077
|
}
|
|
951
1078
|
async function runSeatTurn(state, channel, seat, hops) {
|
|
952
1079
|
seat.busy = true;
|
|
1080
|
+
seat.busySince = new Date().toISOString();
|
|
953
1081
|
const me = { kind: "agent", slug: seat.key };
|
|
954
1082
|
const start = emit(state, { type: "turn_start" }, me).seq;
|
|
955
1083
|
try {
|
|
@@ -1270,7 +1398,11 @@ export async function resolvePermission(id, requestId, optionId) {
|
|
|
1270
1398
|
resolve(optionId);
|
|
1271
1399
|
return {};
|
|
1272
1400
|
}
|
|
1273
|
-
/**
|
|
1401
|
+
/**
|
|
1402
|
+
* Delete a chat: kill its backend, drop the live state, move its folder to
|
|
1403
|
+
* the trash. The agents' own transcripts stay until it is deleted from the
|
|
1404
|
+
* trash (purgeChatTranscripts), so a restored chat can resume.
|
|
1405
|
+
*/
|
|
1274
1406
|
export async function deleteChat(id) {
|
|
1275
1407
|
const state = await loadState(id);
|
|
1276
1408
|
if (!state)
|
|
@@ -1281,18 +1413,19 @@ export async function deleteChat(id) {
|
|
|
1281
1413
|
resolve({ action: "cancel" });
|
|
1282
1414
|
dropBackend(state);
|
|
1283
1415
|
states.delete(id);
|
|
1284
|
-
await
|
|
1285
|
-
// The agent's own transcripts go with the chat (best effort, in the background).
|
|
1286
|
-
const cwd = await effectiveCwd(state.meta);
|
|
1287
|
-
for (const [seatKey, sessionId] of Object.entries(state.meta.sessions ?? {})) {
|
|
1288
|
-
void (async () => {
|
|
1289
|
-
const harness = seatKey === MAIN ? state.meta.agent : (await getAgent(seatKey).catch(() => null))?.harness;
|
|
1290
|
-
if (harness && harness !== "pi")
|
|
1291
|
-
await deleteAgentSession(harness, cwd, sessionId);
|
|
1292
|
-
})().catch(() => { });
|
|
1293
|
-
}
|
|
1416
|
+
await moveToTrash("chats", id, safeChatDir(id));
|
|
1294
1417
|
return {};
|
|
1295
1418
|
}
|
|
1419
|
+
/** The agents' own transcripts of a chat folder (one in the trash), deleted for good. Best effort. */
|
|
1420
|
+
export async function purgeChatTranscripts(dir) {
|
|
1421
|
+
const meta = await readMetaAt(dir);
|
|
1422
|
+
const cwd = await effectiveCwd(meta);
|
|
1423
|
+
for (const [seatKey, sessionId] of Object.entries(meta.sessions ?? {})) {
|
|
1424
|
+
const harness = seatKey === MAIN ? meta.agent : (await getAgent(seatKey).catch(() => null))?.harness;
|
|
1425
|
+
if (harness && harness !== "pi")
|
|
1426
|
+
await deleteAgentSession(harness, cwd, sessionId).catch(() => { });
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1296
1429
|
/**
|
|
1297
1430
|
* Interrupt the agent's turn — in a channel, one agent's (`agent` = its
|
|
1298
1431
|
* slug) or every agent's. Whatever the agent was waiting on from a human
|
|
@@ -3,6 +3,8 @@ export declare function safeChatDir(id: string): string;
|
|
|
3
3
|
export declare function newChatId(): string;
|
|
4
4
|
export declare function writeMeta(meta: ChatMeta): Promise<void>;
|
|
5
5
|
export declare function readMeta(id: string): Promise<ChatMeta | null>;
|
|
6
|
+
/** meta.json of a chat folder wherever it is (also one in the trash); throws when unreadable. */
|
|
7
|
+
export declare function readMetaAt(dir: string): Promise<ChatMeta>;
|
|
6
8
|
/** Where a chat keeps the files dropped into it. */
|
|
7
9
|
export declare function attachmentsDir(id: string): string;
|
|
8
10
|
/** A stored attachment's path; rejects names that could leave the folder. */
|
|
@@ -26,12 +26,16 @@ export async function writeMeta(meta) {
|
|
|
26
26
|
}
|
|
27
27
|
export async function readMeta(id) {
|
|
28
28
|
try {
|
|
29
|
-
return
|
|
29
|
+
return await readMetaAt(safeChatDir(id));
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/** meta.json of a chat folder wherever it is (also one in the trash); throws when unreadable. */
|
|
36
|
+
export async function readMetaAt(dir) {
|
|
37
|
+
return upgradeMeta(JSON.parse(await fs.readFile(path.join(dir, "meta.json"), "utf8")));
|
|
38
|
+
}
|
|
35
39
|
/**
|
|
36
40
|
* Chats written before 0.36 scoped agent sessions as
|
|
37
41
|
* { kind: "team", member } — read them as { kind: "agent", slug } so the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* Files: the material of the work, any format, as plain folders. Two
|
|
4
|
+
* owners, by how long a file lives:
|
|
5
|
+
*
|
|
6
|
+
* kraftwerk-data/files/ workspace files: shared, outlive any project
|
|
7
|
+
* kraftwerk-data/projects/<slug>/files/ a project's files: briefing, drafts, deliverables
|
|
8
|
+
*
|
|
9
|
+
* Agents own none (they link folders; see agentFilesContext). Being plain
|
|
10
|
+
* folders, agents read and write them like any directory, the workspace
|
|
11
|
+
* git keeps their history, and a delete goes to the trash. Files over
|
|
12
|
+
* LOCAL_OVER_BYTES stay out of git: listed in the root's own .gitignore,
|
|
13
|
+
* shown as "local only". Knowledge is separate: a file becomes evidence in
|
|
14
|
+
* a bundle's references/ through "learn from this" (later).
|
|
15
|
+
*/
|
|
16
|
+
export declare const WORKSPACE_FILES_DIR = "kraftwerk-data/files";
|
|
17
|
+
export declare const PROJECT_FILES_DIR = "files";
|
|
18
|
+
/** Bigger files stay local: kept out of the workspace git. */
|
|
19
|
+
export declare const LOCAL_OVER_BYTES: number;
|
|
20
|
+
/** Upload cap: a guard against a runaway request, not a policy. */
|
|
21
|
+
export declare const MAX_UPLOAD_BYTES: number;
|
|
22
|
+
/** "workspace" or "project:<slug>". */
|
|
23
|
+
export type FilesScope = string;
|
|
24
|
+
export interface FileEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
/** Path inside the scope's root, forward slashes. */
|
|
27
|
+
path: string;
|
|
28
|
+
type: "dir" | "file";
|
|
29
|
+
size?: number;
|
|
30
|
+
modifiedAt: string;
|
|
31
|
+
/** dirs: files below it (recursive, capped). */
|
|
32
|
+
count?: number;
|
|
33
|
+
mime?: string;
|
|
34
|
+
/** Over the size limit: kept out of git. */
|
|
35
|
+
local?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface FilesListing {
|
|
38
|
+
scope: FilesScope;
|
|
39
|
+
label: string;
|
|
40
|
+
/** The folder, inside the root ("" = the root). */
|
|
41
|
+
path: string;
|
|
42
|
+
/** The root relative to the workspace, for the agent and the UI. */
|
|
43
|
+
root: string;
|
|
44
|
+
entries: FileEntry[];
|
|
45
|
+
}
|
|
46
|
+
export interface FilesScopeInfo {
|
|
47
|
+
scope: FilesScope;
|
|
48
|
+
label: string;
|
|
49
|
+
root: string;
|
|
50
|
+
count: number;
|
|
51
|
+
status?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare const mimeOf: (name: string) => string;
|
|
54
|
+
/** The root folder of a scope, its display label, and the root relative to the workspace. */
|
|
55
|
+
export declare function scopeRoot(scope: FilesScope): Promise<{
|
|
56
|
+
abs: string;
|
|
57
|
+
rel: string;
|
|
58
|
+
label: string;
|
|
59
|
+
workspace: string;
|
|
60
|
+
}>;
|
|
61
|
+
export declare function listScopes(): Promise<FilesScopeInfo[]>;
|
|
62
|
+
export declare function listFiles(scope: FilesScope, rel?: string): Promise<FilesListing>;
|
|
63
|
+
/** A file to stream to the browser. */
|
|
64
|
+
export declare function openFile(scope: FilesScope, rel: string): Promise<{
|
|
65
|
+
stream: Readable;
|
|
66
|
+
size: number;
|
|
67
|
+
mime: string;
|
|
68
|
+
name: string;
|
|
69
|
+
}>;
|
|
70
|
+
/** Stream an upload into a folder; a name already taken gets " (2)". */
|
|
71
|
+
export declare function saveUpload(scope: FilesScope, dirRel: string, name: string, body: Readable): Promise<FileEntry>;
|
|
72
|
+
export declare function makeFolder(scope: FilesScope, rel: string): Promise<FileEntry>;
|
|
73
|
+
/** Rename or move inside the scope. Refused when the target exists. */
|
|
74
|
+
export declare function moveFile(scope: FilesScope, fromRel: string, toRel: string): Promise<void>;
|
|
75
|
+
/** Move a file or folder to the trash (trash/files/<workspace | projects/slug>/<path>). */
|
|
76
|
+
export declare function deleteFile(scope: FilesScope, rel: string): Promise<void>;
|
|
77
|
+
/** "files/ (23 files; recent: designs/home-v3.png 2026-10-05, …)" — where the files are, not every file. */
|
|
78
|
+
export declare function filesSummary(scope: FilesScope, recent?: number): Promise<{
|
|
79
|
+
root: string;
|
|
80
|
+
count: number;
|
|
81
|
+
recent: {
|
|
82
|
+
path: string;
|
|
83
|
+
at: string;
|
|
84
|
+
}[];
|
|
85
|
+
} | null>;
|
|
86
|
+
/** The "## Files" block of a session's context: the folder to use, what is in it lately. */
|
|
87
|
+
export declare function filesContext(scope: FilesScope, heading: string, intro: string): Promise<string>;
|