@monster-orange/den 0.4.27 → 0.4.28
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/CHANGELOG.md +47 -0
- package/docs/distributions.md +73 -2
- package/package.json +1 -1
- package/src/shared/agents/claude-helpers.ts +623 -0
- package/src/shared/agents/den-claude.ts +54 -0
- package/src/shared/agents/helpers.ts +175 -0
- package/src/shared/agents/runtime.ts +13 -0
- package/src/shared/den-config.ts +42 -1
- package/src/shared/hub.ts +10 -0
- package/src/ui/cli/agent-ops.ts +144 -0
- package/src/ui/cli/agents-command.ts +127 -1
- package/src/ui/mcp/api.ts +8 -2
- package/src/ui/mcp/server.ts +13 -1
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-routes.ts +56 -2
- package/src/ui/server/hub-connect.ts +69 -3
- package/src/ui/server/index.ts +7 -1
- package/src/ui/server/public/agent-helpers.js +311 -0
- package/src/ui/server/public/agents-page.js +76 -9
- package/src/ui/server/public/i18n/es.js +39 -0
- package/src/ui/server/public/i18n/ja.js +37 -0
- package/src/ui/server/public/settings-registry.js +37 -1
- package/src/ui/server/public/shell.js +200 -0
- package/src/ui/server/public/transcript-view.js +202 -0
- package/src/ui/server/request-guard.ts +3 -1
- package/src/ui/server/route-classes.ts +16 -0
- package/src/ui/server/session-routes.ts +71 -1
- package/src/ui/server/ui-assets.ts +3 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D32: the helpers an agent starts inside its own session (Claude Code's
|
|
3
|
+
* Agent and Workflow subagents). They belong to the agent's session, not
|
|
4
|
+
* to Den: Den shows them, read-only, and never stops or steers them.
|
|
5
|
+
*
|
|
6
|
+
* Runtime-neutral shapes; a runtime that can read its helpers implements
|
|
7
|
+
* Runtime.helpers / Runtime.helperTranscript (den-claude: claude-helpers.ts).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** running: working now. idle: no activity for a while, not known done. */
|
|
11
|
+
export type HelperStatus = "running" | "idle" | "done" | "failed" | "stopped";
|
|
12
|
+
|
|
13
|
+
export interface Helper {
|
|
14
|
+
/** The runtime's id for it (Claude Code: agent-<id>'s <id>). */
|
|
15
|
+
id: string;
|
|
16
|
+
/** What the agent asked it to do, in a few words. Missing: show the id. */
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Its kind (general-purpose, Explore, a plugin's agent, …). */
|
|
19
|
+
type?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
/** The worktree it works in, when it has its own. */
|
|
22
|
+
branch?: string;
|
|
23
|
+
worktree?: string;
|
|
24
|
+
/** Started in the background (the agent kept going meanwhile). */
|
|
25
|
+
background?: boolean;
|
|
26
|
+
/** 1: started by the agent; 2+: started by another helper (parentId). */
|
|
27
|
+
depth: number;
|
|
28
|
+
parentId?: string;
|
|
29
|
+
/** When it ran inside a workflow. */
|
|
30
|
+
workflow?: { runId: string; name?: string; phase?: string; label?: string };
|
|
31
|
+
/** It ran in one of the agent's earlier conversations (before a relaunch). */
|
|
32
|
+
earlier?: true;
|
|
33
|
+
status: HelperStatus;
|
|
34
|
+
/** How the status was told, for "why does it say that". */
|
|
35
|
+
statusFrom: string;
|
|
36
|
+
/** Epoch ms. */
|
|
37
|
+
startedAt?: number;
|
|
38
|
+
lastActivityAt?: number;
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
/** The tool it used last, when known. */
|
|
41
|
+
lastTool?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One line of a helper's transcript, tool calls summarized. */
|
|
45
|
+
export interface HelperTranscriptItem {
|
|
46
|
+
at?: number;
|
|
47
|
+
kind: "prompt" | "text" | "tool" | "result" | "note";
|
|
48
|
+
/** For a tool call: its name. */
|
|
49
|
+
tool?: string;
|
|
50
|
+
text: string;
|
|
51
|
+
error?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface HelperTranscript {
|
|
55
|
+
helper: Helper;
|
|
56
|
+
items: HelperTranscriptItem[];
|
|
57
|
+
/** Earlier lines left out (only the last ones are shown). */
|
|
58
|
+
omitted: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read-only, and why: shown wherever helpers are. */
|
|
62
|
+
export const HELPERS_READ_ONLY_NOTE =
|
|
63
|
+
"Helpers run inside this agent's own session, so Den shows them but can't stop or steer them. To change what they do, tell the agent.";
|
|
64
|
+
|
|
65
|
+
/** D32 settings (agents.helpers.*), permissive defaults. */
|
|
66
|
+
export interface HelperSettings {
|
|
67
|
+
/** No activity for this long and not known done: idle, not running. */
|
|
68
|
+
activeMinutes: number;
|
|
69
|
+
/** Done, failed and stopped helpers fold away after this long. */
|
|
70
|
+
collapseDoneMinutes: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const HELPER_DEFAULTS: HelperSettings = {
|
|
74
|
+
activeMinutes: 10,
|
|
75
|
+
collapseDoneMinutes: 30,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** What a runtime's helpers hooks are given. */
|
|
79
|
+
export interface HelperReadOptions {
|
|
80
|
+
env?: NodeJS.ProcessEnv;
|
|
81
|
+
now?: number;
|
|
82
|
+
settings?: Partial<HelperSettings>;
|
|
83
|
+
/** Whether the agent itself is running now. */
|
|
84
|
+
agentLive?: boolean;
|
|
85
|
+
/** A transcript's last lines (default 500). */
|
|
86
|
+
limit?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Running helpers first, then the newest; a helper's own helpers right
|
|
91
|
+
* under it, in the same order (a tree, depth first). One whose parent
|
|
92
|
+
* isn't in the list sits at the top level.
|
|
93
|
+
*/
|
|
94
|
+
export function sortHelpers(hs: Helper[]): Helper[] {
|
|
95
|
+
const ids = new Set(hs.map((h) => h.id));
|
|
96
|
+
const kids = new Map<string, Helper[]>();
|
|
97
|
+
const roots: Helper[] = [];
|
|
98
|
+
for (const h of hs)
|
|
99
|
+
if (h.parentId && h.parentId !== h.id && ids.has(h.parentId))
|
|
100
|
+
kids.set(h.parentId, [...(kids.get(h.parentId) ?? []), h]);
|
|
101
|
+
else roots.push(h);
|
|
102
|
+
// Running counts for the whole branch: a done helper with a running
|
|
103
|
+
// helper under it sorts with the running ones (0.4.28-dev.1's gate).
|
|
104
|
+
const busy = new Map<string, boolean>();
|
|
105
|
+
const runs = (h: Helper, path = new Set<string>()): boolean => {
|
|
106
|
+
const known = busy.get(h.id);
|
|
107
|
+
if (known !== undefined) return known;
|
|
108
|
+
if (path.has(h.id)) return h.status === "running";
|
|
109
|
+
path.add(h.id);
|
|
110
|
+
const r =
|
|
111
|
+
h.status === "running" ||
|
|
112
|
+
(kids.get(h.id) ?? []).some((k) => runs(k, path));
|
|
113
|
+
path.delete(h.id);
|
|
114
|
+
busy.set(h.id, r);
|
|
115
|
+
return r;
|
|
116
|
+
};
|
|
117
|
+
const live = (h: Helper) => (runs(h) ? 0 : 1);
|
|
118
|
+
const order = (a: Helper, b: Helper) =>
|
|
119
|
+
live(a) - live(b) ||
|
|
120
|
+
(b.startedAt ?? b.lastActivityAt ?? 0) -
|
|
121
|
+
(a.startedAt ?? a.lastActivityAt ?? 0) ||
|
|
122
|
+
a.id.localeCompare(b.id);
|
|
123
|
+
const out: Helper[] = [];
|
|
124
|
+
const seen = new Set<string>();
|
|
125
|
+
const walk = (h: Helper) => {
|
|
126
|
+
if (seen.has(h.id)) return;
|
|
127
|
+
seen.add(h.id);
|
|
128
|
+
out.push(h);
|
|
129
|
+
for (const k of (kids.get(h.id) ?? []).sort(order)) walk(k);
|
|
130
|
+
};
|
|
131
|
+
for (const r of roots.sort(order)) walk(r);
|
|
132
|
+
// A loop of parents (never from Claude Code): still listed, flat.
|
|
133
|
+
for (const h of [...hs].sort(order)) if (!seen.has(h.id)) out.push(h);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* At most `n` of `list`, in its order: the running ones first to stay,
|
|
139
|
+
* then the rest (a running helper is never cut for a done one).
|
|
140
|
+
*/
|
|
141
|
+
export function capHelpers(list: readonly Helper[], n: number): Helper[] {
|
|
142
|
+
const kept = new Set(
|
|
143
|
+
[
|
|
144
|
+
...list.filter((h) => h.status === "running"),
|
|
145
|
+
...list.filter((h) => h.status !== "running"),
|
|
146
|
+
].slice(0, Math.max(0, n)),
|
|
147
|
+
);
|
|
148
|
+
return list.filter((h) => kept.has(h));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* How far each helper of `list` (in sortHelpers' order) is indented: one
|
|
153
|
+
* step under a parent shown above it, none when its parent isn't in the
|
|
154
|
+
* list. The page's twin is agent-helpers.js shownDepths.
|
|
155
|
+
*/
|
|
156
|
+
export function shownDepths(list: readonly Helper[]): Map<string, number> {
|
|
157
|
+
const at = new Map<string, number>();
|
|
158
|
+
for (const h of list)
|
|
159
|
+
at.set(
|
|
160
|
+
h.id,
|
|
161
|
+
h.parentId && at.has(h.parentId) ? at.get(h.parentId)! + 1 : 0,
|
|
162
|
+
);
|
|
163
|
+
return at;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** {running, total}: what a badge shows. */
|
|
167
|
+
export function helperCounts(hs: readonly Helper[]): {
|
|
168
|
+
running: number;
|
|
169
|
+
total: number;
|
|
170
|
+
} {
|
|
171
|
+
return {
|
|
172
|
+
running: hs.filter((h) => h.status === "running").length,
|
|
173
|
+
total: hs.length,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Built-ins register here exactly as a runtime plugin would (AG-8a):
|
|
8
8
|
* den-claude and generic.
|
|
9
9
|
*/
|
|
10
|
+
import type { Helper, HelperReadOptions, HelperTranscript } from "./helpers.js";
|
|
10
11
|
import * as fs from "fs";
|
|
11
12
|
import * as path from "path";
|
|
12
13
|
import {
|
|
@@ -252,6 +253,18 @@ export interface Runtime {
|
|
|
252
253
|
* throw; its catalog entry's `sessionName` says how stable its source is.
|
|
253
254
|
*/
|
|
254
255
|
sessionName?(rec: AgentRecord, env?: NodeJS.ProcessEnv): string | null;
|
|
256
|
+
/**
|
|
257
|
+
* D32: the helpers the agent started inside its own session (Claude
|
|
258
|
+
* Code's Agent and Workflow subagents), in every conversation it has had,
|
|
259
|
+
* running first. Read-only. None: the runtime can't tell. Never throws.
|
|
260
|
+
*/
|
|
261
|
+
helpers?(rec: AgentRecord, opts?: HelperReadOptions): Helper[];
|
|
262
|
+
/** D32: one helper's transcript, tool calls summarized; null if none. Never throws. */
|
|
263
|
+
helperTranscript?(
|
|
264
|
+
rec: AgentRecord,
|
|
265
|
+
id: string,
|
|
266
|
+
opts?: HelperReadOptions,
|
|
267
|
+
): HelperTranscript | null;
|
|
255
268
|
/** Where permission prompts surface (Q-AG-2). */
|
|
256
269
|
approvals?: "terminal" | "hook";
|
|
257
270
|
/**
|
package/src/shared/den-config.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* The user file can toggle local_dev on/off without touching the shared config.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { HELPER_DEFAULTS, type HelperSettings } from "./agents/helpers.js";
|
|
11
12
|
import * as fs from "fs";
|
|
12
13
|
import * as os from "os";
|
|
13
14
|
import * as path from "path";
|
|
@@ -916,6 +917,13 @@ export interface AgentsConfig {
|
|
|
916
917
|
* false` keeps standing personas from rotating (default true).
|
|
917
918
|
*/
|
|
918
919
|
rotate?: { after_compactions?: unknown; standing?: unknown };
|
|
920
|
+
/**
|
|
921
|
+
* D32, the helpers an agent starts in its own session:
|
|
922
|
+
* `{ active_minutes, collapse_done_minutes }`. A helper with no activity
|
|
923
|
+
* for `active_minutes` (default 10) and not known done shows idle; done
|
|
924
|
+
* ones fold away after `collapse_done_minutes` (default 30).
|
|
925
|
+
*/
|
|
926
|
+
helpers?: { active_minutes?: unknown; collapse_done_minutes?: unknown };
|
|
919
927
|
/**
|
|
920
928
|
* Any other key is an agent id: `agents.<id>.grants` widens that agent's
|
|
921
929
|
* MCP scope to other workspaces (AG spec §5b). Config only: an agent
|
|
@@ -940,6 +948,8 @@ export const AGENTS_RESERVED_KEYS = [
|
|
|
940
948
|
"runtime_catalog",
|
|
941
949
|
"default_pack",
|
|
942
950
|
"rotate",
|
|
951
|
+
// D32: how helpers show.
|
|
952
|
+
"helpers",
|
|
943
953
|
// AG-10.5, AG-11.1: the distribution's persona categories and roles.
|
|
944
954
|
"categories",
|
|
945
955
|
"roles",
|
|
@@ -968,12 +978,42 @@ export interface ResolvedAgentsConfig {
|
|
|
968
978
|
rotate?: RotateSettings;
|
|
969
979
|
/** D28: agents.rotate as written, the org scope. */
|
|
970
980
|
rotateOrg?: OrgRotate;
|
|
981
|
+
/** agents.helpers (D32) with its defaults. */
|
|
982
|
+
helpers?: HelperSettings;
|
|
971
983
|
/** agents.categories (AG-10.5): the distribution's, after Den's. */
|
|
972
984
|
categories?: Category[];
|
|
973
985
|
/** agents.roles (AG-11.1): new roles and Den roles' preset extensions. */
|
|
974
986
|
roles?: RolesConfig;
|
|
975
987
|
}
|
|
976
988
|
|
|
989
|
+
/** `agents.helpers` (D32) checked; a bad key or value throws naming it. */
|
|
990
|
+
export function helpersBlock(v: unknown): HelperSettings {
|
|
991
|
+
const out = { ...HELPER_DEFAULTS };
|
|
992
|
+
if (v === undefined || v === null) return out;
|
|
993
|
+
if (!isMapping(v))
|
|
994
|
+
throw new Error(
|
|
995
|
+
`agents.helpers: must be a mapping of active_minutes, collapse_done_minutes (got ${JSON.stringify(v)})`,
|
|
996
|
+
);
|
|
997
|
+
for (const [k, x] of Object.entries(v)) {
|
|
998
|
+
const key =
|
|
999
|
+
k === "active_minutes"
|
|
1000
|
+
? "activeMinutes"
|
|
1001
|
+
: k === "collapse_done_minutes"
|
|
1002
|
+
? "collapseDoneMinutes"
|
|
1003
|
+
: null;
|
|
1004
|
+
if (!key)
|
|
1005
|
+
throw new Error(
|
|
1006
|
+
`agents.helpers.${k}: unknown key (agents.helpers takes active_minutes, collapse_done_minutes)`,
|
|
1007
|
+
);
|
|
1008
|
+
if (typeof x !== "number" || !Number.isFinite(x) || x <= 0 || x > 100_000)
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`agents.helpers.${k}: must be a number of minutes, above 0 and at most 100000 (got ${JSON.stringify(x)})`,
|
|
1011
|
+
);
|
|
1012
|
+
out[key] = x;
|
|
1013
|
+
}
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
977
1017
|
/** `agents.rotate` checked; any other key, or a bad value, throws naming it. */
|
|
978
1018
|
/**
|
|
979
1019
|
* `agents.rotate` checked, both ways: den#4's settings (afterCompactions 0
|
|
@@ -1077,6 +1117,7 @@ export function readAgentsConfig(
|
|
|
1077
1117
|
return { rotate: r.settings, rotateOrg: r.org };
|
|
1078
1118
|
})(),
|
|
1079
1119
|
};
|
|
1120
|
+
out.helpers = helpersBlock(raw.helpers);
|
|
1080
1121
|
out.categories = checkCategoriesConfig(raw.categories);
|
|
1081
1122
|
out.roles = checkRolesConfig(raw.roles, [
|
|
1082
1123
|
...DEN_CATEGORIES.map((c) => c.id),
|
|
@@ -1114,7 +1155,7 @@ export function readAgentsConfig(
|
|
|
1114
1155
|
const where = `agents.${id}`;
|
|
1115
1156
|
if (!AGENT_KEY_RE.test(id) || !isMapping(block))
|
|
1116
1157
|
throw new Error(
|
|
1117
|
-
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, rotate, categories, roles, ` +
|
|
1158
|
+
`${where}: unknown key (agents: takes max_concurrent, persona_packs, runtime_catalog, default_pack, rotate, helpers, categories, roles, ` +
|
|
1118
1159
|
`or an agent id mapping to { grants: [...] })`,
|
|
1119
1160
|
);
|
|
1120
1161
|
for (const k of Object.keys(block))
|
package/src/shared/hub.ts
CHANGED
|
@@ -243,6 +243,8 @@ export interface HubWhoami {
|
|
|
243
243
|
company?: { slug?: string; name?: string; url?: string };
|
|
244
244
|
/** Hub's MCP endpoint; opaque. */
|
|
245
245
|
mcpUrl?: string;
|
|
246
|
+
/** Threads on Hub waiting on the person (the rail's badge). */
|
|
247
|
+
unread?: number;
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
/**
|
|
@@ -287,6 +289,8 @@ export type HubStatus =
|
|
|
287
289
|
label?: string;
|
|
288
290
|
role?: string;
|
|
289
291
|
login?: string;
|
|
292
|
+
/** Hub threads waiting on the person (whoami's unread). */
|
|
293
|
+
unread?: number;
|
|
290
294
|
/** Why it isn't connected, in words (never the token). */
|
|
291
295
|
problem?: string;
|
|
292
296
|
};
|
|
@@ -379,6 +383,11 @@ export async function hubWhoami(
|
|
|
379
383
|
...(str(b.login) ? { login: str(b.login)! } : {}),
|
|
380
384
|
...(Object.keys(company).length ? { company } : {}),
|
|
381
385
|
...(str(b.mcp_url) ? { mcpUrl: str(b.mcp_url)! } : {}),
|
|
386
|
+
...(typeof b.unread === "number" &&
|
|
387
|
+
Number.isInteger(b.unread) &&
|
|
388
|
+
b.unread >= 0
|
|
389
|
+
? { unread: Math.min(b.unread, 1_000_000) }
|
|
390
|
+
: {}),
|
|
382
391
|
};
|
|
383
392
|
}
|
|
384
393
|
|
|
@@ -542,6 +551,7 @@ export async function hubStatus(
|
|
|
542
551
|
...(who.install ? { label: who.install } : {}),
|
|
543
552
|
...(who.role ? { role: who.role } : {}),
|
|
544
553
|
...(who.login ? { login: who.login } : {}),
|
|
554
|
+
...(who.unread !== undefined ? { unread: who.unread } : {}),
|
|
545
555
|
};
|
|
546
556
|
} catch (e) {
|
|
547
557
|
const kind = e instanceof HubCallError ? e.kind : "unreachable";
|
package/src/ui/cli/agent-ops.ts
CHANGED
|
@@ -50,6 +50,15 @@
|
|
|
50
50
|
* Nothing here takes a grant: grants come from the org config only. The
|
|
51
51
|
* host enforces agents.max_concurrent on every start, counting agents only.
|
|
52
52
|
*/
|
|
53
|
+
import {
|
|
54
|
+
capHelpers,
|
|
55
|
+
HELPER_DEFAULTS,
|
|
56
|
+
HELPERS_READ_ONLY_NOTE,
|
|
57
|
+
helperCounts,
|
|
58
|
+
type Helper,
|
|
59
|
+
type HelperSettings,
|
|
60
|
+
type HelperTranscript,
|
|
61
|
+
} from "../../shared/agents/helpers.js";
|
|
53
62
|
import * as fs from "fs";
|
|
54
63
|
import * as path from "path";
|
|
55
64
|
import {
|
|
@@ -638,6 +647,12 @@ export interface AgentRow {
|
|
|
638
647
|
/** D28: the last one (when, why, the old conversation, by itself or not). */
|
|
639
648
|
lastRotation?: RotationEntry;
|
|
640
649
|
session?: SessionInfo;
|
|
650
|
+
/**
|
|
651
|
+
* D32: the helpers it started in its own session: how many, how many
|
|
652
|
+
* run now, and the ones worth showing under it (running, or ended
|
|
653
|
+
* within agents.helpers.collapse_done_minutes; at most 8).
|
|
654
|
+
*/
|
|
655
|
+
helpers?: { running: number; total: number; recent: Helper[] };
|
|
641
656
|
}
|
|
642
657
|
|
|
643
658
|
/** Its state as a row shows it (AG-14.1: stopped, exited or interrupted). */
|
|
@@ -831,9 +846,138 @@ export async function listAgents(
|
|
|
831
846
|
const records = listRecords(d.stateDir()).filter((r) => r.workspace === ws);
|
|
832
847
|
noteEnded(d.stateDir(), sessions, records);
|
|
833
848
|
const rows = agentRows(sessions, records, d.stateDir(), policyResolver(d));
|
|
849
|
+
addHelperCounts(d, rows, records);
|
|
834
850
|
return opts.stopped ? rows.filter((r) => isStoppedState(r.state)) : rows;
|
|
835
851
|
}
|
|
836
852
|
|
|
853
|
+
/** At most this many helpers ride along on an agent's row. */
|
|
854
|
+
const ROW_HELPERS = 8;
|
|
855
|
+
|
|
856
|
+
/** D32: agents.helpers, or its defaults when the config can't be read. */
|
|
857
|
+
function helperSettings(d: AgentsResolved): HelperSettings {
|
|
858
|
+
try {
|
|
859
|
+
return d.agentsConfig().helpers ?? HELPER_DEFAULTS;
|
|
860
|
+
} catch {
|
|
861
|
+
return HELPER_DEFAULTS;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** D32: each row's helper counts and recent helpers (never fails a list). */
|
|
866
|
+
function addHelperCounts(
|
|
867
|
+
d: AgentsResolved,
|
|
868
|
+
rows: AgentRow[],
|
|
869
|
+
records: AgentRecord[],
|
|
870
|
+
): void {
|
|
871
|
+
const settings = helperSettings(d);
|
|
872
|
+
const byId = new Map(records.map((r) => [r.id, r]));
|
|
873
|
+
for (const row of rows) {
|
|
874
|
+
const rec = byId.get(row.agent);
|
|
875
|
+
if (!rec) continue;
|
|
876
|
+
const h = rowHelpers(rec, !isStoppedState(row.state), settings, d.env);
|
|
877
|
+
if (h) row.helpers = h;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* D32: what an agent's row shows of its helpers: counts, and the ones
|
|
883
|
+
* worth showing (running, or this conversation's ended within
|
|
884
|
+
* collapse_done_minutes; at most 8). Undefined: none, or its runtime
|
|
885
|
+
* can't tell. Never throws.
|
|
886
|
+
*/
|
|
887
|
+
export function rowHelpers(
|
|
888
|
+
rec: AgentRecord,
|
|
889
|
+
live: boolean,
|
|
890
|
+
settings: HelperSettings = HELPER_DEFAULTS,
|
|
891
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
892
|
+
now = Date.now(),
|
|
893
|
+
): AgentRow["helpers"] {
|
|
894
|
+
const rt = getRuntime(rec.runtime);
|
|
895
|
+
if (!rt?.helpers) return undefined;
|
|
896
|
+
let hs: Helper[] = [];
|
|
897
|
+
try {
|
|
898
|
+
hs = rt.helpers(rec, { env, now, settings, agentLive: live });
|
|
899
|
+
} catch {
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
if (!hs.length) return undefined;
|
|
903
|
+
const keep = settings.collapseDoneMinutes * 60_000;
|
|
904
|
+
const shown = hs.filter(
|
|
905
|
+
(h) =>
|
|
906
|
+
h.status === "running" ||
|
|
907
|
+
(!h.earlier && now - (h.lastActivityAt ?? h.startedAt ?? 0) <= keep),
|
|
908
|
+
);
|
|
909
|
+
return { ...helperCounts(hs), recent: capHelpers(shown, ROW_HELPERS) };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/** D32: an agent's helpers, read-only, and whether its runtime can tell. */
|
|
913
|
+
export async function agentHelpers(
|
|
914
|
+
d: AgentsResolved,
|
|
915
|
+
id: string,
|
|
916
|
+
ws?: string,
|
|
917
|
+
): Promise<{
|
|
918
|
+
rec: AgentRecord;
|
|
919
|
+
supported: boolean;
|
|
920
|
+
live: boolean;
|
|
921
|
+
helpers: Helper[];
|
|
922
|
+
note: string;
|
|
923
|
+
}> {
|
|
924
|
+
const rec = agentRecord(d, id, ws);
|
|
925
|
+
const rt = getRuntime(rec.runtime);
|
|
926
|
+
const live = await agentLive(d, rec);
|
|
927
|
+
const helpers =
|
|
928
|
+
rt?.helpers?.(rec, {
|
|
929
|
+
env: d.env,
|
|
930
|
+
settings: helperSettings(d),
|
|
931
|
+
agentLive: live,
|
|
932
|
+
}) ?? [];
|
|
933
|
+
return {
|
|
934
|
+
rec,
|
|
935
|
+
supported: !!rt?.helpers,
|
|
936
|
+
live,
|
|
937
|
+
helpers,
|
|
938
|
+
note: HELPERS_READ_ONLY_NOTE,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** D32: one of agent `id`'s helpers' transcript; a 404 when there's none. */
|
|
943
|
+
export async function agentHelperTranscript(
|
|
944
|
+
d: AgentsResolved,
|
|
945
|
+
id: string,
|
|
946
|
+
helperId: string,
|
|
947
|
+
ws?: string,
|
|
948
|
+
limit?: number,
|
|
949
|
+
): Promise<HelperTranscript & { note: string }> {
|
|
950
|
+
const rec = agentRecord(d, id, ws);
|
|
951
|
+
const rt = getRuntime(rec.runtime);
|
|
952
|
+
const t = rt?.helperTranscript?.(rec, helperId, {
|
|
953
|
+
env: d.env,
|
|
954
|
+
settings: helperSettings(d),
|
|
955
|
+
agentLive: await agentLive(d, rec),
|
|
956
|
+
...(limit ? { limit } : {}),
|
|
957
|
+
});
|
|
958
|
+
if (!t) throw refused(`agent ${id} has no helper "${helperId}"`, [], 404);
|
|
959
|
+
return { ...t, note: HELPERS_READ_ONLY_NOTE };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** Whether agent `rec`'s session runs now (an unreachable host: no). */
|
|
963
|
+
async function agentLive(
|
|
964
|
+
d: AgentsResolved,
|
|
965
|
+
rec: AgentRecord,
|
|
966
|
+
): Promise<boolean> {
|
|
967
|
+
try {
|
|
968
|
+
const c = await d.connectExisting();
|
|
969
|
+
if (!c) return false;
|
|
970
|
+
try {
|
|
971
|
+
const s = sessionOf(await c.list(), rec);
|
|
972
|
+
return Boolean(s && !s.exited);
|
|
973
|
+
} finally {
|
|
974
|
+
c.close();
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
837
981
|
// ── personas, runtimes ──────────────────────────────────────────────────────
|
|
838
982
|
|
|
839
983
|
/** The agents config; an invalid one is a refusal naming the key. */
|
|
@@ -70,6 +70,7 @@ import { HostError } from "../../shared/sessions/client.js";
|
|
|
70
70
|
import { checkHandback } from "../../shared/agents/handback.js";
|
|
71
71
|
import { checkHandoff } from "../../shared/agents/handoff.js";
|
|
72
72
|
import { handoffFile, reasonText } from "../../shared/agents/rotation.js";
|
|
73
|
+
import { shownDepths } from "../../shared/agents/helpers.js";
|
|
73
74
|
import {
|
|
74
75
|
parseRotateValue,
|
|
75
76
|
type OverrideScope,
|
|
@@ -84,6 +85,8 @@ import {
|
|
|
84
85
|
importAgent,
|
|
85
86
|
changePersona,
|
|
86
87
|
listAgents,
|
|
88
|
+
agentHelpers,
|
|
89
|
+
agentHelperTranscript,
|
|
87
90
|
listConversations,
|
|
88
91
|
listPersonas as personaList,
|
|
89
92
|
listRuntimes,
|
|
@@ -129,7 +132,7 @@ export {
|
|
|
129
132
|
|
|
130
133
|
export const AGENTS_HELP = {
|
|
131
134
|
usage:
|
|
132
|
-
"agents <list|start|stop|resume|relaunch|rotate|keep|handoff|import|persona|rename|rm|personas|doctor|handback-check|handoff-check> ...",
|
|
135
|
+
"agents <list|start|stop|resume|relaunch|rotate|keep|handoff|import|persona|helpers|rename|rm|personas|doctor|handback-check|handoff-check> ...",
|
|
133
136
|
lines: [
|
|
134
137
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
135
138
|
"outlives the web server (see `{cli} sessions`).",
|
|
@@ -167,6 +170,13 @@ export const AGENTS_HELP = {
|
|
|
167
170
|
" agent never rotates. A person's only",
|
|
168
171
|
"{cli} agents handoff <id> [-w <ws>]",
|
|
169
172
|
" ask a running agent, at idle, to write its handoff",
|
|
173
|
+
"{cli} agents helpers <id> [-w <ws>] [--all] [--json]",
|
|
174
|
+
" the helpers it started in its own session (Claude Code's Agent and",
|
|
175
|
+
" Workflow subagents): what each does, its type, model, branch, state and",
|
|
176
|
+
" times; running first. --all: its earlier conversations' too. Read-only:",
|
|
177
|
+
" they belong to the agent's session, so Den can't stop or steer them",
|
|
178
|
+
"{cli} agents helpers <id> <helper> [-w <ws>] [--limit <n>] [--json]",
|
|
179
|
+
" one helper's transcript, tool calls summarized (the last 500 lines)",
|
|
170
180
|
"{cli} agents import -w <ws> [--runtime <r>] [--json]",
|
|
171
181
|
"{cli} agents import -w <ws> --session <id> --persona <p> [--runtime <r>] [--id <id>] [--model <m>]",
|
|
172
182
|
" bring in a conversation started outside Den: the first lists the runtime's",
|
|
@@ -456,6 +466,120 @@ async function persona(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
456
466
|
}
|
|
457
467
|
|
|
458
468
|
/** `<id> [-w <ws>]`. */
|
|
469
|
+
/** D32: `agents helpers <id> [<helper>]`, read-only. */
|
|
470
|
+
async function helpers(args: string[], d: AgentsResolved): Promise<number> {
|
|
471
|
+
const where = "agents helpers";
|
|
472
|
+
const p = parseArgs(where, args, {
|
|
473
|
+
"--workspace": true,
|
|
474
|
+
"--json": false,
|
|
475
|
+
"--all": false,
|
|
476
|
+
"--limit": true,
|
|
477
|
+
});
|
|
478
|
+
noExtra(where, p, 2);
|
|
479
|
+
const [id, helperId] = p.positional;
|
|
480
|
+
if (!id) throw new UsageError(`${where} needs <id>`);
|
|
481
|
+
const ws = workspaceFlag(where, p);
|
|
482
|
+
const ago = (ms?: number) =>
|
|
483
|
+
ms === undefined ? "-" : `${duration(Date.now() - ms)} ago`;
|
|
484
|
+
if (helperId) {
|
|
485
|
+
const raw = p.flags["--limit"];
|
|
486
|
+
const limit = raw === undefined ? undefined : Number(raw);
|
|
487
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
|
|
488
|
+
throw new UsageError(
|
|
489
|
+
`${where}: --limit must be a whole number of at least 1`,
|
|
490
|
+
);
|
|
491
|
+
const t = await agentHelperTranscript(d, id, helperId, ws, limit);
|
|
492
|
+
if (p.flags["--json"]) {
|
|
493
|
+
d.out(JSON.stringify(t, null, 2));
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
d.out(
|
|
497
|
+
`${t.helper.description ?? t.helper.id} (${t.helper.status}, ${t.helper.type ?? "helper"})`,
|
|
498
|
+
);
|
|
499
|
+
if (t.omitted) d.out(`… ${t.omitted} earlier lines`);
|
|
500
|
+
for (const i of t.items) {
|
|
501
|
+
const lead =
|
|
502
|
+
i.kind === "tool"
|
|
503
|
+
? `→ ${i.tool}: `
|
|
504
|
+
: i.kind === "result"
|
|
505
|
+
? i.error
|
|
506
|
+
? " ✗ "
|
|
507
|
+
: " ← "
|
|
508
|
+
: i.kind === "prompt"
|
|
509
|
+
? "asked: "
|
|
510
|
+
: i.kind === "note"
|
|
511
|
+
? "… "
|
|
512
|
+
: "";
|
|
513
|
+
d.out(`${lead}${i.text}`);
|
|
514
|
+
}
|
|
515
|
+
d.out("");
|
|
516
|
+
d.out(t.note);
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
const r = await agentHelpers(d, id, ws);
|
|
520
|
+
const shown = p.flags["--all"]
|
|
521
|
+
? r.helpers
|
|
522
|
+
: r.helpers.filter((h) => !h.earlier);
|
|
523
|
+
if (p.flags["--json"]) {
|
|
524
|
+
d.out(
|
|
525
|
+
JSON.stringify(
|
|
526
|
+
{
|
|
527
|
+
agent: r.rec.id,
|
|
528
|
+
supported: r.supported,
|
|
529
|
+
live: r.live,
|
|
530
|
+
helpers: shown,
|
|
531
|
+
note: r.note,
|
|
532
|
+
},
|
|
533
|
+
null,
|
|
534
|
+
2,
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
if (!r.supported) {
|
|
540
|
+
d.out(`${r.rec.runtime} can't tell which helpers ${r.rec.id} started`);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
const earlier = r.helpers.length - r.helpers.filter((h) => !h.earlier).length;
|
|
544
|
+
if (!shown.length) {
|
|
545
|
+
d.out(
|
|
546
|
+
`${r.rec.id} has started no helpers${earlier && !p.flags["--all"] ? ` in this conversation (${earlier} earlier: --all)` : ""}`,
|
|
547
|
+
);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
const depth = shownDepths(shown);
|
|
551
|
+
for (const l of columns([
|
|
552
|
+
["HELPER", "WHAT", "TYPE", "MODEL", "STATE", "STARTED", "FOR", "BRANCH"],
|
|
553
|
+
...shown.map((h) => [
|
|
554
|
+
`${" ".repeat(depth.get(h.id) ?? 0)}${h.id}`,
|
|
555
|
+
(h.description ?? "-").slice(0, 48),
|
|
556
|
+
h.workflow
|
|
557
|
+
? `${h.type ?? "-"} (${h.workflow.name ?? "workflow"})`
|
|
558
|
+
: (h.type ?? "-"),
|
|
559
|
+
h.model ?? "-",
|
|
560
|
+
`${h.status}${h.earlier ? " (earlier)" : ""}`,
|
|
561
|
+
ago(h.startedAt),
|
|
562
|
+
h.durationMs === undefined ? "-" : duration(h.durationMs),
|
|
563
|
+
h.branch ?? "-",
|
|
564
|
+
]),
|
|
565
|
+
]))
|
|
566
|
+
d.out(l);
|
|
567
|
+
if (earlier && !p.flags["--all"])
|
|
568
|
+
d.out(`${earlier} more from earlier conversations: --all`);
|
|
569
|
+
d.out("");
|
|
570
|
+
d.out(r.note);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** 65 s → "1m 5s"; 2 h → "2h 0m". */
|
|
575
|
+
function duration(ms: number): string {
|
|
576
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
577
|
+
if (s < 60) return `${s}s`;
|
|
578
|
+
const m = Math.floor(s / 60);
|
|
579
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
580
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
581
|
+
}
|
|
582
|
+
|
|
459
583
|
function idAndWorkspace(
|
|
460
584
|
where: string,
|
|
461
585
|
args: string[],
|
|
@@ -962,6 +1086,8 @@ export async function cmdAgents(
|
|
|
962
1086
|
case "persona":
|
|
963
1087
|
await loadAgentWorkspaces(deps);
|
|
964
1088
|
return await persona(rest, d);
|
|
1089
|
+
case "helpers":
|
|
1090
|
+
return await helpers(rest, d);
|
|
965
1091
|
case "rename":
|
|
966
1092
|
return rename(rest, d);
|
|
967
1093
|
case "rm":
|
package/src/ui/mcp/api.ts
CHANGED
|
@@ -147,8 +147,9 @@ const ACTION_ROUTES: Record<string, string> = {
|
|
|
147
147
|
};
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
150
|
+
* Routes with a parameter: an agent's resume (MCP agent_resume), Bring
|
|
151
|
+
* in's list and import (MCP agent_import, AG-17.1), and an agent's helpers
|
|
152
|
+
* (MCP agent_helpers, D32, GET only).
|
|
152
153
|
*/
|
|
153
154
|
const ACTION_ROUTE_PATTERNS: { re: RegExp; method: string }[] = [
|
|
154
155
|
// One held request's state (its own, the server checks). Never /decide.
|
|
@@ -161,6 +162,11 @@ const ACTION_ROUTE_PATTERNS: { re: RegExp; method: string }[] = [
|
|
|
161
162
|
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/conversations$/,
|
|
162
163
|
method: "GET",
|
|
163
164
|
},
|
|
165
|
+
// D32 (MCP agent_helpers): an agent's helpers, or one's transcript. Reads.
|
|
166
|
+
{
|
|
167
|
+
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/agents\/[A-Za-z0-9._-]{1,64}\/helpers(?:\/[A-Za-z0-9_-]{1,128})?$/,
|
|
168
|
+
method: "GET",
|
|
169
|
+
},
|
|
164
170
|
{
|
|
165
171
|
re: /^\/api\/workspaces\/[A-Za-z0-9._-]{1,64}\/conversations\/[A-Za-z0-9][A-Za-z0-9_-]{0,127}\/import$/,
|
|
166
172
|
method: "POST",
|