@narumitw/pi-subagents 0.49.3 → 0.52.0

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.
Files changed (83) hide show
  1. package/README.md +362 -53
  2. package/package.json +10 -7
  3. package/src/adaptive-scheduler.ts +224 -0
  4. package/src/admission-benchmark.ts +95 -0
  5. package/src/admission-policy.ts +78 -0
  6. package/src/agent-projection.ts +53 -0
  7. package/src/agents.ts +58 -1
  8. package/src/auto-transport.ts +114 -0
  9. package/src/blocking-status.ts +63 -0
  10. package/src/capabilities.ts +145 -0
  11. package/src/capability-grant.ts +115 -0
  12. package/src/capability-router.ts +107 -0
  13. package/src/completion-delivery.ts +257 -0
  14. package/src/config-status.ts +221 -0
  15. package/src/config-ui.ts +215 -236
  16. package/src/consult-resources.ts +4 -27
  17. package/src/consult.ts +9 -1
  18. package/src/create-stateful-transport.ts +55 -0
  19. package/src/delegation-contract.ts +417 -0
  20. package/src/execution-plan.ts +322 -0
  21. package/src/execution-profiles.ts +95 -0
  22. package/src/execution-ui.ts +320 -0
  23. package/src/execution.ts +1098 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +321 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +109 -0
  30. package/src/outcome.ts +61 -0
  31. package/src/panel-child-group.ts +35 -0
  32. package/src/panel-contract.ts +343 -0
  33. package/src/panel-evidence.ts +59 -0
  34. package/src/panel-execution.ts +770 -0
  35. package/src/panel-failure.ts +56 -0
  36. package/src/panel-planning.ts +175 -0
  37. package/src/panel-prompts.ts +132 -0
  38. package/src/panel-reconciliation.ts +57 -0
  39. package/src/panel-render.ts +103 -0
  40. package/src/parallel-limit-ui.ts +112 -0
  41. package/src/params.ts +179 -3
  42. package/src/persistence.ts +182 -32
  43. package/src/prompt-resources.ts +38 -0
  44. package/src/registry-types.ts +175 -0
  45. package/src/registry.ts +466 -143
  46. package/src/render.ts +72 -6
  47. package/src/result-contract.ts +416 -0
  48. package/src/retained-semantic-state.ts +100 -0
  49. package/src/rpc-timeout-finalization.ts +207 -0
  50. package/src/rpc-transport-metadata.ts +65 -0
  51. package/src/rpc-transport.ts +990 -0
  52. package/src/rpc-turn-capture.ts +142 -0
  53. package/src/runner-result.ts +55 -0
  54. package/src/runner-usage.ts +48 -0
  55. package/src/runner.ts +325 -73
  56. package/src/semantic-snapshot.ts +214 -0
  57. package/src/settings.ts +254 -35
  58. package/src/spawn-idempotency.ts +61 -0
  59. package/src/stateful-config.ts +13 -0
  60. package/src/stateful-guidance.ts +1 -0
  61. package/src/stateful-lifecycle.ts +45 -2
  62. package/src/stateful-limit-ui.ts +246 -0
  63. package/src/stateful-limits.ts +96 -0
  64. package/src/stateful-prompt.ts +11 -2
  65. package/src/stateful-render.ts +48 -3
  66. package/src/stateful.ts +467 -357
  67. package/src/subagents.ts +114 -46
  68. package/src/subprocess-transport.ts +64 -5
  69. package/src/supervision.ts +103 -0
  70. package/src/timeout-checkpoint.ts +305 -0
  71. package/src/timeout-finalization.ts +75 -0
  72. package/src/transport-types.ts +68 -0
  73. package/src/transport-ui.ts +169 -0
  74. package/src/transport.ts +16 -4
  75. package/src/turn-budget.ts +109 -0
  76. package/src/verification-policy.ts +67 -0
  77. package/src/work-item-ledger.ts +931 -0
  78. package/src/work-item-persistence.ts +223 -0
  79. package/src/workflow-planning.ts +162 -0
  80. package/src/workflow-tree-identity.ts +289 -0
  81. package/src/workflow-ui.ts +61 -0
  82. package/src/workflow-verification.ts +296 -0
  83. package/src/workspace.ts +69 -12
