pi-famulus 0.1.3 → 0.1.4

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/src/wake.ts CHANGED
@@ -5,10 +5,13 @@
5
5
  * discriminator the renderer and the eval adapter both read.
6
6
  */
7
7
 
8
+ // .ts suffix: eval/ loads this module with Node's own TypeScript support, which
9
+ // needs explicit extensions (it imports wake.ts and behavior-guidelines.ts).
10
+ import { fill, PROMPTS } from "./prompts.generated.ts";
11
+
8
12
  export const FAMULUS_WAKE_CUSTOM_TYPE = "pi-famulus-wake";
9
13
 
10
- export const FAMULUS_WAKE_LEAD_IN =
11
- "System wake — not a new user message. Handle this <pi-famulus-wake> before other work.";
14
+ export const FAMULUS_WAKE_LEAD_IN = PROMPTS["wakes.lead-in"];
12
15
 
13
16
  export interface WakeItem {
14
17
  id: string;
@@ -60,6 +63,21 @@ export interface OverrunShell {
60
63
  growing: boolean | null;
61
64
  }
62
65
 
66
+ /** Context retained for every secondary child in a batched overrun wake. */
67
+ export interface SubagentOverrunChild {
68
+ runId: string;
69
+ childId: string;
70
+ name: string;
71
+ elapsedMs: number;
72
+ budgetMs: number;
73
+ reminder: number;
74
+ nextReminderMs: number;
75
+ /** Time left until the opt-in hard ceiling aborts this child. */
76
+ hardCeilingMs?: number;
77
+ lastActivity: { agoMs: number; text: string };
78
+ shell?: OverrunShell;
79
+ }
80
+
63
81
  /**
64
82
  * When the wake's content was generated (design.md §4.5): stamped by the
65
83
  * NotifyCenter when it receives or builds the wake. A steered wake can wait
@@ -133,6 +151,7 @@ type FamulusWakeBody =
133
151
  summary: string;
134
152
  lastActivity: { agoMs: number; text: string };
135
153
  shell?: OverrunShell;
154
+ additional?: SubagentOverrunChild[];
136
155
  }
137
156
  | { kind: "supervisor-request"; from: string; name: string; message: string }
138
157
  | { kind: "supervisor-update"; from: string; name: string; message: string };
@@ -177,6 +196,7 @@ export function wakeIds(details: { kind?: string }): string[] {
177
196
  };
178
197
  if (d.kind === "task") return (d.tasks ?? []).map((task) => task.id);
179
198
  if (d.kind === "subagent-done") return (d.children ?? []).map((child) => child.childId);
199
+ if (d.kind === "subagent-overrun") return [d.childId, ...((d as { additional?: { childId: string }[] }).additional ?? []).map((child) => child.childId)].filter((id): id is string => Boolean(id));
180
200
  return [d.id ?? d.taskId ?? d.childId ?? d.from].filter((id): id is string => Boolean(id));
181
201
  }
182
202
 
@@ -317,7 +337,7 @@ function renderHandover(details: Extract<FamulusWake, { kind: "subagent-handover
317
337
  parts.push(
318
338
  ` <changed-since-as-of>${escapeXml(
319
339
  `${details.name} (${details.childId}): ${details.statusAsOf} → ${details.status}.` +
320
- (active ? " Its result arrives as a new wake when it finishes." : ""),
340
+ (active ? ` ${PROMPTS["wakes.handover.changed-active"]}` : ""),
321
341
  )}</changed-since-as-of>`,
322
342
  );
323
343
  }
@@ -344,7 +364,7 @@ function renderDone(details: Extract<FamulusWake, { kind: "subagent-done" }>): s
344
364
  const active = details.children.some((c) => c.status === "pending" || c.status === "running");
345
365
  parts.push(
346
366
  ` <changed-since-as-of>${escapeXml(
347
- `${list}.` + (active ? " The run is active again; another subagent-done arrives when it finishes." : ""),
367
+ `${list}.` + (active ? ` ${PROMPTS["wakes.done.changed-active"]}` : ""),
348
368
  )}</changed-since-as-of>`,
349
369
  );
350
370
  }
@@ -387,17 +407,29 @@ function renderOverrun(details: Extract<FamulusWake, { kind: "subagent-overrun"
387
407
  parts.push(
388
408
  ` <last-activity ago-ms="${Math.round(details.lastActivity.agoMs)}">${escapeXml(details.lastActivity.text)}</last-activity>`,
389
409
  );
390
- const shell = details.shell;
391
- if (shell) {
392
- const shellAttrs = [`task-id="${escapeXmlAttr(shell.taskId)}"`, `elapsed-ms="${Math.round(shell.elapsedMs)}"`];
393
- if (shell.outputBytes !== null) shellAttrs.push(`output-bytes="${shell.outputBytes}"`);
394
- if (shell.outputIdleMs !== null) shellAttrs.push(`output-idle-ms="${Math.round(shell.outputIdleMs)}"`);
395
- if (shell.growing !== null) shellAttrs.push(`growing="${shell.growing ? "yes" : "no"}"`);
396
- parts.push(` <shell ${shellAttrs.join(" ")}>`);
397
- parts.push(` <command>${escapeXml(shell.command)}</command>`);
398
- parts.push(` <output-file>${escapeXml(shell.outputPath)}</output-file>`);
399
- parts.push(" </shell>");
410
+ if (details.additional?.length) {
411
+ parts.push(" <also-overdue>");
412
+ for (const child of details.additional) {
413
+ const childAttrs = [
414
+ `run-id="${escapeXmlAttr(child.runId)}"`,
415
+ `child-id="${escapeXmlAttr(child.childId)}"`,
416
+ `name="${escapeXmlAttr(child.name)}"`,
417
+ `elapsed-ms="${Math.round(child.elapsedMs)}"`,
418
+ `budget-ms="${Math.round(child.budgetMs)}"`,
419
+ `reminder="${child.reminder}"`,
420
+ `next-reminder-ms="${Math.round(child.nextReminderMs)}"`,
421
+ ];
422
+ if (child.hardCeilingMs !== undefined) childAttrs.push(`hard-ceiling-ms="${Math.round(child.hardCeilingMs)}"`);
423
+ parts.push(` <child ${childAttrs.join(" ")}>`);
424
+ parts.push(
425
+ ` <last-activity ago-ms="${Math.round(child.lastActivity.agoMs)}">${escapeXml(child.lastActivity.text)}</last-activity>`,
426
+ );
427
+ if (child.shell) parts.push(...renderOverrunShell(child.shell, " "));
428
+ parts.push(" </child>");
429
+ }
430
+ parts.push(" </also-overdue>");
400
431
  }
432
+ if (details.shell) parts.push(...renderOverrunShell(details.shell, " "));
401
433
  parts.push(` <options>${escapeXml(overrunOptions(details))}</options>`);
402
434
  parts.push("</pi-famulus-wake>");
403
435
  return parts.join("\n");
@@ -405,37 +437,34 @@ function renderOverrun(details: Extract<FamulusWake, { kind: "subagent-overrun"
405
437
 
406
438
  /** The parent's three actions and what happens if it takes none. */
407
439
  function overrunOptions(details: Extract<FamulusWake, { kind: "subagent-overrun" }>): string {
408
- const ids = `run_id: "${details.runId}", child_id: "${details.childId}"`;
409
- const actions =
410
- `give it more time with subagent({ action: "extend", ${ids}, timeout_ms: <ms from now> }); ` +
411
- `redirect it with agent_message({ action: "send", to: "${details.childId}", message: "<instruction>" }); ` +
412
- `or stop it with subagent({ action: "interrupt", ${ids} }). `;
440
+ const actions = fill("wakes.overrun.actions", { runId: details.runId, childId: details.childId });
441
+ const next = wakeDuration(details.nextReminderMs);
413
442
  const hard = details.hardCeilingMs;
414
- if (hard === undefined) {
415
- return (
416
- "It has not been stopped. Choose one: " +
417
- actions +
418
- `If you do none of these, it keeps running and the next reminder is scheduled in ${wakeDuration(details.nextReminderMs)}; ` +
419
- "its result arrives as usual when it finishes."
420
- );
421
- }
443
+ if (hard === undefined) return fill("wakes.overrun.options", { actions, next });
422
444
  // Opt-in hard ceiling: never promise "keeps running" past it.
423
445
  const ceiling = wakeDuration(hard);
424
446
  const noAction =
425
447
  details.nextReminderMs < hard
426
- ? `If you do none of these, it keeps running and the next reminder is scheduled in ${wakeDuration(details.nextReminderMs)}, until the hard ceiling stops it in ${ceiling}.`
427
- : "If you do none of these, it keeps running until the hard ceiling stops it.";
428
- return (
429
- `It has not been stopped yet, but the configured hard ceiling stops it in ${ceiling}, and extend does not move that ceiling. ` +
430
- "Choose one: " +
431
- actions +
432
- noAction +
433
- " Its result, or the interruption, arrives as a wake."
434
- );
448
+ ? fill("wakes.overrun.ceiling-no-action-reminder", { next, ceiling })
449
+ : PROMPTS["wakes.overrun.ceiling-no-action"];
450
+ return fill("wakes.overrun.options-ceiling", { ceiling, actions, noAction });
451
+ }
452
+
453
+ function renderOverrunShell(shell: OverrunShell, indent: string): string[] {
454
+ const shellAttrs = [`task-id="${escapeXmlAttr(shell.taskId)}"`, `elapsed-ms="${Math.round(shell.elapsedMs)}"`];
455
+ if (shell.outputBytes !== null) shellAttrs.push(`output-bytes="${shell.outputBytes}"`);
456
+ if (shell.outputIdleMs !== null) shellAttrs.push(`output-idle-ms="${Math.round(shell.outputIdleMs)}"`);
457
+ if (shell.growing !== null) shellAttrs.push(`growing="${shell.growing ? "yes" : "no"}"`);
458
+ return [
459
+ `${indent}<shell ${shellAttrs.join(" ")}>`,
460
+ `${indent} <command>${escapeXml(shell.command)}</command>`,
461
+ `${indent} <output-file>${escapeXml(shell.outputPath)}</output-file>`,
462
+ `${indent}</shell>`,
463
+ ];
435
464
  }
436
465
 
437
466
  function renderRequest(details: Extract<FamulusWake, { kind: "supervisor-request" }>): string {
438
- const recipe = `agent_message { action: "reply", to: "${details.from}", message: "<your decision>" }`;
467
+ const recipe = fill("wakes.request.reply-with", { from: details.from });
439
468
  return [
440
469
  `<pi-famulus-wake kind="supervisor-request" from="${escapeXmlAttr(details.from)}" name="${escapeXmlAttr(details.name)}">`,
441
470
  ` <message>${escapeXml(details.message)}</message>`,
package/src/work-index.ts CHANGED
@@ -14,6 +14,10 @@ export const WORK_FINISHED_CAP = 50;
14
14
  export interface WorkItem {
15
15
  id: string;
16
16
  kind: WorkKind;
17
+ /** Daemon work-kind label for agents; shell commands are classified separately. */
18
+ workKind?: string;
19
+ /** Time queued for local/machine admission before the agent started. */
20
+ queueMs?: number;
17
21
  status: string;
18
22
  title: string;
19
23
  command?: string;