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/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * M2: NotifyCenter + monitor tool.
6
6
  * M3: subagent tool (InProcessRunner + tasks/chain + budget-to-async) + fleet widget.
7
7
  */
8
- import { applyBehaviorGuidelines } from "./behavior-guidelines";
8
+ import { registerBehaviorGuidelines } from "./behavior-guidelines";
9
9
  import { realClock } from "./clock";
10
10
  import { ExitNotifyGate } from "./exit-notify-gate";
11
11
  import { ExitWatchdog } from "./exit-watchdog";
@@ -27,6 +27,7 @@ import { describeManagerSearch, getFamulusHome, loadConfig, resolveManagerPath,
27
27
  import type { TaskExitInfo } from "./format";
28
28
  import { ManagerClient, type ManagerEvent, type TaskRecord } from "./manager-client";
29
29
  import { createMonitorTool, exitEventFromRecord, MonitorRegistry } from "./monitor";
30
+ import { admitAgentChild, reregisterAgentLeases } from "./subagent/admission";
30
31
  import { NotifyCenter } from "./notify";
31
32
  import { onWakeMessageEnd, registryStatusLookup } from "./wake-delivery";
32
33
  import { createChildBashTool } from "./subagent/child-bash";
@@ -35,13 +36,14 @@ import {
35
36
  agentEndReason,
36
37
  headOf,
37
38
  tailOf,
39
+ updateAgentChildMetrics,
38
40
  writeAgentChildRecord,
39
41
  type AgentChildRecord,
40
42
  } from "./subagent/agent-records";
41
43
  import { TranscriptWriter } from "./subagent/transcript";
42
44
  import { FleetWidget } from "./subagent/fleet-widget";
43
45
  import { WorkIndex, type WorkItem } from "./work-index";
44
- import { createPiSessionFn, modelCandidates } from "./subagent/pi-runtime";
46
+ import { createPiSessionFn, isFamulusChildSession, modelCandidates } from "./subagent/pi-runtime";
45
47
  import { SubagentRegistry } from "./subagent/registry";
46
48
  import { InProcessRunner } from "./subagent/runner";
47
49
  import { createSubagentTool } from "./subagent/tool";
@@ -68,6 +70,8 @@ function toExitStatus(event: ManagerEvent): TaskExitInfo["status"] {
68
70
  }
69
71
 
70
72
  export default function (pi: ExtensionAPI): void {
73
+ if (isFamulusChildSession(pi)) return;
74
+ registerBehaviorGuidelines(pi);
71
75
  const home = getFamulusHome();
72
76
  const config = loadConfig(home);
73
77
  const managerPath = resolveManagerPath(config, home);
@@ -78,6 +82,29 @@ export default function (pi: ExtensionAPI): void {
78
82
 
79
83
  let ctx: ExtensionContext | null = null;
80
84
  let client: ManagerClient | null = null;
85
+ const globalAgentLeases = new Map<string, string>();
86
+ const pendingAgentLeases = new Set<string>();
87
+ const pendingAgentReregistrations = new Map<string, AbortController>();
88
+ let leaseReconciliation: Promise<void> | null = null;
89
+ const reconcileAgentLeases = (manager: ManagerClient): Promise<void> => {
90
+ if (leaseReconciliation) return leaseReconciliation;
91
+ leaseReconciliation = reregisterAgentLeases(
92
+ manager,
93
+ globalAgentLeases,
94
+ undefined,
95
+ pendingAgentLeases,
96
+ pendingAgentReregistrations,
97
+ ).finally(() => {
98
+ leaseReconciliation = null;
99
+ });
100
+ return leaseReconciliation;
101
+ };
102
+ const capacityNotices = new Set<string>();
103
+ const capacityNotice = (reason: "manager unavailable" | "daemon too old"): void => {
104
+ if (capacityNotices.has(reason)) return;
105
+ capacityNotices.add(reason);
106
+ console.warn(`pi-famulus: machine-wide agent capacity inactive (${reason}); using per-session limit only`);
107
+ };
81
108
  let notifyCenter: NotifyCenter | null = null;
82
109
  let monitorRegistry: MonitorRegistry | null = null;
83
110
  let subagentRegistry: SubagentRegistry | null = null;
@@ -407,10 +434,6 @@ export default function (pi: ExtensionAPI): void {
407
434
 
408
435
  pi.on("session_start", async (_event, startCtx) => {
409
436
  ctx = startCtx;
410
- const base = (
411
- startCtx as { getSystemPromptOptions?: () => { sections?: Record<string, string> } }
412
- ).getSystemPromptOptions?.();
413
- if (base) applyBehaviorGuidelines(base);
414
437
  notifyCenter?.dispose();
415
438
  notifyCenter = new NotifyCenter({
416
439
  sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
@@ -426,8 +449,13 @@ export default function (pi: ExtensionAPI): void {
426
449
  });
427
450
  fleetWidget?.dispose();
428
451
  fleetWidget = null;
429
- subagentRegistry?.disposeAll();
452
+ comms.dispose(); // Release decision waiters before awaiting child aborts.
453
+ await subagentRegistry?.disposeAll().catch(() => {});
430
454
  subagentRegistry = null;
455
+ for (const controller of pendingAgentReregistrations.values()) controller.abort();
456
+ pendingAgentReregistrations.clear();
457
+ globalAgentLeases.clear();
458
+ pendingAgentLeases.clear();
431
459
 
432
460
  client = new ManagerClient({
433
461
  home,
@@ -483,7 +511,21 @@ export default function (pi: ExtensionAPI): void {
483
511
  }
484
512
  });
485
513
  client.onReconnect(() => {
514
+ // A daemon restart may drop permits; a same-session rebind may retain
515
+ // them. Keep each kind for idempotent re-registration and mark all local
516
+ // leases pending until the daemon confirms them.
517
+ for (const childId of globalAgentLeases.keys()) pendingAgentLeases.add(childId);
486
518
  void monitorRegistry?.rewatchAll().then(() => syncWithManager());
519
+ void (async () => {
520
+ const manager = client;
521
+ if (!manager || manager.protocolLevel() < 4) {
522
+ capacityNotice("daemon too old");
523
+ return;
524
+ }
525
+ // The registry and its child ids survive socket reconnects in this
526
+ // extension process; held ids include resumable interrupted children.
527
+ await reconcileAgentLeases(manager);
528
+ })().catch(() => {});
487
529
  });
488
530
 
489
531
  // M3: subagent registry + in-process runner + fleet widget. The runner's
@@ -519,6 +561,7 @@ export default function (pi: ExtensionAPI): void {
519
561
  getParentThinkingLevel: () => ctx?.thinkingLevel,
520
562
  getScopedModels: () => ctx?.scopedModels ?? [],
521
563
  getCwd: () => ctx?.cwd ?? process.cwd(),
564
+ getProjectTrusted: () => ctx?.isProjectTrusted() ?? false,
522
565
  customTools: (req) => {
523
566
  const tools: Array<ToolDefinition<any, any, any>> = [
524
567
  // M4: every child can reach the supervisor and its siblings.
@@ -527,21 +570,19 @@ export default function (pi: ExtensionAPI): void {
527
570
  ];
528
571
  // The no-background bash variant replaces the built-in bash inside
529
572
  // child sessions (custom tools override builtins by name).
530
- if (req.agent.tools.includes("bash")) {
531
- tools.push(
532
- createChildBashTool({
533
- getClient: () => client,
534
- home,
535
- sessionId: () => ctx?.sessionManager.getSessionId() ?? "",
536
- sessionEnv: () => (ctx ? sessionEnv(ctx) : {}),
537
- trackTask,
538
- childId: req.childId,
539
- runId: req.runId,
540
- clock,
541
- shells: childShells,
542
- }),
543
- );
544
- }
573
+ tools.push(
574
+ createChildBashTool({
575
+ getClient: () => client,
576
+ home,
577
+ sessionId: () => ctx?.sessionManager.getSessionId() ?? "",
578
+ sessionEnv: () => (ctx ? sessionEnv(ctx) : {}),
579
+ trackTask,
580
+ childId: req.childId,
581
+ runId: req.runId,
582
+ clock,
583
+ shells: childShells,
584
+ }),
585
+ );
545
586
  return tools;
546
587
  },
547
588
  });
@@ -559,9 +600,37 @@ export default function (pi: ExtensionAPI): void {
559
600
  stallRetryDelayMs: subagentConfig.stallRetryDelayMs,
560
601
  overrunRepeatMs: subagentConfig.overrunRepeatMs,
561
602
  hardTimeoutMs: subagentConfig.hardTimeoutMs,
562
- acquire: (req, ticket) => registry.admitChild(req.childId, ticket),
603
+ acquire: (req, ticket) => admitAgentChild({
604
+ childId: req.childId,
605
+ workKind: req.workKind ?? "other",
606
+ reserveLocal: () => registry.reserveChildSlot(req.childId, ticket),
607
+ manager: client,
608
+ leases: globalAgentLeases,
609
+ pendingLeases: pendingAgentLeases,
610
+ pendingReregistrations: pendingAgentReregistrations,
611
+ ticket,
612
+ notice: capacityNotice,
613
+ }),
563
614
  onActivity: (childId) => {
564
615
  syncTranscript(childId);
616
+ const handle = registry.handle(childId);
617
+ const usage = handle?.tokenUsage();
618
+ const wall = handle?.wallUsage();
619
+ if (usage && wall) {
620
+ const metrics = {
621
+ tokens_input: usage.input,
622
+ tokens_output: usage.output,
623
+ tokens_cache_read: usage.cacheRead,
624
+ tokens_cache_write: usage.cacheWrite,
625
+ llm_ms: wall.llmMs,
626
+ tool_ms: wall.toolMs,
627
+ queue_ms: wall.queueMs,
628
+ wall_other_ms: wall.otherMs,
629
+ wall_approximate: wall.approximate,
630
+ };
631
+ updateAgentChildMetrics(home, sessionIdForAgents(), childId, metrics);
632
+ logEvent("agent.usage", { child_id: childId, ...metrics });
633
+ }
565
634
  },
566
635
  onStall: (childId, attempt) => {
567
636
  // One event per stall detection: an auto-resume follows unless the
@@ -600,6 +669,8 @@ export default function (pi: ExtensionAPI): void {
600
669
  run_id: run.runId,
601
670
  name: c.name,
602
671
  agent: c.agent,
672
+ work_kind: c.workKind ?? "other",
673
+ queue_ms: c.queueMs ?? 0,
603
674
  ...(c.model ? { model: c.model } : {}),
604
675
  });
605
676
  }
@@ -611,8 +682,11 @@ export default function (pi: ExtensionAPI): void {
611
682
  child_id: c.childId,
612
683
  status: c.status,
613
684
  ...(error ? { error } : {}),
685
+ work_kind: c.workKind ?? "other",
614
686
  ...(c.result?.stalls ? { stalls: c.result.stalls } : {}),
687
+ ...(c.queueMs !== undefined ? { queue_ms: c.queueMs } : {}),
615
688
  duration_ms: c.result?.durationMs ?? Math.max(0, clock.now() - c.startedAt),
689
+ ...(c.result?.queueMs !== undefined ? { queue_ms: c.result.queueMs } : {}),
616
690
  });
617
691
  }
618
692
  previousAgentStatus.set(c.childId, c.status);
@@ -624,9 +698,20 @@ export default function (pi: ExtensionAPI): void {
624
698
  session_id: sid,
625
699
  name: c.name,
626
700
  agent: c.agent,
701
+ work_kind: c.workKind ?? "other",
702
+ ...(c.queueMs !== undefined ? { queue_ms: c.queueMs } : {}),
627
703
  ...(c.model !== undefined ? { model: c.model } : {}),
628
704
  status: c.status,
629
705
  started_at: c.startedAt,
706
+ tokens_input: c.tokenUsage?.input ?? 0,
707
+ tokens_output: c.tokenUsage?.output ?? 0,
708
+ tokens_cache_read: c.tokenUsage?.cacheRead ?? 0,
709
+ tokens_cache_write: c.tokenUsage?.cacheWrite ?? 0,
710
+ llm_ms: c.wallUsage?.llmMs ?? 0,
711
+ tool_ms: c.wallUsage?.toolMs ?? 0,
712
+ queue_ms: c.wallUsage?.queueMs ?? 0,
713
+ wall_other_ms: c.wallUsage?.otherMs ?? 0,
714
+ wall_approximate: c.wallUsage?.approximate ?? false,
630
715
  ...(c.endedAt !== undefined ? { ended_at: c.endedAt } : {}),
631
716
  ...(c.result?.error ? { error: c.result.error } : {}),
632
717
  ...(c.result?.attempts !== undefined && c.result.attempts > 1
@@ -655,6 +740,8 @@ export default function (pi: ExtensionAPI): void {
655
740
  runId: run.runId,
656
741
  name: c.name,
657
742
  agent: c.agent,
743
+ workKind: c.workKind ?? "other",
744
+ ...(c.queueMs !== undefined ? { queueMs: c.queueMs } : {}),
658
745
  ...(c.model !== undefined ? { model: c.model } : {}),
659
746
  cwd: startCtx.cwd,
660
747
  ...(c.prompt !== undefined ? { prompt: c.prompt } : {}),
@@ -679,7 +766,11 @@ export default function (pi: ExtensionAPI): void {
679
766
  void c
680
767
  .connect()
681
768
  .then((ok) => {
682
- if (!ok && startCtx.hasUI) {
769
+ if (ok) {
770
+ if (c.protocolLevel() >= 4) void reconcileAgentLeases(c).catch(() => {});
771
+ return;
772
+ }
773
+ if (startCtx.hasUI) {
683
774
  const detail = c.lastError();
684
775
  const reason = detail ? ` (${detail})` : "";
685
776
  const searched = managerPath ? `using ${managerPath}` : `looked in: ${describeManagerSearch(config, home)}`;
@@ -698,19 +789,22 @@ export default function (pi: ExtensionAPI): void {
698
789
  exitWatchdog.dispose();
699
790
  fleetWidget?.dispose();
700
791
  fleetWidget = null;
701
- subagentRegistry?.disposeAll();
792
+ comms.dispose(); // Release decision waiters before awaiting child aborts.
793
+ await subagentRegistry?.disposeAll().catch(() => {});
702
794
  subagentRegistry = null;
703
795
  agentLoader = null;
704
- comms.dispose(); // resolve orphaned need_decision waiters
705
796
  monitorRegistry?.disposeAll();
706
797
  notifyCenter?.dispose();
707
798
  const current = client;
708
799
  client = null;
709
800
  if (current) {
710
- if (current.isAvailable()) {
711
- await current.shutdownSession().catch(() => {});
801
+ try {
802
+ if (current.isAvailable()) await current.shutdownSession();
803
+ } catch {
804
+ // Child teardown is best-effort; still release the manager connection.
805
+ } finally {
806
+ await current.close().catch(() => {});
712
807
  }
713
- await current.close();
714
808
  }
715
809
  ctx = null;
716
810
  });
@@ -728,8 +822,4 @@ export default function (pi: ExtensionAPI): void {
728
822
  });
729
823
  return message ? { message } : undefined;
730
824
  });
731
-
732
- pi.on("before_agent_start", async (event) => {
733
- applyBehaviorGuidelines(event.systemPromptOptions as { sections?: Record<string, string> });
734
- });
735
825
  }