pi-subagents 0.36.0 → 0.37.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 (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +93 -8
  3. package/agents/delegate.md +2 -0
  4. package/agents/worker.md +2 -0
  5. package/package.json +4 -2
  6. package/skills/pi-subagents/SKILL.md +20 -4
  7. package/src/agents/agent-management.ts +7 -1
  8. package/src/agents/agents.ts +99 -12
  9. package/src/api/capability-ceiling.ts +17 -0
  10. package/src/api/delegation.ts +3 -1
  11. package/src/api/preflight.ts +399 -0
  12. package/src/extension/index.ts +2 -0
  13. package/src/extension/rpc.ts +4 -0
  14. package/src/extension/schemas.ts +8 -2
  15. package/src/extension/tool-description.ts +2 -0
  16. package/src/runs/background/async-execution.ts +125 -14
  17. package/src/runs/background/async-resume.ts +24 -7
  18. package/src/runs/background/async-status.ts +18 -0
  19. package/src/runs/background/process-terminal.ts +280 -0
  20. package/src/runs/background/run-status.ts +7 -1
  21. package/src/runs/background/scheduled-runs.ts +6 -1
  22. package/src/runs/background/stale-run-reconciler.ts +6 -0
  23. package/src/runs/background/subagent-runner.ts +182 -12
  24. package/src/runs/foreground/chain-execution.ts +5 -0
  25. package/src/runs/foreground/execution.ts +59 -13
  26. package/src/runs/foreground/subagent-executor.ts +32 -2
  27. package/src/runs/shared/acceptance.ts +41 -30
  28. package/src/runs/shared/capability-ceiling.ts +177 -0
  29. package/src/runs/shared/dynamic-fanout.ts +1 -1
  30. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  31. package/src/runs/shared/nested-events.ts +8 -1
  32. package/src/runs/shared/parallel-utils.ts +5 -0
  33. package/src/runs/shared/pi-args.ts +141 -58
  34. package/src/runs/shared/session-lease.ts +25 -5
  35. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  36. package/src/runs/shared/tool-availability.ts +18 -2
  37. package/src/shared/launch-contract.ts +123 -0
  38. package/src/shared/types.ts +104 -7
  39. package/src/shared/utils.ts +17 -42
  40. package/src/slash/delegation-adapters.ts +1 -1
  41. package/src/slash/slash-commands.ts +1 -1
@@ -7,6 +7,7 @@ import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHI
7
7
  import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
8
8
  import {
9
9
  CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
10
+ MCP_DIRECT_CHILD_TOOLS_ENV,
10
11
  REQUIRED_CHILD_TOOLS_ENV,
11
12
  writeChildToolDiagnostic,
12
13
  type ChildToolDiagnostic,
@@ -78,12 +79,24 @@ function readRequiredChildTools(): string[] | undefined {
78
79
  return required;
79
80
  }
80
81
 
82
+ function readMcpDirectChildTools(): string[] | undefined {
83
+ const encoded = process.env[MCP_DIRECT_CHILD_TOOLS_ENV]?.trim();
84
+ if (!encoded) return undefined;
85
+ try {
86
+ const tools = JSON.parse(encoded) as unknown;
87
+ if (!Array.isArray(tools) || tools.some((name) => typeof name !== "string" || !name)) return undefined;
88
+ return tools;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
81
94
  function refreshChildToolDiagnostic(pi: ExtensionAPI): ChildToolDiagnostic | undefined {
82
95
  const filePath = process.env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim();
83
96
  const required = readRequiredChildTools();
84
97
  if (!filePath || !required) return undefined;
85
98
  const available = pi.getAllTools().map((tool) => tool.name);
86
- return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim());
99
+ return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim(), readMcpDirectChildTools());
87
100
  }
88
101
 
89
102
  function findSectionEnd(prompt: string, startIndex: number, nextHeaders: string[]): number {
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
4
  export const REQUIRED_CHILD_TOOLS_ENV = "PI_SUBAGENT_REQUIRED_TOOLS";
5
+ export const MCP_DIRECT_CHILD_TOOLS_ENV = "PI_SUBAGENT_MCP_DIRECT_TOOLS";
5
6
  export const CHILD_TOOL_DIAGNOSTIC_PATH_ENV = "PI_SUBAGENT_TOOL_DIAGNOSTIC_PATH";
6
7
 
7
8
  export interface ChildToolDiagnostic {
@@ -9,6 +10,7 @@ export interface ChildToolDiagnostic {
9
10
  required: string[];
10
11
  available: string[];
11
12
  missing: string[];
13
+ missingMcpDirectTools?: string[];
12
14
  }
13
15
 
14
16
  const PI_CORE_CHILD_TOOLS = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
@@ -18,6 +20,7 @@ export function writeChildToolDiagnostic(
18
20
  required: string[],
19
21
  available: string[],
20
22
  agent?: string,
23
+ mcpDirectTools?: string[],
21
24
  ): ChildToolDiagnostic | undefined {
22
25
  const availableNames = new Set([...available, ...PI_CORE_CHILD_TOOLS]);
23
26
  const missing = required.filter((name) => !availableNames.has(name));
@@ -26,7 +29,16 @@ export function writeChildToolDiagnostic(
26
29
  return undefined;
27
30
  }
28
31
 
29
- const diagnostic: ChildToolDiagnostic = { agent, required, available, missing };
32
+ const missingMcpDirectTools = mcpDirectTools?.length
33
+ ? missing.filter((name) => mcpDirectTools.includes(name))
34
+ : [];
35
+ const diagnostic: ChildToolDiagnostic = {
36
+ agent,
37
+ required,
38
+ available,
39
+ missing,
40
+ ...(missingMcpDirectTools.length > 0 ? { missingMcpDirectTools } : {}),
41
+ };
30
42
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
31
43
  fs.writeFileSync(filePath, JSON.stringify(diagnostic), { mode: 0o600 });
32
44
  return diagnostic;
@@ -36,7 +48,7 @@ export function readChildToolDiagnostic(filePath: string | undefined): ChildTool
36
48
  if (!filePath || !fs.existsSync(filePath)) return undefined;
37
49
  const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as Partial<ChildToolDiagnostic>;
38
50
  const stringArray = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
39
- if (!stringArray(parsed.required) || !stringArray(parsed.available) || !stringArray(parsed.missing) || (parsed.agent !== undefined && typeof parsed.agent !== "string")) {
51
+ if (!stringArray(parsed.required) || !stringArray(parsed.available) || !stringArray(parsed.missing) || (parsed.agent !== undefined && typeof parsed.agent !== "string") || (parsed.missingMcpDirectTools !== undefined && !stringArray(parsed.missingMcpDirectTools))) {
40
52
  throw new Error(`Malformed child tool diagnostic at '${filePath}'.`);
41
53
  }
42
54
  return {
@@ -44,6 +56,7 @@ export function readChildToolDiagnostic(filePath: string | undefined): ChildTool
44
56
  required: parsed.required,
45
57
  available: parsed.available,
46
58
  missing: parsed.missing,
59
+ ...(parsed.missingMcpDirectTools ? { missingMcpDirectTools: parsed.missingMcpDirectTools } : {}),
47
60
  };
48
61
  }
49
62
 
@@ -52,6 +65,9 @@ export function formatChildToolDiagnostic(diagnostic: ChildToolDiagnostic): stri
52
65
  return [
53
66
  `${subject} requested unavailable child tools: ${diagnostic.missing.join(", ")}.`,
54
67
  "The `tools` field is a strict allowlist; it does not load extension code.",
68
+ ...(diagnostic.missingMcpDirectTools?.length
69
+ ? [`Resolved MCP direct tools missing from the child registry: ${diagnostic.missingMcpDirectTools.join(", ")}. This indicates a host/pi-mcp-adapter registration problem, not a tool-call failure.`]
70
+ : []),
55
71
  "For extension tools, add the provider path to `subagentOnlyExtensions` (child-only), `extensions`, or as a path-like entry in `tools`, while keeping each registered tool name in `tools`.",
56
72
  "For MCP tools, verify the MCP adapter configuration and selected tool names. For builtin tools, verify the name against the installed Pi version.",
57
73
  ].join("\n");
@@ -0,0 +1,123 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import type { AgentConfig } from "../agents/agents.ts";
4
+
5
+ export const AGENT_DEFINITION_PROJECTION_VERSION = 1 as const;
6
+ export const LAUNCH_BINDING_PROJECTION_VERSION = 1 as const;
7
+
8
+ function stableJson(value: unknown): string {
9
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
10
+ if (value && typeof value === "object") {
11
+ return `{${Object.entries(value as Record<string, unknown>)
12
+ .filter(([, entry]) => entry !== undefined)
13
+ .sort(([a], [b]) => a.localeCompare(b))
14
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
15
+ .join(",")}}`;
16
+ }
17
+ return JSON.stringify(value);
18
+ }
19
+
20
+ function sha256(value: unknown): string {
21
+ return createHash("sha256").update(stableJson(value)).digest("hex");
22
+ }
23
+
24
+ function fileDigest(filePath: string): string | undefined {
25
+ try {
26
+ return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ /** Public-safe, deterministic evidence for the parsed launch-affecting agent definition. */
33
+ export function projectAgentDefinition(agent: AgentConfig): Record<string, unknown> {
34
+ return {
35
+ version: AGENT_DEFINITION_PROJECTION_VERSION,
36
+ name: agent.name,
37
+ localName: agent.localName,
38
+ packageName: agent.packageName,
39
+ filePath: agent.filePath,
40
+ fileContentDigest: fileDigest(agent.filePath),
41
+ systemPrompt: agent.systemPrompt,
42
+ systemPromptMode: agent.systemPromptMode,
43
+ inheritProjectContext: agent.inheritProjectContext,
44
+ inheritSkills: agent.inheritSkills,
45
+ model: agent.model,
46
+ fallbackModels: agent.fallbackModels,
47
+ thinking: agent.thinking,
48
+ tools: agent.tools,
49
+ mcpDirectTools: agent.mcpDirectTools,
50
+ extensions: agent.extensions,
51
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
52
+ skills: agent.skills,
53
+ skillPath: agent.skillPath,
54
+ output: agent.output,
55
+ defaultReads: agent.defaultReads,
56
+ defaultProgress: agent.defaultProgress,
57
+ defaultContext: agent.defaultContext,
58
+ defaultAsync: agent.defaultAsync,
59
+ defaultTimeoutMs: agent.defaultTimeoutMs,
60
+ defaultTurnBudget: agent.defaultTurnBudget,
61
+ defaultAcceptance: agent.defaultAcceptance,
62
+ acceptanceRole: agent.acceptanceRole,
63
+ interactive: agent.interactive,
64
+ maxSubagentDepth: agent.maxSubagentDepth,
65
+ completionGuard: agent.completionGuard,
66
+ toolBudget: agent.toolBudget,
67
+ memory: agent.memory,
68
+ };
69
+ }
70
+
71
+ export function agentDefinitionDigest(agent: AgentConfig): string {
72
+ return sha256(projectAgentDefinition(agent));
73
+ }
74
+
75
+ export interface LaunchBindingInput {
76
+ definitionDigest: string;
77
+ /** Caller task; runtime acceptance/output task annotations are explicitly outside the preflight-known subset. */
78
+ task?: string;
79
+ model?: string;
80
+ modelCandidates?: string[];
81
+ thinking?: string;
82
+ systemPrompt?: string | null;
83
+ systemPromptMode?: AgentConfig["systemPromptMode"];
84
+ inheritProjectContext: boolean;
85
+ inheritSkills: boolean;
86
+ skills?: string[];
87
+ tools?: string[];
88
+ extensions?: string[];
89
+ subagentOnlyExtensions?: string[];
90
+ mcpDirectTools?: string[];
91
+ outputPath?: string;
92
+ outputMode?: string;
93
+ structuredOutputSchema?: unknown;
94
+ }
95
+
96
+ /** Canonical projection of the resolved inputs handed to the child. */
97
+ export function projectLaunchBinding(input: LaunchBindingInput): Record<string, unknown> {
98
+ return {
99
+ version: LAUNCH_BINDING_PROJECTION_VERSION,
100
+ definitionDigest: input.definitionDigest,
101
+ taskDigest: input.task === undefined ? undefined : sha256(input.task),
102
+ // The ordered candidate set already contains each attempted model; keeping only
103
+ // this set makes retries correlate to the same preflight binding.
104
+ modelCandidates: input.modelCandidates,
105
+ thinking: input.thinking,
106
+ systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : sha256(input.systemPrompt),
107
+ systemPromptMode: input.systemPromptMode,
108
+ inheritProjectContext: input.inheritProjectContext,
109
+ inheritSkills: input.inheritSkills,
110
+ skills: input.skills,
111
+ tools: input.tools,
112
+ extensions: input.extensions,
113
+ subagentOnlyExtensions: input.subagentOnlyExtensions,
114
+ mcpDirectTools: input.mcpDirectTools,
115
+ outputPath: input.outputPath,
116
+ outputMode: input.outputMode,
117
+ structuredOutputSchema: input.structuredOutputSchema,
118
+ };
119
+ }
120
+
121
+ export function launchBindingDigest(input: LaunchBindingInput): string {
122
+ return sha256(projectLaunchBinding(input));
123
+ }
@@ -9,6 +9,7 @@ import type { AgentConfig } from "../agents/agents.ts";
9
9
  import type { FSWatcher } from "node:fs";
10
10
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
12
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
12
13
 
13
14
  // ============================================================================
14
15
  // Basic Types
@@ -302,7 +303,7 @@ export interface ExecutionProjection {
302
303
  }
303
304
 
304
305
  export interface ReviewProjection {
305
- status: "not-requested" | "no-blockers" | "blockers" | "needs-parent-decision";
306
+ status: "not-requested" | "review-required" | "reviewed" | "blockers";
306
307
  findings?: AcceptanceReviewResult["findings"];
307
308
  }
308
309
 
@@ -317,9 +318,69 @@ export interface EffectsProjection {
317
318
  fileMutation?: FileMutationEffect;
318
319
  }
319
320
 
320
- export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 2;
321
+ export const SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 3;
321
322
  export type SubagentLifecycleArtifactVersion = typeof SUBAGENT_LIFECYCLE_ARTIFACT_VERSION;
322
323
 
324
+ export type ProcessTerminalState = "pending" | "observed" | "unknown" | "not-started";
325
+ export type ProcessTerminalReason =
326
+ | "observer-unavailable"
327
+ | "runner-candidate-missing"
328
+ | "runner-instance-mismatch"
329
+ | "writer-close-unverified"
330
+ | "canonical-session-unavailable"
331
+ | "canonical-session-lease-active"
332
+ | "canonical-session-release-unverified"
333
+ | "proof-write-failed"
334
+ | "stale-repair";
335
+
336
+ export interface RunnerProcessInstanceExitV1 {
337
+ processInstanceId: string;
338
+ kind: "runner";
339
+ closeObservedAt: number;
340
+ exitCode: number | null;
341
+ signal: string | null;
342
+ }
343
+
344
+ export interface PiWriterProcessInstanceExitV1 {
345
+ processInstanceId: string;
346
+ kind: "pi-writer";
347
+ attempt: number;
348
+ closeObservedAt: number;
349
+ exitCode: number | null;
350
+ signal: string | null;
351
+ }
352
+
353
+ export type ProcessInstanceExitV1 = RunnerProcessInstanceExitV1 | PiWriterProcessInstanceExitV1;
354
+
355
+ export interface CanonicalSessionTerminalV1 {
356
+ canonicalSessionId: string;
357
+ leaseDisposition: "released" | "not-held";
358
+ freeAtObservation: true;
359
+ canonicalSessionLeaseReleased?: true;
360
+ }
361
+
362
+ interface ProcessTerminalBaseV1 {
363
+ version: 1;
364
+ runId: string;
365
+ childIndex?: number;
366
+ runnerProcessInstanceId: string;
367
+ resumeDisposition?: "resumable" | "non-resumable" | "unavailable";
368
+ }
369
+
370
+ export type ProcessTerminalV1 =
371
+ | (ProcessTerminalBaseV1 & { state: "pending" | "not-started" })
372
+ | (ProcessTerminalBaseV1 & {
373
+ state: "observed";
374
+ observedAt: number;
375
+ instances: ProcessInstanceExitV1[];
376
+ canonicalSession?: CanonicalSessionTerminalV1;
377
+ })
378
+ | (ProcessTerminalBaseV1 & {
379
+ state: "unknown";
380
+ reason: ProcessTerminalReason;
381
+ diagnostic?: string;
382
+ });
383
+
323
384
  export type SteeringActionState = "delivered" | "scheduled" | "pending" | "partial" | "recovered" | "failed";
324
385
  export type SteeringTargetState = "scheduled" | "routed" | "delivered" | "late" | "failed" | "recovered";
325
386
 
@@ -384,6 +445,7 @@ export interface SteeringNotice {
384
445
 
385
446
  export interface SteeringRecoveryDescriptor {
386
447
  version: 1;
448
+ launchContractDigest?: string;
387
449
  sourceRunId: string;
388
450
  agentContract?: AgentContract;
389
451
  agent: string;
@@ -415,6 +477,7 @@ export interface SteeringRecoveryDescriptor {
415
477
  initialToolBudget?: ResolvedToolBudget;
416
478
  maxSubagentDepth: number;
417
479
  maxOutput?: MaxOutputConfig;
480
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
418
481
  share: boolean;
419
482
  sessionDir?: string;
420
483
  artifactsDir?: string;
@@ -532,7 +595,7 @@ export interface ModelAttempt {
532
595
  usage?: Usage;
533
596
  }
534
597
 
535
- export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified" | "reviewed";
598
+ export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified";
536
599
 
537
600
  export type AcceptanceEvidenceKind =
538
601
  | "changed-files"
@@ -641,7 +704,7 @@ export interface AcceptanceVerifyResult {
641
704
  }
642
705
 
643
706
  export interface AcceptanceReviewResult {
644
- status: "no-blockers" | "blockers" | "needs-parent-decision";
707
+ status: "review-required" | "reviewed" | "blockers";
645
708
  findings: Array<{
646
709
  severity: "blocker" | "non-blocking";
647
710
  file?: string;
@@ -650,19 +713,24 @@ export interface AcceptanceReviewResult {
650
713
  }>;
651
714
  }
652
715
 
653
- export type AcceptanceLedgerStatus =
716
+ export type AcceptanceEvidenceStatus =
654
717
  | "pending"
655
718
  | "not-required"
656
719
  | "claimed"
657
720
  | "attested"
658
721
  | "checked"
659
722
  | "verified"
660
- | "reviewed"
661
- | "accepted"
662
723
  | "rejected";
663
724
 
725
+ export type AcceptanceLedgerStatus =
726
+ | AcceptanceEvidenceStatus
727
+ | "review-required"
728
+ | "reviewed"
729
+ | "accepted";
730
+
664
731
  export interface AcceptanceLedger {
665
732
  status: AcceptanceLedgerStatus;
733
+ evidenceStatus: AcceptanceEvidenceStatus;
666
734
  explicit: boolean;
667
735
  effectiveAcceptance: ResolvedAcceptanceConfig;
668
736
  inferredReason: string[];
@@ -733,6 +801,7 @@ export interface SingleResult {
733
801
  structuredOutputSchemaPath?: string;
734
802
  acceptance?: AcceptanceLedger;
735
803
  agentContract?: AgentContract;
804
+ launchContractDigest?: string;
736
805
  execution?: ExecutionProjection;
737
806
  review?: ReviewProjection;
738
807
  effects?: EffectsProjection;
@@ -740,6 +809,8 @@ export interface SingleResult {
740
809
  transcriptError?: string;
741
810
  children?: NestedRunSummary[];
742
811
  watchdog?: ChildWatchdogProgress;
812
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
813
+ capabilityAudit?: SubagentCapabilityAudit;
743
814
  }
744
815
 
745
816
  export interface SpawnBudgetGrant {
@@ -799,7 +870,13 @@ export interface Details {
799
870
  // Aggregated cost across all agents in the run
800
871
  totalCost?: CostSummary;
801
872
  spawnBudget?: SpawnBudgetSnapshot;
873
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
874
+ capabilityAudit?: SubagentCapabilityAudit;
802
875
  parallelHandoff?: ParallelHandoffReference;
876
+ lifecycleStatus?: {
877
+ processTerminal?: ProcessTerminalV1;
878
+ };
879
+ launchContractDigest?: string;
803
880
  }
804
881
 
805
882
  // ============================================================================
@@ -873,6 +950,9 @@ export interface NestedStepSummary {
873
950
  wrapUpRequested?: boolean;
874
951
  toolBudget?: ToolBudgetState;
875
952
  toolBudgetBlocked?: boolean;
953
+ processTerminal?: ProcessTerminalV1;
954
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
955
+ capabilityAudit?: SubagentCapabilityAudit;
876
956
  children?: NestedRunSummary[];
877
957
  }
878
958
 
@@ -888,6 +968,9 @@ export interface NestedRunSummary extends NestedRunAddress {
888
968
  controlInbox?: string;
889
969
  capabilityToken?: string;
890
970
  mode?: SubagentRunMode;
971
+ processTerminal?: ProcessTerminalV1;
972
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
973
+ capabilityAudit?: SubagentCapabilityAudit;
891
974
  state: NestedRunState;
892
975
  agent?: string;
893
976
  agents?: string[];
@@ -946,10 +1029,12 @@ export interface AsyncStartedEvent {
946
1029
  chainStepCount?: number;
947
1030
  parallelGroups?: AsyncParallelGroupStatus[];
948
1031
  workflowGraph?: WorkflowGraphSnapshot;
1032
+ launchContractDigest?: string;
949
1033
  timeoutMs?: number;
950
1034
  deadlineAt?: number;
951
1035
  turnBudget?: TurnBudgetState;
952
1036
  nestedRoute?: NestedRouteInfo;
1037
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
953
1038
  }
954
1039
 
955
1040
  export interface AsyncStatus {
@@ -987,6 +1072,10 @@ export interface AsyncStatus {
987
1072
  pendingAppends?: number;
988
1073
  parallelGroups?: AsyncParallelGroupStatus[];
989
1074
  workflowGraph?: WorkflowGraphSnapshot;
1075
+ processTerminal?: ProcessTerminalV1;
1076
+ launchContractDigest?: string;
1077
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1078
+ capabilityAudit?: SubagentCapabilityAudit;
990
1079
  steps?: Array<{
991
1080
  agent: string;
992
1081
  /** Resolved launch context for this child step. */
@@ -1035,10 +1124,14 @@ export interface AsyncStatus {
1035
1124
  structuredOutputSchemaPath?: string;
1036
1125
  acceptance?: AcceptanceLedger;
1037
1126
  agentContract?: AgentContract;
1127
+ launchContractDigest?: string;
1038
1128
  execution?: ExecutionProjection;
1039
1129
  review?: ReviewProjection;
1040
1130
  effects?: EffectsProjection;
1041
1131
  watchdog?: ChildWatchdogProgress;
1132
+ processTerminal?: ProcessTerminalV1;
1133
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1134
+ capabilityAudit?: SubagentCapabilityAudit;
1042
1135
  }>;
1043
1136
  sessionDir?: string;
1044
1137
  outputFile?: string;
@@ -1133,6 +1226,8 @@ export interface ForegroundResumeChild {
1133
1226
  execution?: ExecutionProjection;
1134
1227
  review?: ReviewProjection;
1135
1228
  effects?: EffectsProjection;
1229
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1230
+ capabilityAudit?: SubagentCapabilityAudit;
1136
1231
  updatedAt?: number;
1137
1232
  }
1138
1233
 
@@ -1252,6 +1347,7 @@ export const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
1252
1347
  export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
1253
1348
  export const SUBAGENT_ASYNC_STARTED_EVENT = "subagent:async-started";
1254
1349
  export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
1350
+ export const SUBAGENT_PROCESS_TERMINAL_EVENT = "subagent:process-terminal";
1255
1351
  export const SUBAGENT_FOREGROUND_COMPLETE_EVENT = "subagent:foreground-complete";
1256
1352
  export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
1257
1353
  export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
@@ -1299,6 +1395,7 @@ export interface RunSyncOptions {
1299
1395
  maxSubagentDepth?: number;
1300
1396
  /** Effective parent wait-tool setting propagated to the child runtime. */
1301
1397
  waitToolEnabled?: boolean;
1398
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1302
1399
  nestedRoute?: NestedRouteInfo;
1303
1400
  /** Override the agent's default model (format: "provider/id" or just "id") */
1304
1401
  modelOverride?: string;
@@ -412,6 +412,14 @@ export function compactForegroundDetails(details: Details): Details {
412
412
  };
413
413
  }
414
414
 
415
+ export function hasEmptyTerminalAssistantResponse(messages: Message[]): boolean {
416
+ const lastAssistant = messages.findLast((message) => message.role === "assistant");
417
+ return lastAssistant?.role === "assistant"
418
+ && Array.isArray(lastAssistant.content)
419
+ && lastAssistant.content.length === 0
420
+ && lastAssistant.usage.output === 0;
421
+ }
422
+
415
423
  /**
416
424
  * Detect errors in subagent execution from messages (only errors with no subsequent success)
417
425
  */
@@ -438,50 +446,17 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
438
446
  const toolName = "toolName" in msg && typeof msg.toolName === "string" ? msg.toolName : undefined;
439
447
  const isError = "isError" in msg && msg.isError === true;
440
448
 
441
- if (isError) {
442
- const text = msg.content.find((c) => c.type === "text");
443
- const details = text && "text" in text ? text.text : undefined;
444
- const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
445
- return {
446
- hasError: true,
447
- exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
448
- errorType: toolName || "tool",
449
- details: details?.slice(0, 200),
450
- };
451
- }
452
-
453
- if (toolName !== "bash") continue;
449
+ if (!isError) continue;
454
450
 
455
451
  const text = msg.content.find((c) => c.type === "text");
456
- if (!text || !("text" in text)) continue;
457
- const output = text.text;
458
-
459
- const exitMatch = output.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
460
- if (exitMatch) {
461
- const code = parseInt(exitMatch[1], 10);
462
- if (code !== 0) {
463
- return { hasError: true, exitCode: code, errorType: "bash", details: output.slice(0, 200) };
464
- }
465
- }
466
-
467
- // NOTE: These patterns can match legitimate output (grep results, logs,
468
- // testing). With the assistant-message check above, most false positives
469
- // are mitigated since the agent will have responded after routine errors.
470
- const fatalPatterns = [
471
- /command not found/i,
472
- /permission denied/i,
473
- /no such file or directory/i,
474
- /segmentation fault/i,
475
- /killed|terminated/i,
476
- /out of memory/i,
477
- /connection refused/i,
478
- /timeout/i,
479
- ];
480
- for (const pattern of fatalPatterns) {
481
- if (pattern.test(output)) {
482
- return { hasError: true, exitCode: 1, errorType: "bash", details: output.slice(0, 200) };
483
- }
484
- }
452
+ const details = text && "text" in text ? text.text : undefined;
453
+ const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
454
+ return {
455
+ hasError: true,
456
+ exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
457
+ errorType: toolName || "tool",
458
+ details: details?.slice(0, 200),
459
+ };
485
460
  }
486
461
 
487
462
  return { hasError: false };
@@ -491,7 +491,7 @@ export function toSubagentDelegationResponse(
491
491
  ...(child?.finalOutput ? { output: child.finalOutput } : {}),
492
492
  ...(child?.savedOutputPath ? { outputPath: child.savedOutputPath } : {}),
493
493
  ...(child?.sessionFile ? { sessionFile: child.sessionFile } : {}),
494
- ...(child?.acceptance ? { acceptance: { status: child.acceptance.status, explicit: child.acceptance.explicit } } : {}),
494
+ ...(child?.acceptance ? { acceptance: { status: child.acceptance.status, evidenceStatus: child.acceptance.evidenceStatus, explicit: child.acceptance.explicit } } : {}),
495
495
  ...(child?.review ? { review: child.review as SubagentDelegationReviewResult } : {}),
496
496
  ...(child?.effects ? { effects: child.effects as SubagentDelegationEffectsResult } : {}),
497
497
  ...(typeof child?.usage?.turns === "number" ? { turns: child.usage.turns } : {}),
@@ -1008,7 +1008,7 @@ function validateInlineAcceptanceInput(value: string, agent: string): void {
1008
1008
  const errors = validateAcceptanceInput(value, `acceptance for step '${agent}'`);
1009
1009
  if (errors.length > 0) throw new SlashParseError(errors[0]!);
1010
1010
  if (!INLINE_ACCEPTANCE_LEVELS.has(value)) {
1011
- throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none or verified acceptance contracts; reviewed is inferred-only.`);
1011
+ throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none, verified, or review requirements; reviewed is an achieved status, not an input level.`);
1012
1012
  }
1013
1013
  }
1014
1014