@@ -9,12 +9,29 @@ import {
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { type AgentConfig, discoverAgents, type SubagentThinkingLevel } from "./agents.js";
11
11
  import { redactPrivateText } from "./context.js";
12
+ import { appendDelegationContract } from "./delegation-contract.js";
12
13
  import { resolveDefaultSubagentTimeoutMs } from "./execution.js";
13
14
  import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
14
- import { assertPiPromptSourcesAreReadableFiles } from "./prompt-source-safety.js";
15
+ import { resolvePiPromptResources } from "./prompt-resources.js";
15
16
  import type { AgentTurn, ManagedAgent, TurnOutcome } from "./registry.js";
17
+ import { appendResultInstruction } from "./result-contract.js";
18
+ import { safeTerminalLine } from "./safe-text.js";
16
19
  import { readSubagentSettings } from "./settings.js";
20
+ import {
21
+ formatTimeoutCheckpoint,
22
+ formatTurnTerminationMessage,
23
+ journalMessages,
24
+ TimeoutProgressJournal,
25
+ TURN_TERMINATION_VERSION,
26
+ type TurnTerminationReport,
27
+ } from "./timeout-checkpoint.js";
28
+ import {
29
+ buildTimeoutFinalizationPrompt,
30
+ resolveTimeoutFinalizationMs,
31
+ } from "./timeout-finalization.js";
17
32
  import type { SubagentTransport } from "./transport.js";
33
+ import type { TransportProgressCallback, TransportTelemetry } from "./transport-types.js";
34
+ import { TurnBudgetMonitor, type TurnBudgetStop, type TurnLimits } from "./turn-budget.js";
18
35
 
19
36
  const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
20
37
  const DEFAULT_ABORT_GRACE_MS = 5_000;
@@ -57,6 +74,9 @@ export interface ParentRuntimeSnapshot {
57
74
  export interface ChildSession {
58
75
  readonly sessionId: string;
59
76
  readonly messages: readonly unknown[];
77
+ readonly provider?: string;
78
+ readonly model?: string;
79
+ readonly thinkingLevel?: SubagentThinkingLevel;
60
80
  prompt(text: string): Promise<void>;
61
81
  subscribe(listener: (event: unknown) => void): () => void;
62
82
  abort(): Promise<void>;
@@ -83,6 +103,7 @@ export interface InProcessTransportOptions {
83
103
  discoverAgent?: (agent: ManagedAgent) => AgentConfig | undefined;
84
104
  defaultTimeoutMs?: number;
85
105
  abortGraceMs?: number;
106
+ timeoutFinalizationMs?: number;
86
107
  }
87
108
 
88
109
  interface ChildSessionRecord {
@@ -96,6 +117,7 @@ type PromptSettlement =
96
117
  | { kind: "completed" }
97
118
  | { kind: "failed"; error: unknown }
98
119
  | { kind: "timeout" }
120
+ | { kind: "limit"; stop: TurnBudgetStop }
99
121
  | { kind: "aborted" };
100
122
 
101
123
  export class InProcessTransport implements SubagentTransport {
@@ -118,37 +140,101 @@ export class InProcessTransport implements SubagentTransport {
118
140
  this.abortGraceMs = options.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS;
119
141
  }
120
142
 
121
- async runTurn(agent: ManagedAgent, task: string, signal: AbortSignal): Promise<TurnOutcome> {
122
- if (signal.aborted) return interruptedOutcome("");
143
+ async runTurn(
144
+ agent: ManagedAgent,
145
+ task: string,
146
+ signal: AbortSignal,
147
+ onProgress?: TransportProgressCallback,
148
+ ): Promise<TurnOutcome> {
149
+ const startedAt = Date.now();
150
+ let telemetry: TransportTelemetry = {
151
+ transport: "in-process",
152
+ phase: "starting",
153
+ updatedAt: startedAt,
154
+ timing: { startedAt, transportStartedAt: startedAt },
155
+ };
156
+ const publish = (patch: Partial<TransportTelemetry>) => {
157
+ telemetry = {
158
+ ...telemetry,
159
+ ...patch,
160
+ timing: { ...telemetry.timing, ...patch.timing },
161
+ updatedAt: Date.now(),
162
+ };
163
+ onProgress?.({ ...telemetry, timing: { ...telemetry.timing } });
164
+ };
165
+ publish({});
166
+ if (signal.aborted) {
167
+ publish({ phase: "interrupted", failurePhase: "starting" });
168
+ return { ...interruptedOutcome(""), telemetry };
169
+ }
123
170
  const agentConfig = this.discoverAgent(agent);
124
171
  if (!agentConfig) {
125
- return { output: "", exitCode: 1, error: `Unknown subagent: ${agent.agent}` };
172
+ publish({ phase: "failed", failurePhase: "starting" });
173
+ return {
174
+ output: "",
175
+ exitCode: 1,
176
+ error: `Unknown subagent: ${agent.agent}`,
177
+ telemetry,
178
+ };
126
179
  }
127
180
  let tools: string[] | undefined;
128
181
  try {
129
- tools = validateInProcessTools(agentConfig.tools);
182
+ tools = validateInProcessTools(agent.executionPlan?.effectiveTools ?? agentConfig.tools);
130
183
  } catch (error) {
131
- return { output: "", exitCode: 1, error: errorMessage(error) };
184
+ publish({ phase: "failed", failurePhase: "starting" });
185
+ return { output: "", exitCode: 1, error: errorMessage(error), telemetry };
132
186
  }
133
187
  let record: ChildSessionRecord;
134
188
  try {
135
189
  record = await this.getOrCreate(agent, agentConfig, tools);
136
190
  } catch (error) {
137
- return { output: "", exitCode: 1, error: errorMessage(error) };
191
+ publish({ phase: "failed", failurePhase: "starting" });
192
+ return { output: "", exitCode: 1, error: errorMessage(error), telemetry };
193
+ }
194
+ publish({
195
+ phase: "ready",
196
+ provider: record.session.provider,
197
+ model: record.session.model,
198
+ thinkingLevel: record.session.thinkingLevel,
199
+ timing: { readyAt: Date.now() },
200
+ });
201
+ if (signal.aborted) {
202
+ await this.releaseById(agent.id).catch(() => undefined);
203
+ publish({ phase: "interrupted", failurePhase: "ready" });
204
+ return { ...interruptedOutcome(""), telemetry };
138
205
  }
139
- if (signal.aborted) return interruptedOutcome("");
140
206
  const prompt = buildCurrentTurnPrompt(agent, task);
141
- const timeoutMs = agentConfig.timeoutMs ?? this.defaultTimeoutMs;
207
+ const timeoutMs =
208
+ agent.currentTimeoutMs ?? agent.timeoutMs ?? agentConfig.timeoutMs ?? this.defaultTimeoutMs;
142
209
  const startingMessageCount = record.session.messages.length;
143
210
  record.lastOutput = "";
144
- const settlement = await this.runPrompt(record, prompt, signal, timeoutMs);
211
+ publish({ phase: "running", timing: { promptAcceptedAt: Date.now() } });
212
+ const settlement = await this.runPrompt(record, prompt, signal, timeoutMs, {
213
+ idleTimeoutMs: agent.currentIdleTimeoutMs ?? agent.idleTimeoutMs,
214
+ maxTurns: agent.currentMaxTurns ?? agent.maxTurns,
215
+ maxToolCalls: agent.currentMaxToolCalls ?? agent.maxToolCalls,
216
+ });
145
217
  const final = latestAssistant(record.session.messages.slice(startingMessageCount));
146
218
  const output = truncateUtf8(final.output || record.lastOutput, DEFAULT_MAX_OUTPUT_BYTES);
147
219
  const truncated = output.truncated || agent.contextTruncated;
148
220
  const policy = inProcessPolicy(agentConfig, agent);
221
+ const settledAt = Date.now();
222
+ if (signal.aborted && (settlement.kind === "timeout" || settlement.kind === "limit")) {
223
+ publish({ phase: "interrupted", failurePhase: "running", timing: { settledAt } });
224
+ return {
225
+ ...interruptedOutcome(output.text),
226
+ truncated,
227
+ policy,
228
+ telemetry,
229
+ };
230
+ }
149
231
 
150
232
  switch (settlement.kind) {
151
233
  case "completed":
234
+ publish({
235
+ phase: final.stopReason === "error" ? "failed" : "settled",
236
+ timing: { settledAt },
237
+ });
152
238
  if (final.stopReason === "error") {
153
239
  return {
154
240
  output: output.text,
@@ -156,13 +242,16 @@ export class InProcessTransport implements SubagentTransport {
156
242
  truncated,
157
243
  error: final.error || "In-process subagent returned an error",
158
244
  policy,
245
+ telemetry,
159
246
  };
160
247
  }
161
248
  if (final.stopReason === "aborted") {
249
+ publish({ phase: "interrupted", failurePhase: "running" });
162
250
  return {
163
251
  ...interruptedOutcome(output.text),
164
252
  truncated,
165
253
  policy,
254
+ telemetry,
166
255
  };
167
256
  }
168
257
  return {
@@ -170,28 +259,104 @@ export class InProcessTransport implements SubagentTransport {
170
259
  exitCode: 0,
171
260
  truncated,
172
261
  policy,
262
+ telemetry,
173
263
  };
174
264
  case "failed":
265
+ publish({ phase: "failed", failurePhase: "running", timing: { settledAt } });
175
266
  return {
176
267
  output: output.text,
177
268
  exitCode: 1,
178
269
  truncated,
179
270
  error: errorMessage(settlement.error),
180
271
  policy,
272
+ telemetry,
181
273
  };
182
274
  case "timeout":
275
+ case "limit": {
276
+ const stop =
277
+ settlement.kind === "limit"
278
+ ? settlement.stop
279
+ : ({ reason: "work_timeout", limit: timeoutMs } as const);
280
+ const journal = new TimeoutProgressJournal();
281
+ journalMessages(journal, record.session.messages.slice(startingMessageCount));
282
+ const termination: TurnTerminationReport = {
283
+ version: TURN_TERMINATION_VERSION,
284
+ reason: stop.reason,
285
+ limit: stop.limit,
286
+ checkpoint: journal.checkpoint(task, output.text),
287
+ finalization: { attempted: false, status: "skipped", durationMs: 0 },
288
+ };
289
+ let finalizedOutput = output.text || formatTimeoutCheckpoint(termination.checkpoint);
290
+ let finalizationError: string | undefined;
291
+ if (!signal.aborted && this.sessions.get(agent.id) === record) {
292
+ publish({ phase: "finalizing", failurePhase: "running" });
293
+ const summaryStart = record.session.messages.length;
294
+ const finalizationStartedAt = Date.now();
295
+ record.lastOutput = "";
296
+ const summarySettlement = await this.runPrompt(
297
+ record,
298
+ buildTimeoutFinalizationPrompt({
299
+ task,
300
+ partialOutput: output.text,
301
+ checkpoint: termination.checkpoint,
302
+ terminationReason: stop.reason,
303
+ resultFormat: agent.resultFormat,
304
+ }),
305
+ signal,
306
+ resolveTimeoutFinalizationMs(timeoutMs, this.options.timeoutFinalizationMs),
307
+ );
308
+ const summary = latestAssistant(record.session.messages.slice(summaryStart));
309
+ const boundedSummary = truncateUtf8(
310
+ summary.output || record.lastOutput,
311
+ DEFAULT_MAX_OUTPUT_BYTES,
312
+ );
313
+ if (
314
+ summarySettlement.kind === "completed" &&
315
+ summary.stopReason !== "error" &&
316
+ boundedSummary.text.trim()
317
+ ) {
318
+ finalizedOutput = boundedSummary.text;
319
+ termination.finalization = {
320
+ attempted: true,
321
+ status: "completed",
322
+ durationMs: Date.now() - finalizationStartedAt,
323
+ };
324
+ } else {
325
+ finalizationError =
326
+ summarySettlement.kind === "failed"
327
+ ? errorMessage(summarySettlement.error)
328
+ : `timeout summary ${summarySettlement.kind}`;
329
+ termination.finalization = {
330
+ attempted: true,
331
+ status: summarySettlement.kind === "timeout" ? "timed_out" : "failed",
332
+ durationMs: Date.now() - finalizationStartedAt,
333
+ error: finalizationError,
334
+ };
335
+ }
336
+ }
337
+ publish({ phase: "failed", failurePhase: "running", timing: { settledAt: Date.now() } });
183
338
  return {
184
- output: output.text,
339
+ output: finalizedOutput,
185
340
  exitCode: 124,
186
341
  truncated,
187
- error: `In-process subagent timed out after ${timeoutMs}ms`,
342
+ error: [
343
+ formatTurnTerminationMessage(stop.reason, stop.limit, "In-process subagent"),
344
+ finalizationError,
345
+ ]
346
+ .filter(Boolean)
347
+ .join("; "),
188
348
  policy,
349
+ termination,
350
+ telemetry,
189
351
  };
352
+ }
190
353
  case "aborted":
354
+ publish({ phase: "interrupted", failurePhase: "running", timing: { settledAt } });
191
355
  return {
192
356
  ...interruptedOutcome(output.text),
193
357
  truncated,
194
358
  policy,
359
+ telemetry,
195
360
  };
196
361
  }
197
362
  }
@@ -289,11 +454,41 @@ export class InProcessTransport implements SubagentTransport {
289
454
  prompt: string,
290
455
  signal: AbortSignal,
291
456
  timeoutMs: number,
457
+ limits: TurnLimits = {},
292
458
  ): Promise<PromptSettlement> {
459
+ if (signal.aborted) return { kind: "aborted" };
293
460
  let timeout: NodeJS.Timeout | undefined;
294
461
  let abortHandler: (() => void) | undefined;
295
- const promptSettlement: Promise<PromptSettlement> = record.session
296
- .prompt(prompt)
462
+ let resolveLimit!: (settlement: PromptSettlement) => void;
463
+ const limitSettlement = new Promise<PromptSettlement>((resolve) => {
464
+ resolveLimit = resolve;
465
+ });
466
+ const monitor = new TurnBudgetMonitor({
467
+ ...limits,
468
+ onExceeded: (stop) => resolveLimit({ kind: "limit", stop }),
469
+ });
470
+ const unsubscribeBudget = record.session.subscribe((event) => {
471
+ const type = childEventType(event);
472
+ if (type === "tool_execution_end") monitor.recordActivity();
473
+ if (type !== "message_end") return;
474
+ const message = eventMessage(event);
475
+ if (!message || typeof message !== "object" || Array.isArray(message)) return;
476
+ const value = message as Record<string, unknown>;
477
+ if (value.role === "toolResult") {
478
+ monitor.recordActivity();
479
+ return;
480
+ }
481
+ if (value.role !== "assistant") return;
482
+ monitor.recordToolCalls(assistantToolCallCount(value.content));
483
+ monitor.recordAssistantTurn(
484
+ typeof value.stopReason === "string" ? value.stopReason : undefined,
485
+ );
486
+ });
487
+ const promptSettlement: Promise<PromptSettlement> = Promise.resolve()
488
+ .then(() => {
489
+ if (signal.aborted) throw new Error("In-process subagent prompt aborted before start");
490
+ return record.session.prompt(prompt);
491
+ })
297
492
  .then(() => ({ kind: "completed" as const }))
298
493
  .catch((error: unknown) => ({ kind: "failed" as const, error }));
299
494
  const timeoutSettlement = new Promise<PromptSettlement>((resolve) => {
@@ -302,13 +497,31 @@ export class InProcessTransport implements SubagentTransport {
302
497
  const abortSettlement = new Promise<PromptSettlement>((resolve) => {
303
498
  abortHandler = () => resolve({ kind: "aborted" });
304
499
  signal.addEventListener("abort", abortHandler, { once: true });
500
+ if (signal.aborted) abortHandler();
305
501
  });
306
- const settlement = await Promise.race([promptSettlement, timeoutSettlement, abortSettlement]);
502
+ const settlement = await Promise.race([
503
+ promptSettlement,
504
+ timeoutSettlement,
505
+ abortSettlement,
506
+ limitSettlement,
507
+ ]);
307
508
  if (timeout) clearTimeout(timeout);
308
509
  if (abortHandler) signal.removeEventListener("abort", abortHandler);
309
- if (settlement.kind === "timeout" || settlement.kind === "aborted") {
310
- await settleWithin(record.session.abort(), this.abortGraceMs);
311
- const settledAfterAbort = await completesWithin(promptSettlement, this.abortGraceMs);
510
+ try {
511
+ unsubscribeBudget();
512
+ } catch {
513
+ // The owning record's unsubscribe/dispose path remains authoritative.
514
+ }
515
+ monitor.dispose();
516
+ if (
517
+ settlement.kind === "timeout" ||
518
+ settlement.kind === "limit" ||
519
+ settlement.kind === "aborted"
520
+ ) {
521
+ const [, settledAfterAbort] = await Promise.all([
522
+ settleWithin(record.session.abort(), this.abortGraceMs),
523
+ completesWithin(promptSettlement, this.abortGraceMs),
524
+ ]);
312
525
  if (!settledAfterAbort) this.discardRecord(record);
313
526
  }
314
527
  return settlement;
@@ -331,7 +544,7 @@ export function validateInProcessTools(tools: string[] | undefined): string[] |
331
544
  const unsupported = unique.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool));
332
545
  if (unsupported.length > 0) {
333
546
  throw new Error(
334
- `In-process subagents cannot load extension/custom tools: ${unsupported.join(", ")}. Use stateful.transport "subprocess" for this agent.`,
547
+ `In-process subagents cannot load extension/custom tools: ${unsupported.map((tool) => safeTerminalLine(tool, 256)).join(", ")}. Use stateful.transport "subprocess" for this agent.`,
335
548
  );
336
549
  }
337
550
  return unique;
@@ -392,6 +605,15 @@ export async function createSdkChildSession(
392
605
  get messages() {
393
606
  return session.messages;
394
607
  },
608
+ get provider() {
609
+ return session.model?.provider;
610
+ },
611
+ get model() {
612
+ return session.model?.id;
613
+ },
614
+ get thinkingLevel() {
615
+ return session.thinkingLevel;
616
+ },
395
617
  prompt: (text) => session.prompt(text),
396
618
  subscribe: (listener) => session.subscribe((event) => listener(event)),
397
619
  abort: () => session.abort(),
@@ -491,7 +713,7 @@ async function prepareInProcessServices(
491
713
  agentSystemPrompt: string,
492
714
  projectTrusted: boolean,
493
715
  ): Promise<{ services: AgentSessionServices; support: CoreSessionSupport }> {
494
- assertPiPromptSourcesAreReadableFiles(cwd, agentDir, projectTrusted, ["SYSTEM.md"]);
716
+ const promptResources = await resolvePiPromptResources(cwd, projectTrusted, agentDir);
495
717
  const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
496
718
  const support = await loadCoreSessionSupport();
497
719
  if (!support) throw unsupportedInProcessCoreError();
@@ -501,7 +723,10 @@ async function prepareInProcessServices(
501
723
  settingsManager,
502
724
  resourceLoaderOptions: {
503
725
  noExtensions: true,
504
- appendSystemPrompt: agentSystemPrompt.trim() ? [agentSystemPrompt] : [],
726
+ appendSystemPrompt: [
727
+ ...promptResources.appendSystemPromptPaths,
728
+ ...(agentSystemPrompt.trim() ? [agentSystemPrompt] : []),
729
+ ],
505
730
  },
506
731
  });
507
732
  return { services, support };
@@ -556,17 +781,19 @@ export function seedChildSessionManager(
556
781
  }
557
782
  }
558
783
 
559
- function buildCurrentTurnPrompt(agent: ManagedAgent, task: string): string {
784
+ export function buildCurrentTurnPrompt(agent: ManagedAgent, task: string): string {
560
785
  const ids = new Set(agent.currentMailboxMessageIds ?? []);
561
786
  const messages = agent.mailbox
562
787
  .filter((message) => ids.has(message.id))
563
788
  .slice(-20)
564
789
  .map((message) => `From ${message.senderId}: ${redactPrivateText(message.content)}`)
565
790
  .join("\n");
791
+ const base = messages
792
+ ? `${redactPrivateText(task)}\n\nMailbox messages:\n${messages}`
793
+ : redactPrivateText(task);
794
+ const contracted = appendDelegationContract(base, agent.contract, DEFAULT_MAX_CONTEXT_BYTES);
566
795
  return truncateUtf8(
567
- messages
568
- ? `${redactPrivateText(task)}\n\nMailbox messages:\n${messages}`
569
- : redactPrivateText(task),
796
+ appendResultInstruction(contracted.text, agent.resultFormat),
570
797
  DEFAULT_MAX_CONTEXT_BYTES,
571
798
  ).text;
572
799
  }
@@ -590,6 +817,23 @@ function latestAssistant(messages: readonly unknown[]): {
590
817
  return { output: "" };
591
818
  }
592
819
 
820
+ function childEventType(event: unknown): string | undefined {
821
+ if (!event || typeof event !== "object" || Array.isArray(event)) return undefined;
822
+ const type = (event as { type?: unknown }).type;
823
+ return typeof type === "string" ? type : undefined;
824
+ }
825
+
826
+ function assistantToolCallCount(content: unknown): number {
827
+ if (!Array.isArray(content)) return 0;
828
+ return content.filter(
829
+ (part) =>
830
+ part &&
831
+ typeof part === "object" &&
832
+ !Array.isArray(part) &&
833
+ (part as { type?: unknown }).type === "toolCall",
834
+ ).length;
835
+ }
836
+
593
837
  function eventMessage(event: unknown): unknown {
594
838
  if (!event || typeof event !== "object") return undefined;
595
839
  const candidate = event as { type?: string; message?: unknown };
@@ -24,6 +24,7 @@ export function renderInspectCall(args: Partial<SubagentInspectParams>, theme: T
24
24
  if (args.agentScope) metadata.push(`[${args.agentScope}]`);
25
25
  if (args.agent) metadata.push(`agent:${safeLine(args.agent, "", 256)}`);
26
26
  if (args.agentId) metadata.push(`id:${safeLine(args.agentId, "", 256)}`);
27
+ if (args.workflowId) metadata.push(`workflow:${safeLine(args.workflowId, "", 256)}`);
27
28
  if (args.includeClosed) metadata.push("include closed");
28
29
  return new Text(toolHeader(theme, "subagent_inspect", action, metadata), 0, 0);
29
30
  }
@@ -74,6 +75,23 @@ function renderAction(
74
75
  if (!expanded) lines.push(expansionHint());
75
76
  return lines.join("\n");
76
77
  }
78
+ case "list_workflows":
79
+ return renderList(
80
+ "workflow",
81
+ recordList(details.workflows),
82
+ details,
83
+ expanded,
84
+ theme,
85
+ formatWorkflow,
86
+ );
87
+ case "get_workflow": {
88
+ const workflow = recordValue(details.workflow);
89
+ if (!workflow) return undefined;
90
+ return [
91
+ `${statusBadge(theme, "completed")} · workflow ${theme.fg("accent", safeLine(workflow.workflowId, "workflow", 256))}`,
92
+ formatWorkflow(workflow, theme, expanded),
93
+ ].join("\n");
94
+ }
77
95
  case "list_models":
78
96
  return renderList(
79
97
  "model",
@@ -84,6 +102,17 @@ function renderAction(
84
102
  formatModel,
85
103
  stringValue(details.source),
86
104
  );
105
+ case "preview_context": {
106
+ const preview = recordValue(details.preview);
107
+ if (!preview) return undefined;
108
+ return [
109
+ `${statusBadge(theme, "completed")} · context ${theme.fg("accent", safeLine(preview.mode, "none", 128))}`,
110
+ theme.fg(
111
+ "dim",
112
+ `${numberValue(preview.turns)} turns · ${numberValue(preview.sourceCount)} sources · ${numberValue(preview.bytes)} bytes${preview.truncated === true ? " · truncated" : ""}`,
113
+ ),
114
+ ].join("\n");
115
+ }
87
116
  case "status":
88
117
  return renderStatus(details, expanded, theme);
89
118
  case "diagnose":
@@ -146,17 +175,61 @@ function formatRun(run: Record<string, unknown>, theme: Theme, expanded: boolean
146
175
  ];
147
176
  if (expanded) {
148
177
  const thinking = stringValue(run.thinkingLevel);
178
+ const timeout = numberValue(run.currentTimeoutMs) || numberValue(run.timeoutMs);
179
+ const idleTimeout = numberValue(run.currentIdleTimeoutMs) || numberValue(run.idleTimeoutMs);
180
+ const maxTurns = numberValue(run.currentMaxTurns) || numberValue(run.maxTurns);
181
+ const maxToolCalls = numberValue(run.currentMaxToolCalls) || numberValue(run.maxToolCalls);
149
182
  const task = safeBlock(run.currentTask, "", 2 * 1024).trim();
150
183
  const error = safeBlock(run.error, "", 2 * 1024).trim();
151
184
  lines.push(
152
- ` ${theme.fg("dim", `${numberValue(run.historyCount)} history · ${thinking ? `thinking:${safeLine(thinking, "", 128)} · ` : ""}${numberValue(run.children)} children`)}`,
185
+ ` ${theme.fg("dim", `${numberValue(run.historyCount)} history · ${thinking ? `thinking:${safeLine(thinking, "", 128)} · ` : ""}${timeout ? `timeout:${timeout}ms · ` : ""}${idleTimeout ? `idle:${idleTimeout}ms · ` : ""}${maxTurns ? `turns:${maxTurns} · ` : ""}${maxToolCalls ? `tools:${maxToolCalls} · ` : ""}${numberValue(run.children)} children`)}`,
153
186
  );
187
+ const context = recordValue(run.context);
188
+ const telemetry = recordValue(run.telemetry);
189
+ if (context) {
190
+ lines.push(
191
+ ` ${theme.fg("dim", `context: ${numberValue(context.turns)} turns · ${numberValue(context.sources)} sources · ${numberValue(context.bytes)} bytes${context.truncated === true ? " · truncated" : ""}`)}`,
192
+ );
193
+ }
194
+ if (telemetry) {
195
+ lines.push(
196
+ ` ${theme.fg("dim", `transport: ${safeLine(telemetry.transport, "unknown", 128)} · phase:${safeLine(telemetry.phase, "unknown", 128)}${stringValue(telemetry.protocol) ? ` · ${safeLine(telemetry.protocol, "", 128)}` : ""}`)}`,
197
+ );
198
+ }
154
199
  if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
155
200
  if (error) lines.push(` ${theme.fg("error", `error: ${error}`)}`);
156
201
  }
157
202
  return lines.join("\n");
158
203
  }
159
204
 
205
+ function formatWorkflow(
206
+ workflow: Record<string, unknown>,
207
+ theme: Theme,
208
+ expanded: boolean,
209
+ ): string {
210
+ const id = safeLine(workflow.workflowId, "workflow", 256);
211
+ const states = recordValue(workflow.states);
212
+ const stateText = states
213
+ ? Object.entries(states)
214
+ .map(([state, count]) => `${safeLine(state, "unknown", 128)}:${numberValue(count)}`)
215
+ .join(" · ")
216
+ : "";
217
+ const lines = [
218
+ `${theme.fg("muted", "• ")}${theme.fg("accent", id)} ${theme.fg("muted", `${numberValue(workflow.itemCount)} items · generation:${numberValue(workflow.generation)}`)}`,
219
+ ];
220
+ if (stateText) lines.push(` ${theme.fg("dim", stateText)}`);
221
+ if (expanded) {
222
+ for (const item of recordList(workflow.items).slice(0, COLLAPSED_LIST_LIMIT)) {
223
+ lines.push(
224
+ ` ${theme.fg("muted", "- ")}${theme.fg("toolOutput", safeLine(item.id, "task", 256))} ${theme.fg("dim", safeLine(item.state, "unknown", 128))}`,
225
+ );
226
+ }
227
+ const omitted = numberValue(workflow.omittedItems);
228
+ if (omitted > 0) lines.push(theme.fg("muted", ` … ${omitted} tasks omitted`));
229
+ }
230
+ return lines.join("\n");
231
+ }
232
+
160
233
  function formatModel(model: Record<string, unknown>, theme: Theme, expanded: boolean): string {
161
234
  const identity = `${safeLine(model.provider, "provider", 256)}/${safeLine(model.id, "model", 256)}`;
162
235
  const current = booleanValue(model.current) ? theme.fg("success", " · current") : "";
@@ -181,11 +254,15 @@ function renderStatus(
181
254
  const status = recordValue(details.status);
182
255
  const stateful = recordValue(status?.stateful);
183
256
  if (!status || !stateful) return undefined;
257
+ const currentLimits = recordValue(status.statefulLimits) ?? recordValue(stateful.limits);
184
258
  const lines = [
185
259
  `${statusBadge(theme, "completed")} · runtime status`,
186
260
  `${theme.fg("muted", "workflow: ")}${theme.fg("accent", safeLine(status.workflow, "unknown", 128))} · ${numberValue(stateful.activeAgents)} active · ${numberValue(stateful.retainedAgents)} retained`,
187
261
  `${theme.fg("muted", "stateful: ")}${stateful.initialized === true ? "initialized" : "not initialized"} · resources: ${safeLine(status.consultResources, "unknown", 128)}`,
188
262
  ];
263
+ if (currentLimits) {
264
+ lines.push(`${theme.fg("muted", "limits: ")}${formatDetachedLimits(currentLimits)}`);
265
+ }
189
266
  if (expanded) {
190
267
  const delivery = stringValue(stateful.completionDelivery);
191
268
  const transport = stringValue(stateful.transport);
@@ -202,10 +279,33 @@ function renderStatus(
202
279
  ),
203
280
  );
204
281
  }
282
+ const configured = recordValue(status.configuredStatefulLimits);
283
+ const sources = recordValue(status.configuredStatefulLimitSources);
284
+ if (configured) {
285
+ lines.push(theme.fg("dim", `configured: ${formatDetachedLimits(configured, sources)}`));
286
+ }
205
287
  } else lines.push(expansionHint());
206
288
  return lines.join("\n");
207
289
  }
208
290
 
291
+ function formatDetachedLimits(
292
+ limits: Record<string, unknown>,
293
+ sources?: Record<string, unknown>,
294
+ ): string {
295
+ return [
296
+ ["agents", "maxAgents"],
297
+ ["active", "maxActiveTurns"],
298
+ ["children", "maxChildrenPerAgent"],
299
+ ["depth", "maxDepth"],
300
+ ["stored", "maxStoredAgents"],
301
+ ]
302
+ .map(([label, field]) => {
303
+ const source = stringValue(sources?.[field]);
304
+ return `${label}:${numberValue(limits[field])}${source ? ` (${safeLine(source, "", 64)})` : ""}`;
305
+ })
306
+ .join(" · ");
307
+ }
308
+
209
309
  function renderDiagnose(details: Record<string, unknown>, expanded: boolean, theme: Theme): string {
210
310
  const checks = recordList(details.checks);
211
311
  const hasFail = checks.some((check) => check.status === "fail");