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/subagent/tool.ts
CHANGED
|
@@ -22,9 +22,10 @@ import { realClock, type Clock, type ClockTimer } from "../clock";
|
|
|
22
22
|
import type { NotifyCenter } from "../notify";
|
|
23
23
|
import { statusGlyph, toolComponent } from "../tui/tool-component";
|
|
24
24
|
import type { WorkIndex } from "../work-index";
|
|
25
|
+
import { fill, PROMPTS } from "../prompts.generated";
|
|
25
26
|
import { runChain, runTasks, validateChainSteps } from "./pool";
|
|
26
27
|
import type { RunRecord, SubagentRegistry } from "./registry";
|
|
27
|
-
import type
|
|
28
|
+
import { AGENT_WORK_KINDS, type AgentDefinition, type AgentWorkKind, type ChildHandle, type ChildResult, type ChildRunRequest } from "./types";
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
/** Overall result text cap (§4.6: truncateTail 512 lines / 48KB). */
|
|
@@ -38,54 +39,48 @@ const MIN_TIMEOUT_MS = 1_000;
|
|
|
38
39
|
// Schema (§4.6 field-name contract)
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
40
41
|
|
|
42
|
+
const workKindSchema = Type.Union(AGENT_WORK_KINDS.map((kind) => Type.Literal(kind)), {
|
|
43
|
+
description: PROMPTS["tools.subagent.param.work_kind"],
|
|
44
|
+
});
|
|
45
|
+
|
|
41
46
|
const taskItem = Type.Object({
|
|
42
|
-
agent: Type.Optional(Type.String({ description: "
|
|
43
|
-
prompt: Type.String({ description: "
|
|
44
|
-
name: Type.Optional(Type.String({ description: "
|
|
47
|
+
agent: Type.Optional(Type.String({ description: PROMPTS["tools.subagent.param.tasks.agent"] })),
|
|
48
|
+
prompt: Type.String({ description: PROMPTS["tools.subagent.param.tasks.prompt"] }),
|
|
49
|
+
name: Type.Optional(Type.String({ description: PROMPTS["tools.subagent.param.tasks.name"] })),
|
|
50
|
+
work_kind: Type.Optional(workKindSchema),
|
|
45
51
|
});
|
|
46
52
|
|
|
47
53
|
const chainItem = Type.Object({
|
|
48
|
-
agent: Type.Optional(Type.String({ description: "
|
|
49
|
-
prompt: Type.String({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
label: Type.Optional(Type.String({ description: "Label for referencing this step's output later" })),
|
|
54
|
+
agent: Type.Optional(Type.String({ description: PROMPTS["tools.subagent.param.chain.agent"] })),
|
|
55
|
+
prompt: Type.String({ description: PROMPTS["tools.subagent.param.chain.prompt"] }),
|
|
56
|
+
label: Type.Optional(Type.String({ description: PROMPTS["tools.subagent.param.chain.label"] })),
|
|
57
|
+
work_kind: Type.Optional(workKindSchema),
|
|
53
58
|
});
|
|
54
59
|
|
|
55
60
|
const subagentParameters = Type.Object({
|
|
56
61
|
tasks: Type.Optional(
|
|
57
|
-
Type.Array(taskItem, { minItems: 1, maxItems: 10, description: "
|
|
62
|
+
Type.Array(taskItem, { minItems: 1, maxItems: 10, description: PROMPTS["tools.subagent.param.tasks"] }),
|
|
58
63
|
),
|
|
59
64
|
chain: Type.Optional(
|
|
60
|
-
Type.Array(chainItem, { minItems: 1, description: "
|
|
65
|
+
Type.Array(chainItem, { minItems: 1, description: PROMPTS["tools.subagent.param.chain"] }),
|
|
61
66
|
),
|
|
62
67
|
async: Type.Optional(
|
|
63
|
-
Type.Boolean({ description: "
|
|
68
|
+
Type.Boolean({ description: PROMPTS["tools.subagent.param.async"] }),
|
|
64
69
|
),
|
|
65
70
|
concurrency: Type.Optional(
|
|
66
|
-
Type.Number({ minimum: 1, maximum: 8, description: "
|
|
71
|
+
Type.Number({ minimum: 1, maximum: 8, description: PROMPTS["tools.subagent.param.concurrency"] }),
|
|
67
72
|
),
|
|
68
73
|
fail_fast: Type.Optional(
|
|
69
|
-
Type.Boolean({
|
|
70
|
-
description: "Cancel not-yet-started subagents on first failure (already-started ones finish)",
|
|
71
|
-
}),
|
|
74
|
+
Type.Boolean({ description: PROMPTS["tools.subagent.param.fail_fast"] }),
|
|
72
75
|
),
|
|
73
76
|
model: Type.Optional(
|
|
74
77
|
Type.String({
|
|
75
|
-
description:
|
|
76
|
-
'Model override for all subagents: fuzzy ("haiku"), qualified ("provider/id"), ' +
|
|
77
|
-
`optionally with ":<thinking>" suffix (${VALID_THINKING_LEVELS.join(", ")}; ` +
|
|
78
|
-
'an unknown suffix is dropped with a warning when the base resolves; ' +
|
|
79
|
-
'an unknown base still errors). Default: current model. ' +
|
|
80
|
-
'Use action:"models" to list selectable values.',
|
|
78
|
+
description: fill("tools.subagent.param.model", { thinkingLevels: VALID_THINKING_LEVELS.join(", ") }),
|
|
81
79
|
}),
|
|
82
80
|
),
|
|
83
81
|
timeout_ms: Type.Optional(
|
|
84
82
|
Type.Number({
|
|
85
|
-
description:
|
|
86
|
-
`Time budget per subagent turn in ms (default 1800000, max ${MAX_TIMEOUT_MS}). ` +
|
|
87
|
-
'Passing it does not stop the subagent: you get <pi-famulus-wake kind="subagent-overrun"> ' +
|
|
88
|
-
"and choose extend, steer, or interrupt. With resume: the resumed turn's budget. With extend: the new budget from now.",
|
|
83
|
+
description: fill("tools.subagent.param.timeout_ms", { maxMs: MAX_TIMEOUT_MS }),
|
|
89
84
|
maximum: MAX_TIMEOUT_MS,
|
|
90
85
|
}),
|
|
91
86
|
),
|
|
@@ -101,19 +96,19 @@ const subagentParameters = Type.Object({
|
|
|
101
96
|
Type.Literal("extend"),
|
|
102
97
|
Type.Literal("models"),
|
|
103
98
|
],
|
|
104
|
-
{ description: "
|
|
99
|
+
{ description: PROMPTS["tools.subagent.param.action"] },
|
|
105
100
|
),
|
|
106
101
|
),
|
|
107
|
-
run_id: Type.Optional(Type.String({ description: "
|
|
102
|
+
run_id: Type.Optional(Type.String({ description: PROMPTS["tools.subagent.param.run_id"] })),
|
|
108
103
|
child_id: Type.Optional(
|
|
109
|
-
Type.String({ description: "
|
|
104
|
+
Type.String({ description: PROMPTS["tools.subagent.param.child_id"] }),
|
|
110
105
|
),
|
|
111
|
-
message: Type.Optional(Type.String({ description: "
|
|
106
|
+
message: Type.Optional(Type.String({ description: PROMPTS["tools.subagent.param.message"] })),
|
|
112
107
|
});
|
|
113
108
|
|
|
114
109
|
type SubagentParams = {
|
|
115
|
-
tasks?: { agent?: string; prompt: string; name?: string }[];
|
|
116
|
-
chain?: { agent?: string; prompt: string; label?: string }[];
|
|
110
|
+
tasks?: { agent?: string; prompt: string; name?: string; work_kind?: AgentWorkKind }[];
|
|
111
|
+
chain?: { agent?: string; prompt: string; label?: string; work_kind?: AgentWorkKind }[];
|
|
117
112
|
async?: boolean;
|
|
118
113
|
concurrency?: number;
|
|
119
114
|
fail_fast?: boolean;
|
|
@@ -156,8 +151,7 @@ export interface SubagentToolDeps {
|
|
|
156
151
|
/** M3 stopgap resolver until M5 wires the real agent loader (§4.8). */
|
|
157
152
|
const BUILTIN_WORKER: AgentDefinition = {
|
|
158
153
|
name: "worker",
|
|
159
|
-
description: "General-purpose subagent with
|
|
160
|
-
tools: ["read", "bash", "edit", "write"],
|
|
154
|
+
description: "General-purpose subagent with user-configured tools",
|
|
161
155
|
systemPrompt: "",
|
|
162
156
|
source: "builtin",
|
|
163
157
|
};
|
|
@@ -373,12 +367,12 @@ export function createSubagentTool(
|
|
|
373
367
|
const label = "name" in item ? item.name : undefined;
|
|
374
368
|
const chainLabel = "label" in item ? item.label : undefined;
|
|
375
369
|
const name = label ?? chainLabel ?? `${agent.name}-${i + 1}`;
|
|
376
|
-
return { agent, name, prompt: item.prompt };
|
|
370
|
+
return { agent, name, prompt: item.prompt, workKind: item.work_kind ?? "other" };
|
|
377
371
|
});
|
|
378
372
|
|
|
379
373
|
const run = registry.createRun(kind);
|
|
380
374
|
const childIds = resolved.map((r) =>
|
|
381
|
-
registry.addChild(run.runId, { name: r.name, agent: r.agent.name }),
|
|
375
|
+
registry.addChild(run.runId, { name: r.name, agent: r.agent.name, workKind: r.workKind }),
|
|
382
376
|
);
|
|
383
377
|
|
|
384
378
|
const makeRequest = (ordinal: number, prompt: string): ChildRunRequest => ({
|
|
@@ -389,6 +383,7 @@ export function createSubagentTool(
|
|
|
389
383
|
taskPrompt: prompt,
|
|
390
384
|
agent: resolved[ordinal].agent,
|
|
391
385
|
model: params.model,
|
|
386
|
+
workKind: resolved[ordinal].workKind,
|
|
392
387
|
timeoutMs,
|
|
393
388
|
depth: 1,
|
|
394
389
|
});
|
|
@@ -452,13 +447,7 @@ export function createSubagentTool(
|
|
|
452
447
|
content: [
|
|
453
448
|
{
|
|
454
449
|
type: "text",
|
|
455
|
-
text:
|
|
456
|
-
`Started ${items.length} subagent(s) in run ${run.runId}. ${reason}\n` +
|
|
457
|
-
`While others are still running, each finished subagent arrives as <pi-famulus-wake kind="subagent-handover"> ` +
|
|
458
|
-
`with that child's prompt and result. Read it and continue: subagent({action:"resume", run_id, child_id, message}) for that child, ` +
|
|
459
|
-
`or agent_message to steer the ones still running. Do not wait for the whole run. Do not poll. ` +
|
|
460
|
-
`<pi-famulus-wake kind="subagent-done"> arrives when every subagent in the run has finished. ` +
|
|
461
|
-
`Use subagent({action:"get", run_id:"${run.runId}"}) if you need the full record.`,
|
|
450
|
+
text: fill("tools.subagent.result.backgrounded", { count: items.length, runId: run.runId, reason }),
|
|
462
451
|
},
|
|
463
452
|
],
|
|
464
453
|
details: { run_id: run.runId, status: "backgrounded" },
|
|
@@ -624,15 +613,18 @@ export function createSubagentTool(
|
|
|
624
613
|
const hard =
|
|
625
614
|
hardDeadlineAt === null
|
|
626
615
|
? ""
|
|
627
|
-
: `
|
|
616
|
+
: ` ${fill("tools.subagent.result.extended.hard-ceiling", { remaining: formatDurationMs(Math.max(0, hardDeadlineAt - now)) })}`;
|
|
628
617
|
return {
|
|
629
618
|
content: [
|
|
630
619
|
{
|
|
631
620
|
type: "text",
|
|
632
621
|
text:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
622
|
+
fill("tools.subagent.result.extended", {
|
|
623
|
+
name: child?.name ?? handle.childId,
|
|
624
|
+
childId: handle.childId,
|
|
625
|
+
runId: record.runId,
|
|
626
|
+
next: formatDurationMs(deadlineAt - now),
|
|
627
|
+
}) + hard,
|
|
636
628
|
},
|
|
637
629
|
],
|
|
638
630
|
details: { run_id: record.runId, child_id: handle.childId, deadline_at: deadlineAt, hard_deadline_at: hardDeadlineAt },
|
|
@@ -662,11 +654,12 @@ export function createSubagentTool(
|
|
|
662
654
|
content: [
|
|
663
655
|
{
|
|
664
656
|
type: "text",
|
|
665
|
-
text:
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
657
|
+
text: fill("tools.subagent.result.resumed", {
|
|
658
|
+
name: child.name,
|
|
659
|
+
childId: child.childId,
|
|
660
|
+
runId: record.runId,
|
|
661
|
+
queued: queuedText(queuedBehind),
|
|
662
|
+
}),
|
|
670
663
|
},
|
|
671
664
|
],
|
|
672
665
|
details: { run_id: record.runId, child_id: child.childId, queued_behind: queuedBehind },
|
|
@@ -676,22 +669,9 @@ export function createSubagentTool(
|
|
|
676
669
|
return {
|
|
677
670
|
name: "subagent",
|
|
678
671
|
label: "Subagent",
|
|
679
|
-
description:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
"continue in the background. Each child that finishes while others are still running wakes you with " +
|
|
683
|
-
"<pi-famulus-wake kind=\"subagent-handover\"> (its prompt and result). The whole run wakes you with <pi-famulus-wake kind=\"subagent-done\">. " +
|
|
684
|
-
"Never poll or sleep to wait. " +
|
|
685
|
-
"A subagent still running past its timeout_ms is not stopped; it wakes you with " +
|
|
686
|
-
"<pi-famulus-wake kind=\"subagent-overrun\"> so you can extend, steer, or interrupt it. " +
|
|
687
|
-
"Use action=list/get/status/interrupt/resume/steer/extend to manage existing runs.",
|
|
688
|
-
promptSnippet: "Fan out subagents in parallel or sequence them in a chain",
|
|
689
|
-
promptGuidelines: [
|
|
690
|
-
'When a <pi-famulus-wake kind="subagent-handover"> arrives, read <prompt> and <result> immediately and continue: subagent({action:"resume", run_id, child_id, message}) for that child, or agent_message to steer children that are still running. Do not wait for the rest of the run.',
|
|
691
|
-
"Subagent runs that exceed the foreground budget continue in the background; you are notified per finished child and again when the run completes — do not poll.",
|
|
692
|
-
"A failed subagent does not fail the whole run; inspect per-subagent sections in the result.",
|
|
693
|
-
"<pi-famulus-wake> is a system wake, not a user reply. kind=subagent-handover is one child; kind=subagent-done is the whole run.",
|
|
694
|
-
],
|
|
672
|
+
description: PROMPTS["tools.subagent.description"],
|
|
673
|
+
promptSnippet: PROMPTS["tools.subagent.snippet"],
|
|
674
|
+
promptGuidelines: [...PROMPTS["tools.subagent.rules"]],
|
|
695
675
|
parameters: subagentParameters,
|
|
696
676
|
renderResult(result, { expanded }, theme, context) {
|
|
697
677
|
const details = result.details as { run_id?: string; status?: string } | undefined;
|
|
@@ -796,10 +776,7 @@ function resolveSingleTerminalChild(record: RunRecord, childIdOrName: string | u
|
|
|
796
776
|
if (child.status === "pending") {
|
|
797
777
|
// Queued for an admission slot (a launch or an earlier resume): steer
|
|
798
778
|
// would fail too, and a second resume adds nothing.
|
|
799
|
-
throw new Error(
|
|
800
|
-
`subagent ${child.name} (${child.childId}) is already queued and starts when a subagent slot frees; ` +
|
|
801
|
-
"its result arrives as a wake when it finishes. Do not resume it again.",
|
|
802
|
-
);
|
|
779
|
+
throw new Error(fill("tools.subagent.error.resume-queued", { name: child.name, childId: child.childId }));
|
|
803
780
|
}
|
|
804
781
|
if (!matches(child)) {
|
|
805
782
|
throw new Error(`subagent ${child.name} (${child.childId}) is still ${child.status}; use steer instead`);
|
package/src/subagent/types.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* consumer stays testable with plain fakes.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { OverrunTick } from "./overrun";
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Mirror of the M5 `AgentDefinition` (design doc Appendix B, src/agents/).
|
|
11
13
|
* Structurally compatible with the M5 definition, so the real agent loader
|
|
@@ -14,7 +16,7 @@
|
|
|
14
16
|
export interface AgentDefinition {
|
|
15
17
|
name: string; // ^[a-z][a-z0-9-]*$
|
|
16
18
|
description: string; // required, non-empty
|
|
17
|
-
tools
|
|
19
|
+
tools?: string[]; // omitted: inherit user config; explicit list: allowlist; []: no user-configured tools (injected communication tools remain)
|
|
18
20
|
model?: string; // "provider:id" | bare id
|
|
19
21
|
// Mirror of src/agents/definition.ts — keep in sync with
|
|
20
22
|
// src/thinking-levels.ts (VALID_THINKING_LEVELS), the single source.
|
|
@@ -26,6 +28,17 @@ export interface AgentDefinition {
|
|
|
26
28
|
|
|
27
29
|
export type ChildStatus = "pending" | "running" | "completed" | "failed" | "interrupted";
|
|
28
30
|
|
|
31
|
+
export const AGENT_WORK_KINDS = [
|
|
32
|
+
"test-suite",
|
|
33
|
+
"test",
|
|
34
|
+
"build",
|
|
35
|
+
"lint/type",
|
|
36
|
+
"other",
|
|
37
|
+
"git",
|
|
38
|
+
"read/search",
|
|
39
|
+
] as const;
|
|
40
|
+
export type AgentWorkKind = (typeof AGENT_WORK_KINDS)[number];
|
|
41
|
+
|
|
29
42
|
export interface ChildResult {
|
|
30
43
|
status: "completed" | "failed" | "interrupted";
|
|
31
44
|
text: string; // getLastAssistantText() or "(no output)"
|
|
@@ -43,6 +56,8 @@ export interface ChildResult {
|
|
|
43
56
|
/** Stall detections in the user turn that produced this result. */
|
|
44
57
|
stalls?: number;
|
|
45
58
|
durationMs: number;
|
|
59
|
+
/** Time waiting for local and machine-wide admission in this turn. */
|
|
60
|
+
queueMs?: number;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
/** One turn of a child session, for the /tasks conversation view. */
|
|
@@ -60,11 +75,36 @@ export interface ChildRunRequest {
|
|
|
60
75
|
taskPrompt?: string;
|
|
61
76
|
agent: AgentDefinition; // already resolved
|
|
62
77
|
model?: string; // subagent() parameter-level override
|
|
78
|
+
/** Explicit planned workload class; natural-language prompts are not classified. */
|
|
79
|
+
workKind?: AgentWorkKind;
|
|
63
80
|
/** Soft budget per turn: reaching it wakes the parent; it does not abort. */
|
|
64
81
|
timeoutMs: number;
|
|
65
82
|
depth: number; // main session = 0, child = 1
|
|
66
83
|
}
|
|
67
84
|
|
|
85
|
+
export interface ChildTokenUsage {
|
|
86
|
+
input: number;
|
|
87
|
+
output: number;
|
|
88
|
+
cacheRead: number;
|
|
89
|
+
cacheWrite: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ChildWallUsage {
|
|
93
|
+
llmMs: number;
|
|
94
|
+
toolMs: number;
|
|
95
|
+
queueMs: number;
|
|
96
|
+
otherMs: number;
|
|
97
|
+
approximate: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ChildSessionEvent {
|
|
101
|
+
type: string;
|
|
102
|
+
role?: string;
|
|
103
|
+
/** Pi's stable tool-call id, used to pair execution start/end events. */
|
|
104
|
+
toolCallId?: string;
|
|
105
|
+
usage?: ChildTokenUsage;
|
|
106
|
+
}
|
|
107
|
+
|
|
68
108
|
export interface ChildHandle {
|
|
69
109
|
readonly childId: string;
|
|
70
110
|
readonly result: Promise<ChildResult>; // resolves exactly once per generation;
|
|
@@ -85,8 +125,16 @@ export interface ChildHandle {
|
|
|
85
125
|
lastEventAt(): number; // for the stall watchdog / status display
|
|
86
126
|
/** Resolved `provider/id` once the child session has been constructed. */
|
|
87
127
|
resolvedModel(): string | undefined;
|
|
128
|
+
/** Effective thinking level applied when the child session was constructed. */
|
|
129
|
+
effectiveThinkingLevel?(): string | undefined;
|
|
130
|
+
/** Collect and reschedule a reminder if this child is overdue at the given time. */
|
|
131
|
+
collectDueOverrun?(now: number): OverrunTick | undefined;
|
|
88
132
|
/** Live child transcript. Empty when the session never started. */
|
|
89
133
|
conversation(): ConversationTurn[];
|
|
134
|
+
/** Cumulative provider input/output and cache counts for this child. */
|
|
135
|
+
tokenUsage(): ChildTokenUsage;
|
|
136
|
+
/** Cumulative observed LLM, tool, admission-queue and approximate wall time. */
|
|
137
|
+
wallUsage(): ChildWallUsage;
|
|
90
138
|
}
|
|
91
139
|
|
|
92
140
|
/**
|
|
@@ -101,6 +149,8 @@ export interface ChildSessionAdapter {
|
|
|
101
149
|
* Used for fleet/ls persistence and so the child prompt can name its model.
|
|
102
150
|
*/
|
|
103
151
|
readonly resolvedModel?: string;
|
|
152
|
+
/** Effective thinking level passed to createAgentSession, after model fallbacks. */
|
|
153
|
+
readonly effectiveThinkingLevel?: string;
|
|
104
154
|
prompt(text: string): Promise<void>;
|
|
105
155
|
steer(text: string): Promise<void>;
|
|
106
156
|
followUp(text: string): Promise<void>;
|
|
@@ -110,18 +160,22 @@ export interface ChildSessionAdapter {
|
|
|
110
160
|
/** Provider/model failure outcome of the last assistant turn, when any. */
|
|
111
161
|
getLastAssistantFailure?(): { stopReason: "error" | "aborted"; errorMessage?: string } | undefined;
|
|
112
162
|
getConversation(): ConversationTurn[];
|
|
163
|
+
tokenUsage(): ChildTokenUsage;
|
|
113
164
|
/** Introspection for child-session orchestration and contract tests. */
|
|
114
165
|
getActiveToolNames?(): string[];
|
|
115
166
|
getSystemPrompt?(): string;
|
|
116
167
|
isStreaming(): boolean;
|
|
117
|
-
|
|
118
|
-
|
|
168
|
+
/** Current queued steering drain mode, when supported by the runtime. */
|
|
169
|
+
getSteeringMode?(): "all" | "one-at-a-time";
|
|
170
|
+
subscribe(listener: (event: ChildSessionEvent) => void): () => void;
|
|
171
|
+
/** Await extension cleanup before releasing the underlying session. */
|
|
172
|
+
dispose(): void | Promise<void>;
|
|
119
173
|
}
|
|
120
174
|
|
|
121
175
|
export type CreateSessionFn = (req: ChildRunRequest) => Promise<ChildSessionAdapter>;
|
|
122
176
|
|
|
123
177
|
export interface ChildRunner {
|
|
124
|
-
start(req: ChildRunRequest): Promise<ChildHandle>;
|
|
178
|
+
start(req: ChildRunRequest, onHandle?: (handle: ChildHandle) => void): Promise<ChildHandle>;
|
|
125
179
|
}
|
|
126
180
|
|
|
127
181
|
/**
|
|
@@ -130,5 +184,6 @@ export interface ChildRunner {
|
|
|
130
184
|
* duck-types this to release sessions in disposeRun().
|
|
131
185
|
*/
|
|
132
186
|
export interface DisposableChildHandle extends ChildHandle {
|
|
133
|
-
|
|
187
|
+
/** Settles immediately; an optional promise tracks asynchronous session cleanup. */
|
|
188
|
+
dispose(): void | Promise<void>;
|
|
134
189
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface TokenUsage {
|
|
2
|
+
input: number;
|
|
3
|
+
output: number;
|
|
4
|
+
cacheRead: number;
|
|
5
|
+
cacheWrite: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Normalize provider usage counts while keeping missing values at zero. */
|
|
9
|
+
export function normalizedTokenUsage(usage: {
|
|
10
|
+
input?: unknown;
|
|
11
|
+
output?: unknown;
|
|
12
|
+
cacheRead?: unknown;
|
|
13
|
+
cacheWrite?: unknown;
|
|
14
|
+
}): TokenUsage {
|
|
15
|
+
const count = (value: unknown): number => {
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
input: count(usage.input),
|
|
21
|
+
output: count(usage.output),
|
|
22
|
+
cacheRead: count(usage.cacheRead),
|
|
23
|
+
cacheWrite: count(usage.cacheWrite),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Add a provider message's usage to the child's cumulative totals. */
|
|
28
|
+
export function accumulateTokenUsage(total: TokenUsage, delta: TokenUsage): TokenUsage {
|
|
29
|
+
return {
|
|
30
|
+
input: total.input + delta.input,
|
|
31
|
+
output: total.output + delta.output,
|
|
32
|
+
cacheRead: total.cacheRead + delta.cacheRead,
|
|
33
|
+
cacheWrite: total.cacheWrite + delta.cacheWrite,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createTokenUsageAccumulator() {
|
|
38
|
+
let total: TokenUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
39
|
+
return {
|
|
40
|
+
add(usage: { input?: unknown; output?: unknown; cacheRead?: unknown; cacheWrite?: unknown }): TokenUsage {
|
|
41
|
+
total = accumulateTokenUsage(total, normalizedTokenUsage(usage));
|
|
42
|
+
return { ...total };
|
|
43
|
+
},
|
|
44
|
+
snapshot(): TokenUsage {
|
|
45
|
+
return { ...total };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/task-tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
import { realClock, type Clock } from "./clock";
|
|
6
6
|
import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { ManagerClient, TaskRecord } from "./manager-client";
|
|
8
|
+
import { fill, PROMPTS } from "./prompts.generated";
|
|
8
9
|
import {
|
|
9
10
|
formatAgentCommand,
|
|
10
11
|
isAgentStatusActive,
|
|
@@ -85,14 +86,12 @@ function formatTaskLine(task: TaskRecord, now: number): string {
|
|
|
85
86
|
);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
const END_TURN_HINT =
|
|
89
|
-
|
|
90
|
-
const LIST_END_TURN_HINT =
|
|
91
|
-
"Running work wakes you with a <pi-famulus-wake> when it finishes. Reply to the user now with no tool call instead of checking again.";
|
|
89
|
+
const END_TURN_HINT = PROMPTS["tools.task_output.hint.still-running"];
|
|
90
|
+
const LIST_END_TURN_HINT = PROMPTS["tools.task_list.hint.running"];
|
|
92
91
|
|
|
93
92
|
const taskListParameters = Type.Object({
|
|
94
93
|
all: Type.Optional(
|
|
95
|
-
Type.Boolean({ description: "
|
|
94
|
+
Type.Boolean({ description: PROMPTS["tools.task_list.param.all"] }),
|
|
96
95
|
),
|
|
97
96
|
});
|
|
98
97
|
|
|
@@ -102,10 +101,8 @@ export function createTaskListTool(
|
|
|
102
101
|
return {
|
|
103
102
|
name: "task_list",
|
|
104
103
|
label: "Task List",
|
|
105
|
-
description:
|
|
106
|
-
|
|
107
|
-
"Work from other pi sessions is never shown.",
|
|
108
|
-
promptSnippet: "List background shell/monitor tasks and subagents",
|
|
104
|
+
description: PROMPTS["tools.task_list.description"],
|
|
105
|
+
promptSnippet: PROMPTS["tools.task_list.snippet"],
|
|
109
106
|
parameters: taskListParameters,
|
|
110
107
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
111
108
|
const client = await requireClient(deps);
|
|
@@ -180,15 +177,12 @@ export function createTaskListTool(
|
|
|
180
177
|
}
|
|
181
178
|
|
|
182
179
|
const taskOutputParameters = Type.Object({
|
|
183
|
-
task_id: Type.String({ description: "
|
|
180
|
+
task_id: Type.String({ description: PROMPTS["tools.task_output.param.task_id"] }),
|
|
184
181
|
cursor: Type.Optional(
|
|
185
|
-
Type.Number({
|
|
186
|
-
description:
|
|
187
|
-
"Byte offset to read from (for incremental reads). Omit to read the tail of the output.",
|
|
188
|
-
}),
|
|
182
|
+
Type.Number({ description: PROMPTS["tools.task_output.param.cursor"] }),
|
|
189
183
|
),
|
|
190
184
|
max_bytes: Type.Optional(
|
|
191
|
-
Type.Number({ description: "
|
|
185
|
+
Type.Number({ description: PROMPTS["tools.task_output.param.max_bytes"] }),
|
|
192
186
|
),
|
|
193
187
|
});
|
|
194
188
|
|
|
@@ -211,7 +205,7 @@ export function createTaskOutputTool(
|
|
|
211
205
|
const lastRunningSize = new Map<string, number>();
|
|
212
206
|
const guardPoll = (id: string, running: boolean, size: number) => {
|
|
213
207
|
if (running && lastRunningSize.get(id) === size) {
|
|
214
|
-
throw new Error(
|
|
208
|
+
throw new Error(fill("tools.task_output.error.no-new-output", { taskId: id, hint: END_TURN_HINT }));
|
|
215
209
|
}
|
|
216
210
|
if (running) lastRunningSize.set(id, size);
|
|
217
211
|
else lastRunningSize.delete(id);
|
|
@@ -219,11 +213,8 @@ export function createTaskOutputTool(
|
|
|
219
213
|
return {
|
|
220
214
|
name: "task_output",
|
|
221
215
|
label: "Task Output",
|
|
222
|
-
description:
|
|
223
|
-
|
|
224
|
-
"plus the current file pointer; pass the returned next_cursor as cursor for incremental reads. " +
|
|
225
|
-
"Not a way to wait: a running task wakes you when it finishes, so reply with no tool call instead of calling this again.",
|
|
226
|
-
promptSnippet: "Read background task output",
|
|
216
|
+
description: PROMPTS["tools.task_output.description"],
|
|
217
|
+
promptSnippet: PROMPTS["tools.task_output.snippet"],
|
|
227
218
|
parameters: taskOutputParameters,
|
|
228
219
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
229
220
|
const agentText = agentTextFor(deps, params.task_id);
|
|
@@ -280,7 +271,7 @@ export function createTaskOutputTool(
|
|
|
280
271
|
}
|
|
281
272
|
|
|
282
273
|
const taskStopParameters = Type.Object({
|
|
283
|
-
task_id: Type.String({ description: "
|
|
274
|
+
task_id: Type.String({ description: PROMPTS["tools.task_stop.param.task_id"] }),
|
|
284
275
|
});
|
|
285
276
|
|
|
286
277
|
export function createTaskStopTool(
|
|
@@ -289,9 +280,8 @@ export function createTaskStopTool(
|
|
|
289
280
|
return {
|
|
290
281
|
name: "task_stop",
|
|
291
282
|
label: "Task Stop",
|
|
292
|
-
description:
|
|
293
|
-
|
|
294
|
-
promptSnippet: "Stop a background task",
|
|
283
|
+
description: PROMPTS["tools.task_stop.description"],
|
|
284
|
+
promptSnippet: PROMPTS["tools.task_stop.snippet"],
|
|
295
285
|
parameters: taskStopParameters,
|
|
296
286
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx: ExtensionContext) {
|
|
297
287
|
const agent = deps.getRegistry?.()?.handle(params.task_id);
|
|
@@ -102,7 +102,8 @@ function collapsedText(details: FamulusWake, theme: Theme): string {
|
|
|
102
102
|
const shell = details.shell
|
|
103
103
|
? ` · shell ${formatDuration(details.shell.elapsedMs)}${details.shell.growing === null ? "" : details.shell.growing ? " (output growing)" : " (output idle)"}`
|
|
104
104
|
: "";
|
|
105
|
-
|
|
105
|
+
const additional = details.additional?.length ? ` · +${details.additional.length} more overdue` : "";
|
|
106
|
+
return `${theme.fg("warning", "!")} ${theme.fg("muted", "overrun")} ${details.name} ${formatDuration(details.elapsedMs)} / ${formatDuration(details.budgetMs)} budget · still running${additional}${shell}`;
|
|
106
107
|
}
|
|
107
108
|
case "supervisor-request":
|
|
108
109
|
return [
|
|
@@ -168,6 +169,21 @@ export function expandedWakeText(details: FamulusWake | undefined, content: stri
|
|
|
168
169
|
...(details.hardCeilingMs === undefined || details.nextReminderMs < details.hardCeilingMs
|
|
169
170
|
? [`Next reminder in ${formatDuration(details.nextReminderMs)} unless you extend, steer, or interrupt it.`]
|
|
170
171
|
: []),
|
|
172
|
+
...(details.additional?.length
|
|
173
|
+
? [
|
|
174
|
+
`Also overdue (${details.additional.length}):`,
|
|
175
|
+
...details.additional.map((child) => [
|
|
176
|
+
`${child.name} (${child.childId}) · run ${child.runId}`,
|
|
177
|
+
`Elapsed this turn: ${formatDuration(child.elapsedMs)} of ${formatDuration(child.budgetMs)}`,
|
|
178
|
+
`Last activity (${formatDuration(child.lastActivity.agoMs)} ago): ${child.lastActivity.text}`,
|
|
179
|
+
...(child.shell ? [`Shell ${child.shell.taskId} · ${formatDuration(child.shell.elapsedMs)}\n$ ${child.shell.command}\nOutput: ${child.shell.outputPath}`] : []),
|
|
180
|
+
...(child.hardCeilingMs !== undefined ? [`Hard ceiling stops it in ${formatDuration(child.hardCeilingMs)}.`] : []),
|
|
181
|
+
...(child.hardCeilingMs === undefined || child.nextReminderMs < child.hardCeilingMs
|
|
182
|
+
? [`Next reminder in ${formatDuration(child.nextReminderMs)}.`]
|
|
183
|
+
: []),
|
|
184
|
+
].join("\n")),
|
|
185
|
+
]
|
|
186
|
+
: []),
|
|
171
187
|
].join("\n\n");
|
|
172
188
|
}
|
|
173
189
|
case "supervisor-request":
|