@monster-orange/den 0.4.8 → 0.4.9
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 +80 -0
- package/README.md +7 -7
- package/docs/distributions.md +124 -1
- package/package.json +1 -1
- package/src/shared/agents/den-claude.ts +38 -3
- package/src/shared/agents/guardrails/hooks.json +23 -0
- package/src/shared/agents/guardrails/instructions.md +23 -0
- package/src/shared/agents/guardrails/rotation-hook.ts +127 -0
- package/src/shared/agents/guardrails/stop-hook.ts +48 -2
- package/src/shared/agents/guardrails.ts +38 -7
- package/src/shared/agents/handoff.ts +118 -0
- package/src/shared/agents/records.ts +8 -1
- package/src/shared/agents/rotation.ts +362 -0
- package/src/shared/agents/runtime.ts +15 -0
- package/src/shared/den-config.ts +42 -1
- package/src/shared/git/gh-pr-state.ts +32 -0
- package/src/shared/usage/report.ts +98 -4
- package/src/shared/usage/transcript.ts +62 -5
- package/src/ui/cli/agent-ops.ts +196 -7
- package/src/ui/cli/agents-command.ts +75 -11
- package/src/ui/cli/usage-command.ts +77 -10
- package/src/ui/server/agent-routes.ts +19 -2
- package/src/ui/server/public/agents-page.js +78 -13
- package/src/ui/server/public/i18n/es.js +12 -0
- package/src/ui/server/public/i18n/ja.js +11 -0
- package/src/ui/server/public/shell.js +13 -0
- package/src/ui/server/route-classes.ts +6 -0
- package/src/ui/server/session-routes.ts +8 -1
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* POST /api/workspaces/<ws>/agents/<id>/resume the same conversation (AG-14)
|
|
19
19
|
* POST /api/workspaces/<ws>/agents/<id>/fresh a new conversation, same
|
|
20
20
|
* persona and workspace
|
|
21
|
+
* POST /api/workspaces/<ws>/agents/<id>/relaunch a rotated agent (den#4):
|
|
22
|
+
* a new conversation whose
|
|
23
|
+
* first prompt is its
|
|
24
|
+
* handoff.md; a resume of
|
|
25
|
+
* one is refused (409 rotated)
|
|
21
26
|
* POST /api/workspaces/<ws>/agents/<id>/remove forget a stopped one (Undo:
|
|
22
27
|
* /api/agents/restore)
|
|
23
28
|
* POST /api/agents/clear-stopped { workspaces: [...] }: forget
|
|
@@ -74,6 +79,7 @@ import {
|
|
|
74
79
|
resumeInterrupted,
|
|
75
80
|
setRuntimePath,
|
|
76
81
|
resolveAgentsDeps,
|
|
82
|
+
relaunchAgent,
|
|
77
83
|
resumeAgent,
|
|
78
84
|
startAgent,
|
|
79
85
|
startFresh,
|
|
@@ -98,7 +104,7 @@ export interface AgentRouteDeps {
|
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
const AGENTS_PATH =
|
|
101
|
-
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|remove))?$/;
|
|
107
|
+
/^\/api\/workspaces\/([^/]+)\/agents(?:\/([^/]+)\/(stop|resume|fresh|relaunch|remove))?$/;
|
|
102
108
|
|
|
103
109
|
const RUNTIME_PATH = /^\/api\/agents\/runtimes\/([^/]+)\/path$/;
|
|
104
110
|
/** A start's body is a few short names. */
|
|
@@ -282,7 +288,13 @@ export async function handleAgentRoutes(
|
|
|
282
288
|
return true;
|
|
283
289
|
}
|
|
284
290
|
const id = m[2] === undefined ? undefined : safeDecode(m[2]);
|
|
285
|
-
const action = m[3] as
|
|
291
|
+
const action = m[3] as
|
|
292
|
+
| "stop"
|
|
293
|
+
| "resume"
|
|
294
|
+
| "fresh"
|
|
295
|
+
| "relaunch"
|
|
296
|
+
| "remove"
|
|
297
|
+
| undefined;
|
|
286
298
|
|
|
287
299
|
if (!action && method === "GET") {
|
|
288
300
|
try {
|
|
@@ -364,6 +376,11 @@ export async function handleAgentRoutes(
|
|
|
364
376
|
what = `start ${s.rec.id} fresh (${s.rec.persona}, ${s.rec.runtime}) in ${ws}`;
|
|
365
377
|
audit(what, s.early ? "exited at once" : undefined);
|
|
366
378
|
sendStarted(res, s);
|
|
379
|
+
} else if (verb === "relaunch") {
|
|
380
|
+
const s = await relaunchAgent(d, id!, ws);
|
|
381
|
+
what = `relaunch ${s.rec.id} from its handoff (${s.why}) in ${ws}`;
|
|
382
|
+
audit(what, s.early ? "exited at once" : undefined);
|
|
383
|
+
sendStarted(res, s);
|
|
367
384
|
} else if (verb === "remove") {
|
|
368
385
|
const removed = await removeAgent(d, id!, ws);
|
|
369
386
|
audit(what);
|
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
* shows only Start fresh, saying so. Clear stopped removes every stopped
|
|
41
41
|
* agent in view. After a restart, one line says how many were interrupted,
|
|
42
42
|
* with Resume all and Review.
|
|
43
|
+
*
|
|
44
|
+
* Session rotation (den#4): an agent whose session rotated (after
|
|
45
|
+
* agents.rotate.after_compactions compactions, or its pull request merged)
|
|
46
|
+
* and whose handoff.md passes the check reads "Ready to relaunch (3
|
|
47
|
+
* compactions)" or "(PR merged)", with Relaunch from handoff as its primary
|
|
48
|
+
* action, then Start fresh and Remove; Resume is not offered. With the
|
|
49
|
+
* handoff missing or failing the check, the row says so and keeps Resume
|
|
50
|
+
* and Start fresh. A running one says "Handoff due" or "Ready to relaunch"
|
|
51
|
+
* as a tag.
|
|
43
52
|
*/
|
|
44
53
|
import { fillStartForm } from "/agent-start.js";
|
|
45
54
|
|
|
@@ -181,6 +190,23 @@ export function stopReason(r) {
|
|
|
181
190
|
return stateText(r.state);
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
/** Why it rotates, in words: "3 compactions", "PR merged" (den#4). */
|
|
194
|
+
function rotationWhy(rot) {
|
|
195
|
+
if (rot.due === "pr-merged") return t("ag.rot.merged");
|
|
196
|
+
return t(
|
|
197
|
+
rot.compactions === 1 ? "ag.rot.compactions.one" : "ag.rot.compactions",
|
|
198
|
+
{
|
|
199
|
+
n: rot.compactions,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Its rotation, when one is due: "ready" or "due" (den#4). */
|
|
205
|
+
function rotationOf(r) {
|
|
206
|
+
if (!r.rotation?.due) return "";
|
|
207
|
+
return r.rotation.ready ? "ready" : "due";
|
|
208
|
+
}
|
|
209
|
+
|
|
184
210
|
/** A stopped agent's row: its button (it opens an ended pane) and actions. */
|
|
185
211
|
function stoppedHtml(r) {
|
|
186
212
|
const open = st.open.includes(r.id);
|
|
@@ -188,26 +214,47 @@ function stoppedHtml(r) {
|
|
|
188
214
|
const nm = `${esc(name)}${r.persona && r.persona !== r.label ? ` <span class="id">${esc(r.label)}</span>` : ""}`;
|
|
189
215
|
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
190
216
|
const who = esc(r.agent);
|
|
217
|
+
const rot = rotationOf(r);
|
|
218
|
+
const relaunch =
|
|
219
|
+
rot === "ready"
|
|
220
|
+
? `<button type="button" class="den-btn sm primary" data-act="relaunch" aria-label="${esc(t("ag.relaunchName", { id: r.agent }))}">${esc(t("ag.relaunch"))}</button>`
|
|
221
|
+
: "";
|
|
191
222
|
const resume = r.resumable
|
|
192
223
|
? `<button type="button" class="den-btn sm primary" data-act="resume" aria-label="${esc(t("ag.resumeName", { id: r.agent }))}">${esc(t("ag.resume"))}</button>`
|
|
193
224
|
: "";
|
|
194
225
|
const fresh = `<button type="button" class="den-btn sm" data-act="fresh" aria-label="${esc(t("ag.freshName", { id: r.agent }))}">${esc(t("ag.fresh"))}</button>`;
|
|
195
226
|
const remove = `<button type="button" class="den-btn sm" data-act="remove" aria-label="${esc(t("ag.removeName", { id: r.agent }))}">${esc(t("ag.remove"))}</button>`;
|
|
227
|
+
// A rotation due without a handoff that passes: say so (den#4).
|
|
228
|
+
const handoff =
|
|
229
|
+
rot === "due"
|
|
230
|
+
? `<span class="agcant" data-handoff="${esc(r.rotation.handoff)}">${esc(
|
|
231
|
+
t(
|
|
232
|
+
r.rotation.handoff === "missing"
|
|
233
|
+
? "ag.rot.noHandoff"
|
|
234
|
+
: "ag.rot.badHandoff",
|
|
235
|
+
{ why: rotationWhy(r.rotation) },
|
|
236
|
+
),
|
|
237
|
+
)}</span>`
|
|
238
|
+
: "";
|
|
196
239
|
const cant =
|
|
197
|
-
!r.resumable && r.noResume
|
|
240
|
+
!r.resumable && r.noResume && rot !== "ready"
|
|
198
241
|
? `<span class="agcant" data-no-resume>${esc(r.noResume.startsWith(`${r.runtime} `) ? t("ag.noResume", { runtime: r.runtime }) : r.noResume)}</span>`
|
|
199
242
|
: "";
|
|
243
|
+
const stText =
|
|
244
|
+
rot === "ready"
|
|
245
|
+
? t("ag.rot.ready", { why: rotationWhy(r.rotation) })
|
|
246
|
+
: stateText(r.state);
|
|
200
247
|
return (
|
|
201
|
-
`<div class="agst" data-agent="${who}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}">` +
|
|
248
|
+
`<div class="agst" data-agent="${who}" data-state="${esc(r.state)}" data-ws="${esc(r.workspace)}"${rot ? ` data-rotation="${rot}"` : ""}>` +
|
|
202
249
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="agent" data-state="${esc(r.state)}"` +
|
|
203
250
|
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
204
251
|
` aria-label="${esc(t("ag.openName", { id: `${r.workspace} / ${name}` }))}">` +
|
|
205
252
|
`<span class="den-dot ${stateTone(r)}" aria-hidden="true"></span>` +
|
|
206
|
-
`<span class="nm">${nm}</span><span class="st">${esc(
|
|
253
|
+
`<span class="nm">${nm}</span><span class="st">${esc(stText)}</span>` +
|
|
207
254
|
(meta ? `<span class="meta">${esc(meta)}</span>` : "") +
|
|
208
255
|
`<span class="why">${esc(stopReason(r))}</span>` +
|
|
209
256
|
`</button>` +
|
|
210
|
-
`<div class="agacts">${cant}${resume}${fresh}${remove}</div>` +
|
|
257
|
+
`<div class="agacts">${handoff}${cant}${relaunch}${resume}${fresh}${remove}</div>` +
|
|
211
258
|
`</div>`
|
|
212
259
|
);
|
|
213
260
|
}
|
|
@@ -223,9 +270,21 @@ function rowHtml(r) {
|
|
|
223
270
|
? `<span class="wbdg">${esc(t("ag.waiting"))}</span>`
|
|
224
271
|
: `<span class="st">${esc(stateText(r.state))}</span>`;
|
|
225
272
|
const meta = [r.runtime, r.cwd].filter(Boolean).join(" · ");
|
|
226
|
-
const
|
|
227
|
-
|
|
273
|
+
const rot = rotationOf(r);
|
|
274
|
+
const rotTag = rot
|
|
275
|
+
? `<span class="tagx warn" data-rotation="${rot}">${esc(
|
|
276
|
+
t(rot === "ready" ? "ag.rot.ready" : "ag.rot.due", {
|
|
277
|
+
why: rotationWhy(r.rotation),
|
|
278
|
+
}),
|
|
279
|
+
)}</span>`
|
|
280
|
+
: "";
|
|
281
|
+
const revokedTag = r.tokenRevoked
|
|
282
|
+
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
228
283
|
: "";
|
|
284
|
+
const tags =
|
|
285
|
+
rotTag || revokedTag
|
|
286
|
+
? `<span class="agtags">${revokedTag}${rotTag}</span>`
|
|
287
|
+
: "";
|
|
229
288
|
return (
|
|
230
289
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="${esc(r.kind)}" data-state="${esc(r.state)}"` +
|
|
231
290
|
` aria-pressed="${open}"${r.attachable ? "" : ' aria-disabled="true"'}` +
|
|
@@ -760,18 +819,24 @@ async function stopAgent(p, button) {
|
|
|
760
819
|
const agentPath = (r, verb) =>
|
|
761
820
|
`/api/workspaces/${encodeURIComponent(r.workspace)}/agents/${encodeURIComponent(r.agent)}/${verb}`;
|
|
762
821
|
|
|
763
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* Resume (the same conversation), Start fresh, or Relaunch from handoff
|
|
824
|
+
* (den#4: a new conversation that starts from handoff.md); its pane opens
|
|
825
|
+
* on it.
|
|
826
|
+
*/
|
|
764
827
|
async function restartRow(r, verb, button) {
|
|
765
828
|
if (button) button.disabled = true;
|
|
766
829
|
try {
|
|
767
830
|
const out = await api("POST", agentPath(r, verb));
|
|
768
831
|
window.denShell?.toast?.(
|
|
769
832
|
t(
|
|
770
|
-
verb
|
|
771
|
-
? "ag.
|
|
772
|
-
:
|
|
773
|
-
? "ag.
|
|
774
|
-
:
|
|
833
|
+
verb === "relaunch"
|
|
834
|
+
? "ag.relaunched"
|
|
835
|
+
: verb !== "resume"
|
|
836
|
+
? "ag.startedFresh"
|
|
837
|
+
: out?.fresh
|
|
838
|
+
? "ag.resumedFresh"
|
|
839
|
+
: "ag.resumed",
|
|
775
840
|
{ id: r.agent },
|
|
776
841
|
),
|
|
777
842
|
);
|
|
@@ -1093,7 +1158,7 @@ export async function mountAgentsPage(root) {
|
|
|
1093
1158
|
(x) => x.agent === agent && x.workspace === ws,
|
|
1094
1159
|
);
|
|
1095
1160
|
if (!r) return;
|
|
1096
|
-
if (which === "resume" || which === "fresh")
|
|
1161
|
+
if (which === "resume" || which === "fresh" || which === "relaunch")
|
|
1097
1162
|
void restartRow(r, which, act);
|
|
1098
1163
|
else if (which === "remove") void removeRow(r, act);
|
|
1099
1164
|
return;
|
|
@@ -414,6 +414,18 @@
|
|
|
414
414
|
"ag.fresh": "Empezar de cero",
|
|
415
415
|
"ag.freshName": "Empezar {id} de cero",
|
|
416
416
|
"ag.startedFresh": "{id} empezó de cero",
|
|
417
|
+
"ag.rot.ready": "Listo para relanzar ({why})",
|
|
418
|
+
"ag.rot.due": "Falta el traspaso ({why})",
|
|
419
|
+
"ag.rot.compactions": "{n} compactaciones",
|
|
420
|
+
"ag.rot.compactions.one": "{n} compactación",
|
|
421
|
+
"ag.rot.merged": "PR fusionado",
|
|
422
|
+
"ag.rot.noHandoff":
|
|
423
|
+
"Su sesión rotó ({why}), pero no escribió handoff.md. Reanúdalo para que lo escriba, o empieza de cero.",
|
|
424
|
+
"ag.rot.badHandoff":
|
|
425
|
+
"Su sesión rotó ({why}), pero su handoff.md no está completo. Reanúdalo para arreglarlo, o empieza de cero.",
|
|
426
|
+
"ag.relaunch": "Relanzar desde el traspaso",
|
|
427
|
+
"ag.relaunchName": "Relanzar {id} desde el traspaso",
|
|
428
|
+
"ag.relaunched": "{id} relanzado desde su traspaso",
|
|
417
429
|
"ag.remove": "Quitar",
|
|
418
430
|
"ag.removeName": "Quitar {id}",
|
|
419
431
|
"ag.removed": "{id} quitado",
|
|
@@ -401,6 +401,17 @@
|
|
|
401
401
|
"ag.fresh": "新しく始める",
|
|
402
402
|
"ag.freshName": "{id} を新しく始める",
|
|
403
403
|
"ag.startedFresh": "{id} を新しく始めました",
|
|
404
|
+
"ag.rot.ready": "再起動の準備ができました({why})",
|
|
405
|
+
"ag.rot.due": "引き継ぎが必要です({why})",
|
|
406
|
+
"ag.rot.compactions": "圧縮 {n} 回",
|
|
407
|
+
"ag.rot.merged": "PR がマージされました",
|
|
408
|
+
"ag.rot.noHandoff":
|
|
409
|
+
"セッションは交代しました({why})が、handoff.md がありません。再開して書かせるか、新しく始めてください。",
|
|
410
|
+
"ag.rot.badHandoff":
|
|
411
|
+
"セッションは交代しました({why})が、handoff.md が不完全です。再開して直すか、新しく始めてください。",
|
|
412
|
+
"ag.relaunch": "引き継ぎから再起動",
|
|
413
|
+
"ag.relaunchName": "{id} を引き継ぎから再起動",
|
|
414
|
+
"ag.relaunched": "{id} を引き継ぎから再起動しました",
|
|
404
415
|
"ag.remove": "削除",
|
|
405
416
|
"ag.removeName": "{id} を削除",
|
|
406
417
|
"ag.removed": "{id} を削除しました",
|
|
@@ -534,6 +534,19 @@
|
|
|
534
534
|
"ag.restored": "Back in the list: {ids}",
|
|
535
535
|
"ag.clear": "Clear stopped",
|
|
536
536
|
"ag.noResume": "{runtime} can't resume a conversation",
|
|
537
|
+
// Session rotation (den#4).
|
|
538
|
+
"ag.rot.ready": "Ready to relaunch ({why})",
|
|
539
|
+
"ag.rot.due": "Handoff due ({why})",
|
|
540
|
+
"ag.rot.compactions": "{n} compactions",
|
|
541
|
+
"ag.rot.compactions.one": "{n} compaction",
|
|
542
|
+
"ag.rot.merged": "PR merged",
|
|
543
|
+
"ag.rot.noHandoff":
|
|
544
|
+
"Its session rotated ({why}), but it wrote no handoff.md. Resume it to write one, or start fresh.",
|
|
545
|
+
"ag.rot.badHandoff":
|
|
546
|
+
"Its session rotated ({why}), but its handoff.md is not complete. Resume it to fix it, or start fresh.",
|
|
547
|
+
"ag.relaunch": "Relaunch from handoff",
|
|
548
|
+
"ag.relaunchName": "Relaunch {id} from handoff",
|
|
549
|
+
"ag.relaunched": "Relaunched {id} from its handoff",
|
|
537
550
|
"ag.interrupted": "{n} agents were interrupted when {name} restarted",
|
|
538
551
|
"ag.interrupted.one": "{n} agent was interrupted when {name} restarted",
|
|
539
552
|
"ag.resumeAll": "Resume all",
|
|
@@ -318,6 +318,11 @@ export const ROUTES: readonly RouteEntry[] = [
|
|
|
318
318
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
319
319
|
"starts an agent afresh: a runtime in a PTY",
|
|
320
320
|
),
|
|
321
|
+
S(
|
|
322
|
+
["POST"],
|
|
323
|
+
"/api/workspaces/:ws/agents/:id/relaunch",
|
|
324
|
+
"relaunches a rotated agent from its handoff: a runtime in a PTY (den#4)",
|
|
325
|
+
),
|
|
321
326
|
S(
|
|
322
327
|
["POST"],
|
|
323
328
|
"/api/agents/resume-interrupted",
|
|
@@ -400,6 +405,7 @@ export const REGEX_ROUTES: Readonly<Record<string, readonly string[]>> = {
|
|
|
400
405
|
"/api/workspaces/:ws/agents/:id/stop",
|
|
401
406
|
"/api/workspaces/:ws/agents/:id/resume",
|
|
402
407
|
"/api/workspaces/:ws/agents/:id/fresh",
|
|
408
|
+
"/api/workspaces/:ws/agents/:id/relaunch",
|
|
403
409
|
"/api/workspaces/:ws/agents/:id/remove",
|
|
404
410
|
],
|
|
405
411
|
"agent-routes.ts:RUNTIME_PATH": ["/api/agents/runtimes/:id/path"],
|
|
@@ -43,6 +43,7 @@ import { getStateDir } from "../../shared/paths.js";
|
|
|
43
43
|
import { product } from "../../shared/product.js";
|
|
44
44
|
import { revokedAgentIds } from "../../shared/actor-token.js";
|
|
45
45
|
import { listRecords } from "../../shared/agents/records.js";
|
|
46
|
+
import type { RotationInfo } from "../../shared/agents/rotation.js";
|
|
46
47
|
import {
|
|
47
48
|
connectHost,
|
|
48
49
|
noHost,
|
|
@@ -153,6 +154,11 @@ export interface ListedSession {
|
|
|
153
154
|
tokenRevoked?: boolean;
|
|
154
155
|
/** An approval is pending (AG-6a; only when the host knows). */
|
|
155
156
|
waiting?: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Session rotation (den#4): why it rotates and its handoff.md. `ready`:
|
|
159
|
+
* Relaunch from handoff is its action, not Resume.
|
|
160
|
+
*/
|
|
161
|
+
rotation?: RotationInfo;
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
/** The list, from the host's sessions and the agent records. */
|
|
@@ -176,7 +182,7 @@ export function listedSessions(
|
|
|
176
182
|
const out: ListedSession[] = [];
|
|
177
183
|
const agentSessions = new Set<string>();
|
|
178
184
|
noteEnded(d.stateDir(), mine, records);
|
|
179
|
-
for (const r of agentRows(mine, records)) {
|
|
185
|
+
for (const r of agentRows(mine, records, d.stateDir())) {
|
|
180
186
|
const rec = records.find((x) => x.id === r.agent)!;
|
|
181
187
|
const s = r.session;
|
|
182
188
|
if (s) agentSessions.add(s.ref.id);
|
|
@@ -201,6 +207,7 @@ export function listedSessions(
|
|
|
201
207
|
...(r.stoppedAt ? { stoppedAt: r.stoppedAt } : {}),
|
|
202
208
|
...(r.endedAt ? { endedAt: r.endedAt } : {}),
|
|
203
209
|
...(r.interruptedAt ? { interruptedAt: r.interruptedAt } : {}),
|
|
210
|
+
...(r.rotation?.due ? { rotation: r.rotation } : {}),
|
|
204
211
|
...(revoked.has(rec.id) && s && !s.exited ? { tokenRevoked: true } : {}),
|
|
205
212
|
});
|
|
206
213
|
}
|