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/src/notify.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * ever delivered once.
15
15
  */
16
16
  import { formatMonitorEvent, formatTaskNotification, type TaskExitInfo } from "./format";
17
- import { FAMULUS_WAKE_CUSTOM_TYPE, type WakeItem } from "./wake";
17
+ import { FAMULUS_WAKE_CUSTOM_TYPE, stampWakeAsOf, wakeIds, type WakeItem } from "./wake";
18
18
  import { realClock, type Clock, type ClockTimer } from "./clock";
19
19
 
20
20
 
@@ -88,6 +88,8 @@ export class NotifyCenter {
88
88
  */
89
89
  notify(message: NotifyMessage, opts: { passive?: boolean; quietEvents?: boolean } = {}): void {
90
90
  if (this.disposed) return;
91
+ // Stamped on arrival: a held passive notice keeps the time it was built.
92
+ message = stampWakeAsOf(message, this.clock.now());
91
93
  const wake = message.details as { kind?: string; id?: string } | undefined;
92
94
  if (wake?.kind === "monitor" && typeof wake.id === "string") this.flushMonitor(wake.id, opts.quietEvents);
93
95
  if (!opts.passive) {
@@ -203,14 +205,12 @@ export class NotifyCenter {
203
205
  }
204
206
 
205
207
  private deliver(message: NotifyMessage, passive = false): void {
208
+ // Wakes built here (task batches, monitor events) are stamped now.
209
+ message = stampWakeAsOf(message, this.clock.now());
206
210
  const msg = { ...message, display: true };
207
- const details = message.details as { kind?: string; id?: string; taskId?: string; tasks?: { id: string }[]; children?: { childId: string }[]; childId?: string; from?: string; eventCount?: number } | undefined;
211
+ const details = message.details as { kind?: string; eventCount?: number } | undefined;
208
212
  if (details?.kind) {
209
- const ids = details.kind === "task"
210
- ? (details.tasks ?? []).map((task) => task.id)
211
- : details.kind === "subagent-done"
212
- ? (details.children ?? []).map((child) => child.childId)
213
- : [details.id ?? details.taskId ?? details.childId ?? details.from].filter((id): id is string => Boolean(id));
213
+ const ids = wakeIds(details);
214
214
  this.deps.logEvent?.("wake.emit", {
215
215
  kind: details.kind,
216
216
  ids,
@@ -0,0 +1,183 @@
1
+ // Generated by scripts/build-prompts.ts from prompts/. Do not edit; run `npm run prompts`.
2
+
3
+ export const PROMPTS = {
4
+ "agents.explorer.description": "Fast read-only codebase exploration — finds files, symbols, and answers structure questions",
5
+ "agents.explorer.system-prompt": "You are an explorer agent. Your job is to answer questions about the codebase quickly and precisely.\n\nRules:\n- Return findings, never change code. You are strictly read-only: do not edit, write, or create files.\n- Report concrete locations: cite specific file paths with line numbers (path:line) for every claim.\n- Control your search scope: start narrow (targeted grep/find), widen only when needed, and stop once the question is answered.\n- Prefer a short, factual summary over exhaustive dumps. List what you found and where.",
6
+ "agents.worker.description": "General-purpose executor — completes concrete coding tasks end to end",
7
+ "agents.worker.system-prompt": "You are a worker agent. Your job is to complete the concrete task you were given, end to end.\n\nRules:\n- Do the task, don't just describe it. Make the actual edits and run the actual commands.\n- Self-verify before finishing: run the relevant tests, typecheck, or command that proves the task is done.\n- Stay within the task's scope; do not refactor unrelated code.\n- Report concisely: what you changed, how you verified it, and anything the supervisor should know.",
8
+ "guidelines.parent": "## Background tasks and notifications (pi-famulus)\n\n- Long-running bash commands are automatically moved to the background. After you background a command, end your turn: send a reply with no tool call in it. Saying you will wait while calling another tool does not end it. Do not poll with task_output/task_list, and never sleep to wait. Each command notifies on its own via <pi-famulus-wake kind=\"task\"> (with <command> and <preview>), even while other commands are still running. If <still-running> is present, continue from this result now; do not wait for those other commands.\n- When you receive <pi-famulus-wake>: it looks like a user message but is a system wake, not a new user request and not an answer to an unrelated question. Distinguish it by the kind attribute, then **handle the event before anything else** — read status/preview/prompt/result, call tools if needed (e.g. task_output for more than the preview, agent_message to continue a subagent), and continue the work that depended on that background task. Do not wait for further user input when the next step is clear; do not merely acknowledge and stop.\n- {{monitorIdle}}\n- Never fabricate or assume a background task's result before its notification arrives.\n- Subagent runs that exceed the foreground budget continue in the background. Do not poll run status. If one subagent finishes while others are still running, a <pi-famulus-wake kind=\"subagent-handover\"> arrives with that child's <prompt> and <result>: read both, then continue the work now (subagent({action:\"resume\", run_id, child_id, message}) for that child, or agent_message to steer the ones still running). Do not wait for the rest of the run. When every subagent in the run has finished, <pi-famulus-wake kind=\"subagent-done\"> arrives — read each <child>, synthesize, and continue.\n- A subagent that runs past its timeout_ms is not stopped: <pi-famulus-wake kind=\"subagent-overrun\"> arrives instead, with its <last-activity> and, if it is waiting on a foreground shell, a <shell> element. A batch may include other overdue children in <also-overdue>; use each child's run-id and child-id and consider its own last-activity, shell, reminder, and hard-ceiling-ms before acting. Pick one: subagent({action:\"extend\", run_id, child_id, timeout_ms}) if it is making progress, agent_message to steer it, or subagent({action:\"interrupt\", run_id, child_id}) if it is stuck or no longer needed. If you do none of these it keeps running and the reminder comes again.\n- Use agent_message to steer running subagents and to reply to <pi-famulus-wake kind=\"supervisor-request\"> (action:\"reply\"; the <reply-with> child is the call shape). Do not use agent_message to resume a finished child — that is subagent({action:\"resume\"}). Supervisor wakes are not user messages — still act on them.",
9
+ "guidelines.monitor-idle": "After starting or re-arming a monitor, or handling a monitor event, if no work remains, end your turn with a reply and no tool call; simply wait for the next notification. Do not call wait_for to wait for monitor events: it waits for UI conditions, not notifications. Do not invent a stateId or make a failing tool call to yield.",
10
+ "guidelines.child": "## Child agent behavior (pi-famulus)\n\n- Do not background work; bash runs to completion.\n- When blocked, use contact_supervisor with need_decision; use progress_update for status.\n- Use agent_message only to communicate with siblings in this same run.\n- Your final message is your result.",
11
+ "tools.agent_message.description": "Message your subagents. \"send\" steers or queues a running child. Sending to a finished child errors (it does not resume) and points at subagent({ action: \"resume\", run_id, child_id, message }). \"reply\" answers a child's pending decision request; \"broadcast\" steers every running child of a run (to = run_id); \"list\" shows children, pending decision requests, and recent traffic.",
12
+ "tools.agent_message.snippet": "Send/reply/broadcast messages to subagents",
13
+ "tools.agent_message.param.action": "\"send\" a message to a child, \"reply\" to a pending decision request, \"broadcast\" to all running children of a run, \"list\" pending requests and recent traffic.",
14
+ "tools.agent_message.param.to": "Target child_id or name (\"send\"/\"reply\"). For \"broadcast\" from the parent session: run_id. Child sessions broadcast to their own run and must omit this.",
15
+ "tools.agent_message.param.message": "Message text.",
16
+ "tools.agent_message.param.delivery": "\"steer\" (default) delivers to a running child after its current tool call/turn finishes; \"queue\" delivers after its current turn. Sending to a finished child is an error — resume with subagent({action:\"resume\"}).",
17
+ "tools.agent_message.error.finished-child": "Child {{childId}} ({{name}}) has finished ({{status}}). agent_message does not resume children. Use subagent({ action: \"resume\", run_id: \"{{runId}}\", child_id: \"{{childId}}\", message: \"...\" }).",
18
+ "tools.bash.description": "Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last {{maxLines}} lines or {{maxKb}}KB (whichever is hit first). Foreground commands that exceed the foreground budget are automatically moved to the background; you will be notified when they complete. Optionally provide a timeout in seconds (hard kill limit), or run_in_background to background immediately.",
19
+ "tools.bash.snippet": "Execute bash commands (ls, grep, find, etc.)",
20
+ "tools.bash.rules": ["You can inspect PI_* environment variables for current model and session details.","Long-running bash commands are moved to the background automatically; do not poll or sleep to wait for them. End your turn (a reply with no tool call) and resume from the task wake when it arrives."],
21
+ "tools.bash.param.command": "The bash command to execute",
22
+ "tools.bash.param.timeout": "Hard kill timeout in seconds (optional, no default timeout)",
23
+ "tools.bash.param.run_in_background": "Start the command in the background and return immediately. You will be notified when it completes.",
24
+ "tools.bash.result.backgrounded": "Command \"{{command}}\" moved to background (task_id: {{taskId}}). Output: {{outputPath}}.\nYou will be notified when it completes, even if other commands are still running. Do not poll or sleep: reply to the user now with no tool call, and continue from the <pi-famulus-wake kind=\"task\"> when it arrives.",
25
+ "tools.bash.error.bare-sleep": "Refusing to run a bare sleep/idle-loop command: {{command}}. Do not sleep to wait for background work; completion is delivered via notification. Use the monitor tool to watch for a condition, or run_in_background for long commands.",
26
+ "tools.child.agent_message.description": "Message sibling subagents in your run, or list pending requests. \"send\" delivers to a running sibling. Sending to a finished sibling errors and does not resume it; resume with subagent({ action: \"resume\", run_id, child_id, message }). \"broadcast\" steers every running sibling in your run; \"list\" shows pending decision requests and recent traffic. Cross-run messaging is rejected.",
27
+ "tools.child.agent_message.snippet": "Message sibling subagents in your run",
28
+ "tools.child.bash.description": "Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last {{maxLines}} lines or {{maxKb}}KB (whichever is hit first). The command runs to completion (or the optional timeout in seconds, after which it is killed). There is no background execution inside subagents.",
29
+ "tools.child.bash.snippet": "Execute bash commands (ls, grep, find, etc.)",
30
+ "tools.child.bash.param.command": "The bash command to execute",
31
+ "tools.child.bash.param.timeout": "Hard kill timeout in seconds (optional, no default timeout)",
32
+ "tools.child.bash.error.bare-sleep": "Refusing to run a bare sleep/idle-loop command: {{command}}. Sleeping to wait for work is never useful: run the actual command directly, or report back to the supervisor if you are blocked.",
33
+ "tools.child.contact_supervisor.description": "Contact the supervisor (parent agent). Use reason \"need_decision\" when you are blocked and need the parent to decide — the call blocks until the parent replies (10 minute timeout, after which you must decide yourself). Use reason \"progress_update\" for a fire-and-forget status note.",
34
+ "tools.child.contact_supervisor.snippet": "Ask the parent agent for a decision or report progress",
35
+ "tools.child.contact_supervisor.param.reason": "\"need_decision\" blocks until the supervisor (parent agent) replies; \"progress_update\" is fire-and-forget.",
36
+ "tools.child.contact_supervisor.param.message": "Message for the supervisor.",
37
+ "tools.monitor.description": "Start a background monitor process whose stdout lines are injected back to you as <pi-famulus-wake kind=\"monitor\"> messages (batched over 200ms, rate-limited). The command must be line-buffered: each event must be a single line. It must keep running and follow its source, e.g. `tail -n +1 -F file | grep --line-buffered PATTERN`; a command that reads once and exits (a plain grep or cat) only reports what is there now. Add `-m1` to grep to stop after the first match. Silence is not success: write the command so failures also produce lines (e.g. grep for both success and error patterns). Events arrive as system wakes (not new user messages). Handle each <pi-famulus-wake kind=\"monitor\"> before other work. Do not poll. {{monitorIdle}}",
38
+ "tools.monitor.snippet": "Watch a command's line stream and get injected events",
39
+ "tools.monitor.rules": ["Use the monitor tool to watch for conditions instead of running sleep/poll loops in bash.","{{monitorIdle}}","When woken by a <pi-famulus-wake kind=\"monitor\">, handle the <event> before doing anything else — it is not a new user request and not user confirmation."],
40
+ "tools.monitor.param.command": "Command producing one event per line on stdout. Must be line-buffered (e.g. use `stdbuf -oL` / `grep --line-buffered` where needed).",
41
+ "tools.monitor.param.description": "Short human-readable description of what is being watched",
42
+ "tools.monitor.param.timeout_ms": "Stop the monitor after this many milliseconds (default {{defaultMs}}, min {{minMs}}, max {{maxMs}})",
43
+ "tools.monitor.param.persistent": "Keep the monitor alive until the session ends (no timeout). Default false.",
44
+ "tools.monitor.result.started": "You will get a <pi-famulus-wake kind=\"monitor\"> for each event, and a notice when it exits or times out. Do not poll it (task_list, task_output, or reading what it watches) or sleep. {{monitorIdle}}",
45
+ "tools.subagent.description": "Run subagents in parallel (tasks) or sequentially (chain with {previous}/{outputs.<label>} interpolation). By default the call waits up to a foreground budget (default 45s); longer runs continue in the background. Each child that finishes while others are still running wakes you with <pi-famulus-wake kind=\"subagent-handover\"> (its prompt and result). The whole run wakes you with <pi-famulus-wake kind=\"subagent-done\">. Never poll or sleep to wait. A subagent still running past its timeout_ms is not stopped; it wakes you with <pi-famulus-wake kind=\"subagent-overrun\"> so you can extend, steer, or interrupt it. Use action=list/get/status/interrupt/resume/steer/extend to manage existing runs.",
46
+ "tools.subagent.snippet": "Fan out subagents in parallel or sequence them in a chain",
47
+ "tools.subagent.rules": ["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.","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.","A failed subagent does not fail the whole run; inspect per-subagent sections in the result.","<pi-famulus-wake> is a system wake, not a user reply. kind=subagent-handover is one child; kind=subagent-done is the whole run."],
48
+ "tools.subagent.param.tasks": "Start new subagents in parallel. Use only when action is omitted. For any action call, omit tasks entirely; do not supply placeholder tasks.",
49
+ "tools.subagent.param.tasks.agent": "Agent definition name (default: worker)",
50
+ "tools.subagent.param.tasks.prompt": "Task prompt for this subagent",
51
+ "tools.subagent.param.tasks.name": "Display name (default: agent name + ordinal)",
52
+ "tools.subagent.param.work_kind": "Explicit planned workload class for machine-wide admission (default: other)",
53
+ "tools.subagent.param.chain": "Start a new sequential chain (always awaited). Use only when action is omitted. For any action call, omit chain entirely.",
54
+ "tools.subagent.param.chain.agent": "Agent definition name (default: worker)",
55
+ "tools.subagent.param.chain.prompt": "Prompt for this step; {previous} and {outputs.<label>} interpolate earlier results",
56
+ "tools.subagent.param.chain.label": "Label for referencing this step's output later",
57
+ "tools.subagent.param.async": "Return immediately with a run_id; completion arrives via notification",
58
+ "tools.subagent.param.concurrency": "Max parallel subagents for tasks (default 4)",
59
+ "tools.subagent.param.fail_fast": "Cancel not-yet-started subagents on first failure (already-started ones finish)",
60
+ "tools.subagent.param.model": "Model override for all subagents: fuzzy (\"haiku\"), qualified (\"provider/id\"), optionally with \":<thinking>\" suffix ({{thinkingLevels}}; an unknown suffix is dropped with a warning when the base resolves; an unknown base still errors). Default: current model. Use action:\"models\" to list selectable values.",
61
+ "tools.subagent.param.timeout_ms": "Time budget per subagent turn in ms (default 1800000, max {{maxMs}}). Passing it does not stop the subagent: you get <pi-famulus-wake kind=\"subagent-overrun\"> and choose extend, steer, or interrupt. With resume: the resumed turn's budget. With extend: the new budget from now.",
62
+ "tools.subagent.param.action": "Manage an existing run (or list selectable models) instead of starting a new one. When action is set, omit tasks and chain entirely; these launch-only fields are not required placeholders for control calls.",
63
+ "tools.subagent.param.run_id": "Target run for action",
64
+ "tools.subagent.param.child_id": "Target child (id or name) for steer/interrupt/resume/extend",
65
+ "tools.subagent.param.message": "Message content for steer/resume",
66
+ "tools.subagent.result.backgrounded": "Started {{count}} subagent(s) in run {{runId}}. {{reason}}\nWhile others are still running, each finished subagent arrives as <pi-famulus-wake kind=\"subagent-handover\"> with that child's prompt and result. Read it and continue: subagent({action:\"resume\", run_id, child_id, message}) for that child, or agent_message to steer the ones still running. Do not wait for the whole run. Do not poll. <pi-famulus-wake kind=\"subagent-done\"> arrives when every subagent in the run has finished. Use subagent({action:\"get\", run_id:\"{{runId}}\"}) if you need the full record.",
67
+ "tools.subagent.result.extended": "Extended subagent {{name}} ({{childId}}) in run {{runId}}: the next <pi-famulus-wake kind=\"subagent-overrun\"> comes in {{next}} if it is still running. Its result arrives as a wake when it finishes; do not poll.",
68
+ "tools.subagent.result.extended.hard-ceiling": "The configured hard ceiling (hardTimeoutMs) is not moved: it stops this subagent in {{remaining}}.",
69
+ "tools.subagent.result.resumed": "Resumed subagent {{name}} ({{childId}}) in run {{runId}}. {{queued}}You will be notified via <pi-famulus-wake kind=\"subagent-handover\"> if others are still running, otherwise via <pi-famulus-wake kind=\"subagent-done\"> when it completes. Do not poll.",
70
+ "tools.subagent.error.resume-queued": "subagent {{name}} ({{childId}}) is already queued and starts when a subagent slot frees; its result arrives as a wake when it finishes. Do not resume it again.",
71
+ "tools.task_list.description": "List background work this session started: shell/monitor tasks and subagent children. Work from other pi sessions is never shown.",
72
+ "tools.task_list.snippet": "List background shell/monitor tasks and subagents",
73
+ "tools.task_list.param.all": "Also include finished work (default: running only). Always limited to this session.",
74
+ "tools.task_list.hint.running": "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.",
75
+ "tools.task_output.description": "Read output of a background task. Without a cursor, returns the tail of the output plus the current file pointer; pass the returned next_cursor as cursor for incremental reads. Not a way to wait: a running task wakes you when it finishes, so reply with no tool call instead of calling this again.",
76
+ "tools.task_output.snippet": "Read background task output",
77
+ "tools.task_output.param.task_id": "Task id, e.g. sh_a1b2c3d4",
78
+ "tools.task_output.param.cursor": "Byte offset to read from (for incremental reads). Omit to read the tail of the output.",
79
+ "tools.task_output.param.max_bytes": "Maximum bytes to return (default 65536)",
80
+ "tools.task_output.hint.still-running": "The task is still running. Reply to the user now with no tool call; a <pi-famulus-wake> arrives when it finishes.",
81
+ "tools.task_output.error.no-new-output": "No new output from {{taskId}} since your last read. {{hint}}",
82
+ "tools.task_stop.description": "Stop a running background task (SIGTERM to the process group, SIGKILL after a 2s grace).",
83
+ "tools.task_stop.snippet": "Stop a background task",
84
+ "tools.task_stop.param.task_id": "Task id to stop, e.g. sh_a1b2c3d4",
85
+ "wakes.lead-in": "System wake — not a new user message. Handle this <pi-famulus-wake> before other work.",
86
+ "wakes.handover.changed-active": "Its result arrives as a new wake when it finishes.",
87
+ "wakes.done.changed-active": "The run is active again; another subagent-done arrives when it finishes.",
88
+ "wakes.overrun.summary": "{{name}} has run {{elapsed}} in this turn, past its {{budget}} budget, and is still running{{shell}}.",
89
+ "wakes.overrun.summary-shell": "; it is waiting on a shell command that has run {{elapsed}}",
90
+ "wakes.overrun.actions": "give it more time with subagent({ action: \"extend\", run_id: \"{{runId}}\", child_id: \"{{childId}}\", timeout_ms: <ms from now> }); redirect it with agent_message({ action: \"send\", to: \"{{childId}}\", message: \"<instruction>\" }); or stop it with subagent({ action: \"interrupt\", run_id: \"{{runId}}\", child_id: \"{{childId}}\" }).",
91
+ "wakes.overrun.options": "It has not been stopped. Extend only when progress or a known, still-useful silent workload justifies more time within the user's timing constraints; a child shell timeout is not evidence of progress and is not, by itself, a reason to extend past the requested timeframe. Choose one: {{actions}} If you do none of these, it keeps running and the next reminder is scheduled in {{next}}; its result arrives as usual when it finishes.",
92
+ "wakes.overrun.options-ceiling": "It has not been stopped yet, but the configured hard ceiling stops it in {{ceiling}}, and extend does not move that ceiling. Choose one: {{actions}} {{noAction}} Its result, or the interruption, arrives as a wake.",
93
+ "wakes.overrun.ceiling-no-action-reminder": "If you do none of these, it keeps running and the next reminder is scheduled in {{next}}, until the hard ceiling stops it in {{ceiling}}.",
94
+ "wakes.overrun.ceiling-no-action": "If you do none of these, it keeps running until the hard ceiling stops it.",
95
+ "wakes.request.reply-with": "agent_message { action: \"reply\", to: \"{{from}}\", message: \"<your decision>\" }",
96
+ } as const;
97
+
98
+ export type PromptId = keyof typeof PROMPTS;
99
+
100
+ /** Placeholder names of each template prompt. */
101
+ export interface PromptVars {
102
+ "guidelines.parent": "monitorIdle";
103
+ "tools.agent_message.error.finished-child": "childId" | "name" | "status" | "runId";
104
+ "tools.bash.description": "maxLines" | "maxKb";
105
+ "tools.bash.result.backgrounded": "command" | "taskId" | "outputPath";
106
+ "tools.bash.error.bare-sleep": "command";
107
+ "tools.child.bash.description": "maxLines" | "maxKb";
108
+ "tools.child.bash.error.bare-sleep": "command";
109
+ "tools.monitor.description": "monitorIdle";
110
+ "tools.monitor.rules": "monitorIdle";
111
+ "tools.monitor.param.timeout_ms": "defaultMs" | "minMs" | "maxMs";
112
+ "tools.monitor.result.started": "monitorIdle";
113
+ "tools.subagent.param.model": "thinkingLevels";
114
+ "tools.subagent.param.timeout_ms": "maxMs";
115
+ "tools.subagent.result.backgrounded": "count" | "runId" | "reason";
116
+ "tools.subagent.result.extended": "name" | "childId" | "runId" | "next";
117
+ "tools.subagent.result.extended.hard-ceiling": "remaining";
118
+ "tools.subagent.result.resumed": "name" | "childId" | "runId" | "queued";
119
+ "tools.subagent.error.resume-queued": "name" | "childId";
120
+ "tools.task_output.error.no-new-output": "taskId" | "hint";
121
+ "wakes.overrun.summary": "name" | "elapsed" | "budget" | "shell";
122
+ "wakes.overrun.summary-shell": "elapsed";
123
+ "wakes.overrun.actions": "runId" | "childId";
124
+ "wakes.overrun.options": "actions" | "next";
125
+ "wakes.overrun.options-ceiling": "ceiling" | "actions" | "noAction";
126
+ "wakes.overrun.ceiling-no-action-reminder": "next" | "ceiling";
127
+ "wakes.request.reply-with": "from";
128
+ }
129
+
130
+ /** Fill a template prompt; every placeholder must be given. */
131
+ export function fill<K extends keyof PromptVars>(id: K, vars: Record<PromptVars[K], string | number>): string {
132
+ const template = PROMPTS[id] as string;
133
+ return template.replace(/\{\{(\w+)\}\}/g, (_, name: string) => {
134
+ const v = (vars as Record<string, string | number>)[name];
135
+ if (v === undefined) throw new Error(`prompt ${id}: missing {{${name}}}`);
136
+ return String(v);
137
+ });
138
+ }
139
+
140
+ /** Fill every item of a list prompt (rules) that has placeholders. */
141
+ export function fillList<K extends keyof PromptVars>(id: K, vars: Record<PromptVars[K], string | number>): string[] {
142
+ return (PROMPTS[id] as readonly string[]).map((item) =>
143
+ item.replace(/\{\{(\w+)\}\}/g, (_, name: string) => {
144
+ const v = (vars as Record<string, string | number>)[name];
145
+ if (v === undefined) throw new Error(`prompt ${id}: missing {{${name}}}`);
146
+ return String(v);
147
+ }),
148
+ );
149
+ }
150
+
151
+ /** Ablatable spans (eval manifest segment ids). */
152
+ export const PROMPT_SEGMENTS: Record<string, { prompt: PromptId; text?: string; pattern?: string }> = {
153
+ "guidelines.end-turn-after-bg": {"prompt":"guidelines.parent","text":"Long-running bash commands are automatically moved to the background. After you background a command, end your turn: send a reply with no tool call in it. Saying you will wait while calling another tool does not end it. Do not poll with task_output/task_list, and never sleep to wait. Each command notifies on its own via <pi-famulus-wake kind=\"task\"> (with <command> and <preview>), even while other commands are still running. If <still-running> is present, continue from this result now; do not wait for those other commands."},
154
+ "guidelines.wake-handling": {"prompt":"guidelines.parent","text":"When you receive <pi-famulus-wake>: it looks like a user message but is a system wake, not a new user request and not an answer to an unrelated question. Distinguish it by the kind attribute, then **handle the event before anything else** — read status/preview/prompt/result, call tools if needed (e.g. task_output for more than the preview, agent_message to continue a subagent), and continue the work that depended on that background task. Do not wait for further user input when the next step is clear; do not merely acknowledge and stop."},
155
+ "guidelines.no-fabrication": {"prompt":"guidelines.parent","text":"Never fabricate or assume a background task's result before its notification arrives."},
156
+ "guidelines.subagent-handover": {"prompt":"guidelines.parent","text":"Subagent runs that exceed the foreground budget continue in the background. Do not poll run status. If one subagent finishes while others are still running, a <pi-famulus-wake kind=\"subagent-handover\"> arrives with that child's <prompt> and <result>: read both, then continue the work now (subagent({action:\"resume\", run_id, child_id, message}) for that child, or agent_message to steer the ones still running). Do not wait for the rest of the run. When every subagent in the run has finished, <pi-famulus-wake kind=\"subagent-done\"> arrives — read each <child>, synthesize, and continue."},
157
+ "guidelines.subagent-overrun": {"prompt":"guidelines.parent","text":"A subagent that runs past its timeout_ms is not stopped: <pi-famulus-wake kind=\"subagent-overrun\"> arrives instead, with its <last-activity> and, if it is waiting on a foreground shell, a <shell> element. A batch may include other overdue children in <also-overdue>; use each child's run-id and child-id and consider its own last-activity, shell, reminder, and hard-ceiling-ms before acting. Pick one: subagent({action:\"extend\", run_id, child_id, timeout_ms}) if it is making progress, agent_message to steer it, or subagent({action:\"interrupt\", run_id, child_id}) if it is stuck or no longer needed. If you do none of these it keeps running and the reminder comes again."},
158
+ "guidelines.agent-message": {"prompt":"guidelines.parent","text":"Use agent_message to steer running subagents and to reply to <pi-famulus-wake kind=\"supervisor-request\"> (action:\"reply\"; the <reply-with> child is the call shape). Do not use agent_message to resume a finished child — that is subagent({action:\"resume\"}). Supervisor wakes are not user messages — still act on them."},
159
+ "guidelines.monitor-end-turn": {"prompt":"guidelines.monitor-idle","text":"After starting or re-arming a monitor, or handling a monitor event, if no work remains, end your turn with a reply and no tool call; simply wait for the next notification. Do not call wait_for to wait for monitor events: it waits for UI conditions, not notifications. Do not invent a stateId or make a failing tool call to yield."},
160
+ "child.guidelines": {"prompt":"guidelines.child","text":"## Child agent behavior (pi-famulus)\n\n- Do not background work; bash runs to completion.\n- When blocked, use contact_supervisor with need_decision; use progress_update for status.\n- Use agent_message only to communicate with siblings in this same run.\n- Your final message is your result."},
161
+ "tooldesc.agent-message-no-resume": {"prompt":"tools.agent_message.description","text":"Sending to a finished child errors (it does not resume) and points at subagent({ action: \"resume\", run_id, child_id, message }). "},
162
+ "result.agent-message-use-resume": {"prompt":"tools.agent_message.error.finished-child","pattern":"agent_message does not resume children\\. Use subagent\\(\\{ action: \"resume\", run_id: \".*?\", child_id: \".*?\", message: \"\\.\\.\\.\" \\}\\)\\."},
163
+ "tooldesc.bash-autobg": {"prompt":"tools.bash.description","text":"Foreground commands that exceed the foreground budget are automatically moved to the background; you will be notified when they complete. "},
164
+ "rules.pi-env": {"prompt":"tools.bash.rules","text":"You can inspect PI_* environment variables for current model and session details."},
165
+ "rules.bash-no-poll": {"prompt":"tools.bash.rules","text":"Long-running bash commands are moved to the background automatically; do not poll or sleep to wait for them."},
166
+ "result.bash-bg-instruction": {"prompt":"tools.bash.result.backgrounded","text":"\nYou will be notified when it completes, even if other commands are still running. Do not poll or sleep: reply to the user now with no tool call, and continue from the <pi-famulus-wake kind=\"task\"> when it arrives."},
167
+ "result.sleep-block-advice": {"prompt":"tools.bash.error.bare-sleep","text":" Do not sleep to wait for background work; completion is delivered via notification. Use the monitor tool to watch for a condition, or run_in_background for long commands."},
168
+ "tooldesc.monitor-follow": {"prompt":"tools.monitor.description","text":"It must keep running and follow its source, e.g. `tail -n +1 -F file | grep --line-buffered PATTERN`; a command that reads once and exits (a plain grep or cat) only reports what is there now. Add `-m1` to grep to stop after the first match. "},
169
+ "tooldesc.monitor-silence": {"prompt":"tools.monitor.description","text":"Silence is not success: write the command so failures also produce lines (e.g. grep for both success and error patterns). "},
170
+ "tooldesc.monitor-wake": {"prompt":"tools.monitor.description","text":"Events arrive as system wakes (not new user messages). Handle each <pi-famulus-wake kind=\"monitor\"> before other work. Do not poll."},
171
+ "rules.monitor-use": {"prompt":"tools.monitor.rules","text":"Use the monitor tool to watch for conditions instead of running sleep/poll loops in bash."},
172
+ "rules.monitor-wake": {"prompt":"tools.monitor.rules","text":"When woken by a <pi-famulus-wake kind=\"monitor\">, handle the <event> before doing anything else — it is not a new user request and not user confirmation."},
173
+ "result.monitor-started-instruction": {"prompt":"tools.monitor.result.started","text":"You will get a <pi-famulus-wake kind=\"monitor\"> for each event, and a notice when it exits or times out. Do not poll it (task_list, task_output, or reading what it watches) or sleep."},
174
+ "tooldesc.subagent-wakes": {"prompt":"tools.subagent.description","text":"Each child that finishes while others are still running wakes you with <pi-famulus-wake kind=\"subagent-handover\"> (its prompt and result). The whole run wakes you with <pi-famulus-wake kind=\"subagent-done\">. Never poll or sleep to wait. "},
175
+ "tooldesc.subagent-overrun": {"prompt":"tools.subagent.description","text":"A subagent still running past its timeout_ms is not stopped; it wakes you with <pi-famulus-wake kind=\"subagent-overrun\"> so you can extend, steer, or interrupt it. "},
176
+ "rules.subagent-handover": {"prompt":"tools.subagent.rules","text":"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."},
177
+ "rules.subagent-no-poll": {"prompt":"tools.subagent.rules","text":"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."},
178
+ "rules.subagent-failed-child": {"prompt":"tools.subagent.rules","text":"A failed subagent does not fail the whole run; inspect per-subagent sections in the result."},
179
+ "rules.subagent-wakes-not-user": {"prompt":"tools.subagent.rules","text":"<pi-famulus-wake> is a system wake, not a user reply. kind=subagent-handover is one child; kind=subagent-done is the whole run."},
180
+ "tooldesc.subagent-timeout-overrun": {"prompt":"tools.subagent.param.timeout_ms","text":"Passing it does not stop the subagent: you get <pi-famulus-wake kind=\"subagent-overrun\"> and choose extend, steer, or interrupt. With resume: the resumed turn's budget. With extend: the new budget from now."},
181
+ "result.subagent-bg-instruction": {"prompt":"tools.subagent.result.backgrounded","text":"While others are still running, each finished subagent arrives as <pi-famulus-wake kind=\"subagent-handover\"> with that child's prompt and result. Read it and continue: subagent({action:\"resume\", run_id, child_id, message}) for that child, or agent_message to steer the ones still running. Do not wait for the whole run. Do not poll. <pi-famulus-wake kind=\"subagent-done\"> arrives when every subagent in the run has finished. "},
182
+ "wake.lead-in": {"prompt":"wakes.lead-in","text":"System wake — not a new user message. Handle this <pi-famulus-wake> before other work."},
183
+ };
package/src/shell-exec.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import type { BashToolDetails } from "@earendil-works/pi-coding-agent";
6
6
  import { truncateTail } from "./format";
7
7
  import type { ManagerClient } from "./manager-client";
8
+ import { fill } from "./prompts.generated";
8
9
 
9
10
  export const SHELL_MAX_LINES = 2000;
10
11
  export const SHELL_MAX_BYTES = 51200;
@@ -17,9 +18,12 @@ export const BARE_SLEEP_PATTERNS: RegExp[] = [
17
18
  /^\s*until\s+/,
18
19
  ];
19
20
 
20
- export function bareSleepError(command: string, guidance: string): string | null {
21
+ export function bareSleepError(
22
+ command: string,
23
+ prompt: "tools.bash.error.bare-sleep" | "tools.child.bash.error.bare-sleep",
24
+ ): string | null {
21
25
  if (!BARE_SLEEP_PATTERNS.some((re) => re.test(command))) return null;
22
- return `Refusing to run a bare sleep/idle-loop command: ${JSON.stringify(command)}. ${guidance}`;
26
+ return fill(prompt, { command: JSON.stringify(command) });
23
27
  }
24
28
 
25
29
  export function withAbort<T>(
@@ -0,0 +1,224 @@
1
+ export interface LocalReservation {
2
+ admit(): void;
3
+ release(): void;
4
+ }
5
+
6
+ export interface AgentAdmissionManager {
7
+ isAvailable(): boolean;
8
+ protocolLevel(): number;
9
+ acquireAgent(
10
+ childId: string,
11
+ workKind?: string,
12
+ signal?: AbortSignal,
13
+ ): Promise<{ granted: boolean; rejection?: string }>;
14
+ releaseAgent(childId: string): Promise<void>;
15
+ }
16
+
17
+ export interface AdmissionTicket {
18
+ current(): boolean;
19
+ signal: AbortSignal;
20
+ }
21
+
22
+ export interface AgentAdmissionOptions {
23
+ childId: string;
24
+ workKind?: string;
25
+ reserveLocal(): Promise<LocalReservation>;
26
+ manager: AgentAdmissionManager | null;
27
+ /** Locally held or potentially-held daemon permits, keyed by their kind. */
28
+ leases: Map<string, string>;
29
+ /** Locally admitted children still waiting for a daemon permit. */
30
+ pendingLeases?: Set<string>;
31
+ /** Reconnect-time acquires to abort if the child settles while queued. */
32
+ pendingReregistrations?: Map<string, AbortController>;
33
+ ticket?: AdmissionTicket;
34
+ notice(reason: "manager unavailable" | "daemon too old"): void;
35
+ wait?: (signal?: AbortSignal) => Promise<void>;
36
+ }
37
+
38
+ const MAX_REREGISTER_ATTEMPTS = 40;
39
+
40
+ const defaultWait = (signal?: AbortSignal) => new Promise<void>((resolve, reject) => {
41
+ const timer = setTimeout(() => {
42
+ signal?.removeEventListener("abort", onAbort);
43
+ resolve();
44
+ }, 500);
45
+ const onAbort = () => {
46
+ clearTimeout(timer);
47
+ signal?.removeEventListener("abort", onAbort);
48
+ reject(new Error("subagent admission cancelled"));
49
+ };
50
+ if (signal?.aborted) onAbort();
51
+ else signal?.addEventListener("abort", onAbort, { once: true });
52
+ });
53
+
54
+ function waitForRetry(wait: (signal?: AbortSignal) => Promise<void>, signal?: AbortSignal): Promise<void> {
55
+ if (!signal) return wait();
56
+ if (signal.aborted) return Promise.reject(new Error("subagent admission cancelled"));
57
+ return new Promise<void>((resolve, reject) => {
58
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
59
+ const onAbort = () => {
60
+ cleanup();
61
+ reject(new Error("subagent admission cancelled"));
62
+ };
63
+ signal.addEventListener("abort", onAbort, { once: true });
64
+ wait(signal).then(
65
+ () => {
66
+ cleanup();
67
+ resolve();
68
+ },
69
+ (error) => {
70
+ cleanup();
71
+ reject(error);
72
+ },
73
+ );
74
+ });
75
+ }
76
+
77
+ /** Reserve the per-session slot first, then obtain a machine-wide work-kind permit. */
78
+ export async function admitAgentChild(options: AgentAdmissionOptions): Promise<(terminal?: boolean) => void> {
79
+ const { childId, reserveLocal, manager, leases, pendingLeases, ticket, notice } = options;
80
+ const workKind = options.workKind ?? "other";
81
+ const wait = options.wait ?? defaultWait;
82
+ const cancelReregistration = () => options.pendingReregistrations?.get(childId)?.abort();
83
+ const finish = (reservation: LocalReservation) => (terminal = true) => {
84
+ reservation.release();
85
+ if (terminal) {
86
+ cancelReregistration();
87
+ pendingLeases?.delete(childId);
88
+ if (leases.delete(childId)) void manager?.releaseAgent(childId).catch(() => {});
89
+ }
90
+ };
91
+ const admitLocally = (reservation: LocalReservation) => {
92
+ try {
93
+ reservation.admit();
94
+ } catch (error) {
95
+ reservation.release();
96
+ throw error;
97
+ }
98
+ // Keep locally admitted fallback children in the reconciliation set too.
99
+ // A daemon reconnect can then acquire their permit before a later resume.
100
+ leases.set(childId, workKind);
101
+ pendingLeases?.add(childId);
102
+ return finish(reservation);
103
+ };
104
+
105
+ for (;;) {
106
+ const local = await reserveLocal();
107
+ if (!manager?.isAvailable()) {
108
+ notice("manager unavailable");
109
+ return admitLocally(local);
110
+ }
111
+ if (manager.protocolLevel() < 4) {
112
+ notice("daemon too old");
113
+ return admitLocally(local);
114
+ }
115
+ // A local fallback lease is not yet a daemon permit. Reacquire it, while a
116
+ // previously granted machine permit survives normal generation resumes.
117
+ if (leases.has(childId) && !pendingLeases?.has(childId)) {
118
+ try {
119
+ local.admit();
120
+ } catch (error) {
121
+ local.release();
122
+ throw error;
123
+ }
124
+ return finish(local);
125
+ }
126
+
127
+ let admission: { granted: boolean; rejection?: string };
128
+ try {
129
+ admission = await manager.acquireAgent(childId, workKind, ticket?.signal);
130
+ } catch (error) {
131
+ local.release();
132
+ if (ticket?.signal.aborted || (ticket && !ticket.current())) {
133
+ throw new Error("subagent admission cancelled");
134
+ }
135
+ if (manager.isAvailable()) throw error;
136
+ notice("manager unavailable");
137
+ // The request may have been granted before its response was lost.
138
+ // Remember it as pending so reconnect's idempotent acquire can reconcile
139
+ // that uncertain server-side result without consuming another slot.
140
+ let fallback: LocalReservation;
141
+ try {
142
+ fallback = await reserveLocal();
143
+ } catch (fallbackError) {
144
+ void manager.releaseAgent(childId).catch(() => {});
145
+ throw fallbackError;
146
+ }
147
+ try {
148
+ return admitLocally(fallback);
149
+ } catch (fallbackError) {
150
+ void manager.releaseAgent(childId).catch(() => {});
151
+ throw fallbackError;
152
+ }
153
+ }
154
+ if (admission.granted) {
155
+ try {
156
+ if (ticket && !ticket.current()) throw new Error("subagent admission cancelled");
157
+ local.admit();
158
+ } catch (error) {
159
+ local.release();
160
+ void manager.releaseAgent(childId).catch(() => {});
161
+ throw error;
162
+ }
163
+ leases.set(childId, workKind);
164
+ pendingLeases?.delete(childId);
165
+ return finish(local);
166
+ }
167
+
168
+ local.release();
169
+ if (ticket && !ticket.current()) throw new Error("subagent admission cancelled");
170
+ await waitForRetry(wait, ticket?.signal);
171
+ }
172
+ }
173
+
174
+ /** Re-register extension-held leases after hello/reconnect, preserving kind. */
175
+ export async function reregisterAgentLeases(
176
+ manager: AgentAdmissionManager,
177
+ leases: Map<string, string>,
178
+ wait: (signal?: AbortSignal) => Promise<void> = defaultWait,
179
+ pendingLeases?: Set<string>,
180
+ pendingReregistrations?: Map<string, AbortController>,
181
+ ): Promise<void> {
182
+ if (manager.protocolLevel() < 4) return;
183
+ const childIds = new Set([...leases.keys(), ...(pendingLeases ?? [])]);
184
+ await Promise.all([...childIds].map(async (childId) => {
185
+ const stillHeld = () => leases.has(childId) || pendingLeases?.has(childId) === true;
186
+ for (let attempt = 0; attempt < MAX_REREGISTER_ATTEMPTS && stillHeld() && manager.isAvailable() && manager.protocolLevel() >= 4; attempt++) {
187
+ if (pendingReregistrations?.has(childId)) break;
188
+ const workKind = leases.get(childId) ?? "other";
189
+ const controller = new AbortController();
190
+ pendingReregistrations?.set(childId, controller);
191
+ let admission: { granted: boolean; rejection?: string };
192
+ try {
193
+ admission = await (pendingReregistrations
194
+ ? manager.acquireAgent(childId, workKind, controller.signal)
195
+ : manager.acquireAgent(childId, workKind));
196
+ } catch (error) {
197
+ if (!stillHeld() || controller.signal.aborted) break;
198
+ if (
199
+ error instanceof Error
200
+ && error.message.startsWith("pi-famulus request timed out: acquire_agent")
201
+ && manager.isAvailable()
202
+ ) {
203
+ if (attempt + 1 < MAX_REREGISTER_ATTEMPTS) await waitForRetry(wait, controller.signal);
204
+ continue;
205
+ }
206
+ throw error;
207
+ } finally {
208
+ if (pendingReregistrations?.get(childId) === controller) pendingReregistrations.delete(childId);
209
+ }
210
+ if (!stillHeld()) {
211
+ // The child may have settled after the grant was made but before the
212
+ // acquire response reached this continuation.
213
+ if (admission.granted) void manager.releaseAgent(childId).catch(() => {});
214
+ break;
215
+ }
216
+ if (admission.granted) {
217
+ leases.set(childId, workKind);
218
+ pendingLeases?.delete(childId);
219
+ break;
220
+ }
221
+ if (attempt + 1 < MAX_REREGISTER_ATTEMPTS) await waitForRetry(wait, controller.signal);
222
+ }
223
+ }));
224
+ }
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Layout: <home>/sessions/<session_id>/agents/<child_id>.json
6
6
  */
7
- import { mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { randomBytes } from "node:crypto";
8
+ import { mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
8
9
  import { join } from "node:path";
9
10
 
10
11
  export interface AgentChildRecord {
@@ -15,6 +16,10 @@ export interface AgentChildRecord {
15
16
  session_id: string;
16
17
  name: string;
17
18
  agent: string;
19
+ /** Explicit planned work class; old records default to `other`. */
20
+ work_kind?: string;
21
+ /** Time spent waiting for admission, when measured. */
22
+ queue_ms?: number;
18
23
  /** Resolved model id when known (param override or agent definition). */
19
24
  model?: string;
20
25
  status: "pending" | "running" | "completed" | "failed" | "interrupted";
@@ -33,6 +38,18 @@ export interface AgentChildRecord {
33
38
  result_tail?: string;
34
39
  /** Tool results seen in the transcript so far. */
35
40
  tool_calls?: number;
41
+ /** Cumulative provider usage.input and usage.output as reported. */
42
+ tokens_input?: number;
43
+ tokens_output?: number;
44
+ /** Separate cumulative provider cache counters; not folded into tokens_input. */
45
+ tokens_cache_read?: number;
46
+ tokens_cache_write?: number;
47
+ /** Observed assistant-message, tool-execution, and admission-queue wall time. */
48
+ llm_ms?: number;
49
+ tool_ms?: number;
50
+ /** Wall time that did not fit the observed phase boundaries. */
51
+ wall_other_ms?: number;
52
+ wall_approximate?: boolean;
36
53
  /** Absolute path of the live `<child_id>.jsonl` transcript. */
37
54
  transcript?: string;
38
55
  }
@@ -82,11 +99,57 @@ export function agentRecordPath(home: string, sessionId: string, childId: string
82
99
  return join(agentRecordsDir(home, sessionId), `${childId}.json`);
83
100
  }
84
101
 
85
- /** Write one child record (mkdir + overwrite). */
102
+ /** Write one child record atomically (mkdir + same-directory temp file + rename). */
86
103
  export function writeAgentChildRecord(home: string, record: AgentChildRecord): void {
87
104
  const dir = agentRecordsDir(home, record.session_id);
88
105
  mkdirSync(dir, { recursive: true });
89
- writeFileSync(agentRecordPath(home, record.session_id, record.child_id), JSON.stringify(record));
106
+ const path = agentRecordPath(home, record.session_id, record.child_id);
107
+ const tempPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
108
+ try {
109
+ writeFileSync(tempPath, JSON.stringify(record), { flag: "wx" });
110
+ renameSync(tempPath, path);
111
+ } finally {
112
+ try {
113
+ unlinkSync(tempPath);
114
+ } catch {
115
+ // rename consumed the temporary file, or a failed write left none.
116
+ }
117
+ }
118
+ }
119
+
120
+ export type AgentChildTelemetry = Partial<Pick<AgentChildRecord,
121
+ "tokens_input" | "tokens_output" | "tokens_cache_read" | "tokens_cache_write" | "llm_ms" | "tool_ms" | "queue_ms" | "wall_other_ms" | "wall_approximate"
122
+ >>;
123
+
124
+ /** Refresh live telemetry without waiting for a child state transition. */
125
+ export function updateAgentChildMetrics(
126
+ home: string,
127
+ sessionId: string,
128
+ childId: string,
129
+ metrics: AgentChildTelemetry,
130
+ ): void {
131
+ const path = agentRecordPath(home, sessionId, childId);
132
+ try {
133
+ const record = JSON.parse(readFileSync(path, "utf8")) as Partial<AgentChildRecord>;
134
+ if (record.v !== 1 || record.kind !== "agent" || record.session_id !== sessionId || record.child_id !== childId) return;
135
+ writeAgentChildRecord(home, { ...(record as AgentChildRecord), ...metrics });
136
+ } catch {
137
+ // A metrics refresh must never interfere with the in-process child.
138
+ }
139
+ }
140
+
141
+ export function updateAgentChildTokens(
142
+ home: string,
143
+ sessionId: string,
144
+ childId: string,
145
+ tokens: { input: number; output: number; cacheRead: number; cacheWrite: number },
146
+ ): void {
147
+ updateAgentChildMetrics(home, sessionId, childId, {
148
+ tokens_input: tokens.input,
149
+ tokens_output: tokens.output,
150
+ tokens_cache_read: tokens.cacheRead,
151
+ tokens_cache_write: tokens.cacheWrite,
152
+ });
90
153
  }
91
154
 
92
155
  export function removeAgentChildRecord(home: string, sessionId: string, childId: string): void {