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/README.md +3 -1
- package/package.json +8 -6
- package/src/agents/builtins.ts +5 -22
- package/src/agents/definition.ts +4 -6
- package/src/bash-override.ts +8 -21
- package/src/behavior-guidelines.ts +40 -16
- package/src/comms/comms.ts +7 -3
- package/src/comms/tools.ts +11 -36
- package/src/format.ts +31 -14
- package/src/index.ts +123 -33
- package/src/manager-client.ts +265 -114
- package/src/monitor.ts +14 -33
- package/src/prompts.generated.ts +183 -0
- package/src/shell-exec.ts +6 -2
- package/src/subagent/admission.ts +224 -0
- package/src/subagent/agent-records.ts +66 -3
- package/src/subagent/child-bash.ts +6 -14
- package/src/subagent/overrun.ts +36 -21
- package/src/subagent/pi-runtime.ts +116 -31
- package/src/subagent/registry.ts +165 -42
- package/src/subagent/runner.ts +335 -60
- package/src/subagent/tool.ts +49 -72
- package/src/subagent/types.ts +60 -5
- package/src/subagent/usage.ts +48 -0
- package/src/task-tools.ts +15 -25
- package/src/tui/message-renderers.ts +17 -1
- package/src/wake.ts +66 -37
- package/src/work-index.ts +4 -0
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 {
|
|
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
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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) =>
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
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
|
}
|