pi-famulus 0.1.2 → 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 +31 -12
- package/src/agents/builtins.ts +5 -22
- package/src/agents/definition.ts +16 -12
- package/src/bash-override.ts +8 -21
- package/src/behavior-guidelines.ts +40 -15
- package/src/clock.ts +5 -0
- package/src/comms/comms.ts +7 -3
- package/src/comms/tools.ts +11 -36
- package/src/config.ts +25 -1
- package/src/format.ts +83 -5
- package/src/index.ts +155 -30
- package/src/manager-client.ts +265 -114
- package/src/monitor.ts +14 -33
- package/src/notify.ts +7 -7
- 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 +65 -56
- package/src/subagent/model-spec.ts +134 -16
- package/src/subagent/overrun.ts +191 -0
- package/src/subagent/pi-runtime.ts +118 -33
- package/src/subagent/registry.ts +235 -34
- package/src/subagent/runner.ts +565 -99
- package/src/subagent/tool.ts +111 -61
- package/src/subagent/types.ts +71 -7
- package/src/subagent/usage.ts +48 -0
- package/src/task-tools.ts +15 -25
- package/src/thinking-levels.ts +26 -0
- package/src/tui/message-renderers.ts +52 -2
- package/src/wake-delivery.ts +137 -0
- package/src/wake.ts +270 -7
- package/src/work-index.ts +4 -0
package/src/index.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
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";
|
|
12
12
|
import { createExtensionEventLog } from "./events";
|
|
13
13
|
import { readFileTail } from "./file-tail";
|
|
14
|
+
import { statSync } from "node:fs";
|
|
14
15
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { join } from "node:path";
|
|
@@ -26,19 +27,23 @@ import { describeManagerSearch, getFamulusHome, loadConfig, resolveManagerPath,
|
|
|
26
27
|
import type { TaskExitInfo } from "./format";
|
|
27
28
|
import { ManagerClient, type ManagerEvent, type TaskRecord } from "./manager-client";
|
|
28
29
|
import { createMonitorTool, exitEventFromRecord, MonitorRegistry } from "./monitor";
|
|
30
|
+
import { admitAgentChild, reregisterAgentLeases } from "./subagent/admission";
|
|
29
31
|
import { NotifyCenter } from "./notify";
|
|
32
|
+
import { onWakeMessageEnd, registryStatusLookup } from "./wake-delivery";
|
|
30
33
|
import { createChildBashTool } from "./subagent/child-bash";
|
|
34
|
+
import { ChildShellTracker, createOverrunNotifier } from "./subagent/overrun";
|
|
31
35
|
import {
|
|
32
36
|
agentEndReason,
|
|
33
37
|
headOf,
|
|
34
38
|
tailOf,
|
|
39
|
+
updateAgentChildMetrics,
|
|
35
40
|
writeAgentChildRecord,
|
|
36
41
|
type AgentChildRecord,
|
|
37
42
|
} from "./subagent/agent-records";
|
|
38
43
|
import { TranscriptWriter } from "./subagent/transcript";
|
|
39
44
|
import { FleetWidget } from "./subagent/fleet-widget";
|
|
40
45
|
import { WorkIndex, type WorkItem } from "./work-index";
|
|
41
|
-
import { createPiSessionFn, modelCandidates } from "./subagent/pi-runtime";
|
|
46
|
+
import { createPiSessionFn, isFamulusChildSession, modelCandidates } from "./subagent/pi-runtime";
|
|
42
47
|
import { SubagentRegistry } from "./subagent/registry";
|
|
43
48
|
import { InProcessRunner } from "./subagent/runner";
|
|
44
49
|
import { createSubagentTool } from "./subagent/tool";
|
|
@@ -65,6 +70,8 @@ function toExitStatus(event: ManagerEvent): TaskExitInfo["status"] {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
export default function (pi: ExtensionAPI): void {
|
|
73
|
+
if (isFamulusChildSession(pi)) return;
|
|
74
|
+
registerBehaviorGuidelines(pi);
|
|
68
75
|
const home = getFamulusHome();
|
|
69
76
|
const config = loadConfig(home);
|
|
70
77
|
const managerPath = resolveManagerPath(config, home);
|
|
@@ -75,6 +82,29 @@ export default function (pi: ExtensionAPI): void {
|
|
|
75
82
|
|
|
76
83
|
let ctx: ExtensionContext | null = null;
|
|
77
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
|
+
};
|
|
78
108
|
let notifyCenter: NotifyCenter | null = null;
|
|
79
109
|
let monitorRegistry: MonitorRegistry | null = null;
|
|
80
110
|
let subagentRegistry: SubagentRegistry | null = null;
|
|
@@ -404,10 +434,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
404
434
|
|
|
405
435
|
pi.on("session_start", async (_event, startCtx) => {
|
|
406
436
|
ctx = startCtx;
|
|
407
|
-
const base = (
|
|
408
|
-
startCtx as { getSystemPromptOptions?: () => { sections?: Record<string, string> } }
|
|
409
|
-
).getSystemPromptOptions?.();
|
|
410
|
-
if (base) applyBehaviorGuidelines(base);
|
|
411
437
|
notifyCenter?.dispose();
|
|
412
438
|
notifyCenter = new NotifyCenter({
|
|
413
439
|
sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
|
|
@@ -423,8 +449,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
423
449
|
});
|
|
424
450
|
fleetWidget?.dispose();
|
|
425
451
|
fleetWidget = null;
|
|
426
|
-
|
|
452
|
+
comms.dispose(); // Release decision waiters before awaiting child aborts.
|
|
453
|
+
await subagentRegistry?.disposeAll().catch(() => {});
|
|
427
454
|
subagentRegistry = null;
|
|
455
|
+
for (const controller of pendingAgentReregistrations.values()) controller.abort();
|
|
456
|
+
pendingAgentReregistrations.clear();
|
|
457
|
+
globalAgentLeases.clear();
|
|
458
|
+
pendingAgentLeases.clear();
|
|
428
459
|
|
|
429
460
|
client = new ManagerClient({
|
|
430
461
|
home,
|
|
@@ -480,7 +511,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
480
511
|
}
|
|
481
512
|
});
|
|
482
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);
|
|
483
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(() => {});
|
|
484
529
|
});
|
|
485
530
|
|
|
486
531
|
// M3: subagent registry + in-process runner + fleet widget. The runner's
|
|
@@ -504,6 +549,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
504
549
|
"warning",
|
|
505
550
|
);
|
|
506
551
|
}
|
|
552
|
+
// The foreground shell each child is blocked on, for its overrun wake.
|
|
553
|
+
const childShells = new ChildShellTracker();
|
|
507
554
|
const createSession = createPiSessionFn({
|
|
508
555
|
getModelRegistry: () => ctx?.modelRegistry ?? null,
|
|
509
556
|
getModelRuntime: () => {
|
|
@@ -514,6 +561,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
514
561
|
getParentThinkingLevel: () => ctx?.thinkingLevel,
|
|
515
562
|
getScopedModels: () => ctx?.scopedModels ?? [],
|
|
516
563
|
getCwd: () => ctx?.cwd ?? process.cwd(),
|
|
564
|
+
getProjectTrusted: () => ctx?.isProjectTrusted() ?? false,
|
|
517
565
|
customTools: (req) => {
|
|
518
566
|
const tools: Array<ToolDefinition<any, any, any>> = [
|
|
519
567
|
// M4: every child can reach the supervisor and its siblings.
|
|
@@ -522,20 +570,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
522
570
|
];
|
|
523
571
|
// The no-background bash variant replaces the built-in bash inside
|
|
524
572
|
// child sessions (custom tools override builtins by name).
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
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
|
+
);
|
|
539
586
|
return tools;
|
|
540
587
|
},
|
|
541
588
|
});
|
|
@@ -551,15 +598,61 @@ export default function (pi: ExtensionAPI): void {
|
|
|
551
598
|
stallMs: subagentConfig.stallMs,
|
|
552
599
|
stallRetries: subagentConfig.stallRetries,
|
|
553
600
|
stallRetryDelayMs: subagentConfig.stallRetryDelayMs,
|
|
554
|
-
|
|
601
|
+
overrunRepeatMs: subagentConfig.overrunRepeatMs,
|
|
602
|
+
hardTimeoutMs: subagentConfig.hardTimeoutMs,
|
|
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
|
+
}),
|
|
555
614
|
onActivity: (childId) => {
|
|
556
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
|
+
}
|
|
557
634
|
},
|
|
558
635
|
onStall: (childId, attempt) => {
|
|
559
636
|
// One event per stall detection: an auto-resume follows unless the
|
|
560
637
|
// retry budget (stallRetries) is already spent.
|
|
561
638
|
logEvent("agent.stall", { child_id: childId, attempt });
|
|
562
639
|
},
|
|
640
|
+
// Soft deadline: the child keeps running; the parent decides.
|
|
641
|
+
onOverrun: createOverrunNotifier({
|
|
642
|
+
now: () => clock.now(),
|
|
643
|
+
registry,
|
|
644
|
+
shells: childShells,
|
|
645
|
+
stat: (path) => {
|
|
646
|
+
try {
|
|
647
|
+
const st = statSync(path);
|
|
648
|
+
return { size: st.size, mtimeMs: st.mtimeMs };
|
|
649
|
+
} catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
notify: (wake) => notifyCenter?.notify(wake),
|
|
654
|
+
logEvent,
|
|
655
|
+
}),
|
|
563
656
|
});
|
|
564
657
|
registry.setRunner(runner);
|
|
565
658
|
subagentRegistry = registry;
|
|
@@ -576,18 +669,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
576
669
|
run_id: run.runId,
|
|
577
670
|
name: c.name,
|
|
578
671
|
agent: c.agent,
|
|
672
|
+
work_kind: c.workKind ?? "other",
|
|
673
|
+
queue_ms: c.queueMs ?? 0,
|
|
579
674
|
...(c.model ? { model: c.model } : {}),
|
|
580
675
|
});
|
|
581
676
|
}
|
|
582
677
|
if (["completed", "failed", "interrupted"].includes(c.status) && previousStatus !== c.status) {
|
|
583
678
|
const error = c.result?.error;
|
|
679
|
+
// Only the opt-in hard ceiling settles with error=timeout now.
|
|
584
680
|
if (error === "timeout") logEvent("agent.timeout", { child_id: c.childId });
|
|
585
681
|
logEvent("agent.settle", {
|
|
586
682
|
child_id: c.childId,
|
|
587
683
|
status: c.status,
|
|
588
684
|
...(error ? { error } : {}),
|
|
685
|
+
work_kind: c.workKind ?? "other",
|
|
589
686
|
...(c.result?.stalls ? { stalls: c.result.stalls } : {}),
|
|
687
|
+
...(c.queueMs !== undefined ? { queue_ms: c.queueMs } : {}),
|
|
590
688
|
duration_ms: c.result?.durationMs ?? Math.max(0, clock.now() - c.startedAt),
|
|
689
|
+
...(c.result?.queueMs !== undefined ? { queue_ms: c.result.queueMs } : {}),
|
|
591
690
|
});
|
|
592
691
|
}
|
|
593
692
|
previousAgentStatus.set(c.childId, c.status);
|
|
@@ -599,9 +698,20 @@ export default function (pi: ExtensionAPI): void {
|
|
|
599
698
|
session_id: sid,
|
|
600
699
|
name: c.name,
|
|
601
700
|
agent: c.agent,
|
|
701
|
+
work_kind: c.workKind ?? "other",
|
|
702
|
+
...(c.queueMs !== undefined ? { queue_ms: c.queueMs } : {}),
|
|
602
703
|
...(c.model !== undefined ? { model: c.model } : {}),
|
|
603
704
|
status: c.status,
|
|
604
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,
|
|
605
715
|
...(c.endedAt !== undefined ? { ended_at: c.endedAt } : {}),
|
|
606
716
|
...(c.result?.error ? { error: c.result.error } : {}),
|
|
607
717
|
...(c.result?.attempts !== undefined && c.result.attempts > 1
|
|
@@ -630,6 +740,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
630
740
|
runId: run.runId,
|
|
631
741
|
name: c.name,
|
|
632
742
|
agent: c.agent,
|
|
743
|
+
workKind: c.workKind ?? "other",
|
|
744
|
+
...(c.queueMs !== undefined ? { queueMs: c.queueMs } : {}),
|
|
633
745
|
...(c.model !== undefined ? { model: c.model } : {}),
|
|
634
746
|
cwd: startCtx.cwd,
|
|
635
747
|
...(c.prompt !== undefined ? { prompt: c.prompt } : {}),
|
|
@@ -654,7 +766,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
654
766
|
void c
|
|
655
767
|
.connect()
|
|
656
768
|
.then((ok) => {
|
|
657
|
-
if (
|
|
769
|
+
if (ok) {
|
|
770
|
+
if (c.protocolLevel() >= 4) void reconcileAgentLeases(c).catch(() => {});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (startCtx.hasUI) {
|
|
658
774
|
const detail = c.lastError();
|
|
659
775
|
const reason = detail ? ` (${detail})` : "";
|
|
660
776
|
const searched = managerPath ? `using ${managerPath}` : `looked in: ${describeManagerSearch(config, home)}`;
|
|
@@ -673,19 +789,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
673
789
|
exitWatchdog.dispose();
|
|
674
790
|
fleetWidget?.dispose();
|
|
675
791
|
fleetWidget = null;
|
|
676
|
-
|
|
792
|
+
comms.dispose(); // Release decision waiters before awaiting child aborts.
|
|
793
|
+
await subagentRegistry?.disposeAll().catch(() => {});
|
|
677
794
|
subagentRegistry = null;
|
|
678
795
|
agentLoader = null;
|
|
679
|
-
comms.dispose(); // resolve orphaned need_decision waiters
|
|
680
796
|
monitorRegistry?.disposeAll();
|
|
681
797
|
notifyCenter?.dispose();
|
|
682
798
|
const current = client;
|
|
683
799
|
client = null;
|
|
684
800
|
if (current) {
|
|
685
|
-
|
|
686
|
-
|
|
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(() => {});
|
|
687
807
|
}
|
|
688
|
-
await current.close();
|
|
689
808
|
}
|
|
690
809
|
ctx = null;
|
|
691
810
|
});
|
|
@@ -694,7 +813,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
694
813
|
notifyCenter?.settled();
|
|
695
814
|
});
|
|
696
815
|
|
|
697
|
-
|
|
698
|
-
|
|
816
|
+
// A wake entering the model's context (steer / triggerTurn); see wake-delivery.ts.
|
|
817
|
+
pi.on("message_end", async (event) => {
|
|
818
|
+
const message = onWakeMessageEnd(event.message, {
|
|
819
|
+
now: () => clock.now(),
|
|
820
|
+
logEvent,
|
|
821
|
+
lookup: () => (subagentRegistry ? registryStatusLookup(subagentRegistry) : null),
|
|
822
|
+
});
|
|
823
|
+
return message ? { message } : undefined;
|
|
699
824
|
});
|
|
700
825
|
}
|