@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
package/src/inspect.ts CHANGED
@@ -13,26 +13,38 @@ import {
13
13
  type DelegationCwdPolicy,
14
14
  discoverAgents,
15
15
  } from "./agents.js";
16
+ import { projectCapabilityManifest } from "./capabilities.js";
16
17
  import { resolveConsultTools } from "./consult-policy.js";
18
+ import { buildContextSnapshot, type ContextMode } from "./context.js";
17
19
  import { renderInspectCall, renderInspectResult } from "./inspect-render.js";
20
+ import { DEFAULT_MAX_CONTEXT_BYTES } from "./limits.js";
21
+ import { resolvePiInvocation } from "./pi-invocation.js";
18
22
  import type { AgentRunInspectionDetail, AgentRunInspectionSummary } from "./registry.js";
19
23
  import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
20
24
  import {
25
+ inspectBlockingParallelLimitSettings,
21
26
  inspectCompletionDeliverySettings,
22
27
  inspectConsultResourceSettings,
23
28
  inspectCwdPolicySettings,
24
29
  inspectDelegationWorkflowSettings,
30
+ inspectStatefulLimitSettings,
31
+ inspectStatefulTransportSettings,
25
32
  inspectSubagentSettings,
26
33
  resolveDelegationWorkflow,
27
34
  } from "./settings.js";
28
35
  import type { StatefulSubagentRuntimeStatus } from "./stateful.js";
36
+ import type { WorkItemLedgerSnapshot } from "./work-item-ledger.js";
37
+ import { inspectSessionWorkflows } from "./work-item-persistence.js";
29
38
 
30
39
  const INSPECT_ACTIONS = [
31
40
  "list_agents",
32
41
  "get_agent",
33
42
  "list_runs",
34
43
  "get_run",
44
+ "list_workflows",
45
+ "get_workflow",
35
46
  "list_models",
47
+ "preview_context",
36
48
  "status",
37
49
  "diagnose",
38
50
  ] as const;
@@ -43,6 +55,10 @@ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
43
55
  });
44
56
 
45
57
  const LimitSchema = Type.Number({ minimum: 1, maximum: 100, multipleOf: 1 });
58
+ const ContextModeSchema = Type.Union([
59
+ StringEnum(["none", "all", "summary"] as const),
60
+ Type.Number({ minimum: 1, multipleOf: 1 }),
61
+ ]);
46
62
  const MAX_DETAILS_LIST_BYTES = 40 * 1024;
47
63
 
48
64
  export const SubagentInspectParams = Type.Object(
@@ -50,9 +66,12 @@ export const SubagentInspectParams = Type.Object(
50
66
  action: StringEnum(INSPECT_ACTIONS),
51
67
  agent: Type.Optional(Type.String({ minLength: 1 })),
52
68
  agentId: Type.Optional(Type.String({ minLength: 1 })),
69
+ workflowId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
53
70
  agentScope: Type.Optional(AgentScopeSchema),
54
71
  limit: Type.Optional(LimitSchema),
55
72
  includeClosed: Type.Optional(Type.Boolean({ default: false })),
73
+ context: Type.Optional(ContextModeSchema),
74
+ contextEntryIds: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
56
75
  },
57
76
  { additionalProperties: false },
58
77
  );
@@ -61,6 +80,7 @@ export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
61
80
 
62
81
  export interface SubagentInspectRuntime {
63
82
  getBlockingEnabled(): boolean;
83
+ getMaxParallelTasks(): number;
64
84
  getConsultResourcePolicy(): "project-context" | "none" | "all";
65
85
  getConsultationCwdPolicy(): ConsultationCwdPolicy;
66
86
  getDelegationCwdPolicy(): DelegationCwdPolicy;
@@ -79,7 +99,10 @@ type ValidatedInspectOperation =
79
99
  | { action: "get_agent"; agent: string; agentScope: AgentScope }
80
100
  | { action: "list_runs"; includeClosed: boolean; limit: number }
81
101
  | { action: "get_run"; agentId: string }
102
+ | { action: "list_workflows"; limit: number }
103
+ | { action: "get_workflow"; workflowId: string }
82
104
  | { action: "list_models"; limit: number }
105
+ | { action: "preview_context"; context: ContextMode; contextEntryIds?: string[] }
83
106
  | { action: "status" }
84
107
  | { action: "diagnose" };
85
108
 
@@ -88,7 +111,7 @@ export function registerSubagentInspect(pi: ExtensionAPI, runtime: SubagentInspe
88
111
  name: "subagent_inspect",
89
112
  label: "Inspect Subagents",
90
113
  description:
91
- "Inspect available subagent definitions, models, retained runs, runtime status, and diagnostics without changing subagent or workspace state. This tool never starts a child, sends or acknowledges messages, interrupts or closes runs, changes settings, or modifies files.",
114
+ "Inspect available subagent definitions, models, retained runs, persisted blocking workflows, runtime status, and diagnostics without changing subagent or workspace state. This tool never starts a child, sends or acknowledges messages, interrupts or closes runs, changes settings, or modifies files.",
92
115
  promptSnippet: "Inspect subagent metadata and runtime state without changing it",
93
116
  parameters: SubagentInspectParams,
94
117
  async execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<InspectToolResult> {
@@ -118,7 +141,10 @@ export function validateInspectParams(params: unknown): ValidatedInspectOperatio
118
141
  get_agent: ["action", "agent", "agentScope"],
119
142
  list_runs: ["action", "includeClosed", "limit"],
120
143
  get_run: ["action", "agentId"],
144
+ list_workflows: ["action", "limit"],
145
+ get_workflow: ["action", "workflowId"],
121
146
  list_models: ["action", "limit"],
147
+ preview_context: ["action", "context", "contextEntryIds"],
122
148
  status: ["action"],
123
149
  diagnose: ["action"],
124
150
  };
@@ -148,9 +174,24 @@ export function validateInspectParams(params: unknown): ValidatedInspectOperatio
148
174
  if (action === "get_run") {
149
175
  return { action, agentId: requiredString(values.agentId, action, "agentId") };
150
176
  }
177
+ if (action === "list_workflows") {
178
+ return { action, limit: optionalLimit(values.limit, 50) };
179
+ }
180
+ if (action === "get_workflow") {
181
+ return { action, workflowId: requiredString(values.workflowId, action, "workflowId") };
182
+ }
151
183
  if (action === "list_models") {
152
184
  return { action, limit: optionalLimit(values.limit, 50) };
153
185
  }
186
+ if (action === "preview_context") {
187
+ const context = optionalContextMode(values.context);
188
+ const contextEntryIds = optionalStringArray(values.contextEntryIds, "contextEntryIds");
189
+ return {
190
+ action,
191
+ context: values.context === undefined && contextEntryIds ? "all" : context,
192
+ ...(contextEntryIds ? { contextEntryIds } : {}),
193
+ };
194
+ }
154
195
  return { action };
155
196
  }
156
197
 
@@ -204,9 +245,59 @@ async function executeSubagentInspect(
204
245
  }
205
246
  return inspectResult({ action: operation.action, run: projectRun(run, ctx) });
206
247
  }
248
+ if (operation.action === "list_workflows" || operation.action === "get_workflow") {
249
+ const owner =
250
+ ctx.sessionManager.getSessionId?.() ??
251
+ ctx.sessionManager.getSessionFile?.() ??
252
+ `ephemeral:${ctx.cwd}`;
253
+ const inspected = inspectSessionWorkflows(owner, {
254
+ maxStoredWorkflows: operation.action === "list_workflows" ? operation.limit : 64,
255
+ });
256
+ if (operation.action === "list_workflows") {
257
+ const selected = boundedProjection(
258
+ inspected.workflows,
259
+ operation.limit,
260
+ projectWorkflowSummary,
261
+ );
262
+ return inspectResult({
263
+ action: operation.action,
264
+ workflows: selected.items,
265
+ returned: selected.items.length,
266
+ omitted: inspected.omitted + selected.omitted,
267
+ invalid: inspected.invalid,
268
+ });
269
+ }
270
+ const workflow = inspected.workflows.find(
271
+ (candidate) => candidate.workflowId === operation.workflowId,
272
+ );
273
+ if (!workflow) {
274
+ throw new Error(
275
+ `Unknown persisted workflow: ${boundedPrivateText(operation.workflowId, 256)}`,
276
+ );
277
+ }
278
+ return inspectResult({ action: operation.action, workflow: projectWorkflow(workflow) });
279
+ }
207
280
  if (operation.action === "list_models") {
208
281
  return inspectResult({ action: operation.action, ...projectModels(ctx, operation.limit) });
209
282
  }
283
+ if (operation.action === "preview_context") {
284
+ const snapshot = buildContextSnapshot(
285
+ ctx.sessionManager.getBranch(),
286
+ operation.context,
287
+ DEFAULT_MAX_CONTEXT_BYTES,
288
+ operation.contextEntryIds,
289
+ );
290
+ return inspectResult({
291
+ action: operation.action,
292
+ preview: {
293
+ mode: operation.context,
294
+ turns: snapshot.turns,
295
+ sourceCount: snapshot.sourceIds.length,
296
+ bytes: Buffer.byteLength(snapshot.text, "utf8"),
297
+ truncated: snapshot.truncated,
298
+ },
299
+ });
300
+ }
210
301
  if (operation.action === "status") {
211
302
  return inspectResult({ action: operation.action, status: projectStatus(runtime) });
212
303
  }
@@ -215,6 +306,8 @@ async function executeSubagentInspect(
215
306
  const userDiscovery = discoverAgents(ctx.cwd, "user", settings.settings);
216
307
  const modelCount = availableModelCount(ctx);
217
308
  const runtimeStatus = runtime.getRuntimeStatus();
309
+ const rpcCapability = inspectRpcCapability();
310
+ const inProcessCapability = await inspectInProcessCapability();
218
311
  const checks = [
219
312
  {
220
313
  name: "settings",
@@ -244,6 +337,20 @@ async function executeSubagentInspect(
244
337
  ? "Stateful runtime initialized."
245
338
  : "Stateful runtime not initialized.",
246
339
  },
340
+ {
341
+ name: "in-process-sdk",
342
+ status: inProcessCapability.error ? "fail" : "pass",
343
+ message: inProcessCapability.error
344
+ ? boundedPrivateText(inProcessCapability.error, 2 * 1024)
345
+ : "Required public Pi in-process session APIs are available.",
346
+ },
347
+ {
348
+ name: "rpc-cli",
349
+ status: rpcCapability.error ? "fail" : "pass",
350
+ message: rpcCapability.error
351
+ ? boundedPrivateText(rpcCapability.error, 2 * 1024)
352
+ : "The exact loaded Pi CLI is available for persistent RPC transport.",
353
+ },
247
354
  {
248
355
  name: "consultation",
249
356
  status: runtime.getBlockingEnabled() && modelCount > 0 ? "pass" : "fail",
@@ -280,6 +387,7 @@ function projectAgent(
280
387
  : safeDisplayPath(agent.filePath, ctx.cwd),
281
388
  model: agent.model ? boundedPrivateText(agent.model, 256) : undefined,
282
389
  thinkingLevel: agent.thinkingLevel,
390
+ capabilityManifest: projectCapabilityManifest(agent.capabilityManifest),
283
391
  ...(includeTools
284
392
  ? { tools, toolCount: agent.tools?.length }
285
393
  : { toolCount: agent.tools?.length }),
@@ -305,6 +413,67 @@ function projectRun(run: AgentRunInspectionDetail, ctx: ExtensionContext): Recor
305
413
  cwd: safeDisplayPath(run.cwd, ctx.cwd),
306
414
  workspaceMode: run.workspaceMode ?? "shared",
307
415
  thinkingLevel: run.thinkingLevel,
416
+ timeoutMs: run.timeoutMs,
417
+ currentTimeoutMs: run.currentTimeoutMs,
418
+ idleTimeoutMs: run.idleTimeoutMs,
419
+ currentIdleTimeoutMs: run.currentIdleTimeoutMs,
420
+ maxTurns: run.maxTurns,
421
+ currentMaxTurns: run.currentMaxTurns,
422
+ maxToolCalls: run.maxToolCalls,
423
+ currentMaxToolCalls: run.currentMaxToolCalls,
424
+ context: {
425
+ turns: run.contextTurns ?? 0,
426
+ sources: run.contextSources ?? 0,
427
+ bytes: run.contextBytes ?? 0,
428
+ truncated: run.contextTruncated === true,
429
+ },
430
+ contract: run.contract
431
+ ? {
432
+ version: run.contract.version,
433
+ level: run.contract.level,
434
+ taskId: boundedPrivateText(run.contract.taskId, 256),
435
+ enforcement: run.contract.enforcement,
436
+ dependencies: run.contract.dependencies.length,
437
+ acceptanceCriteria: run.contract.acceptanceCriteria.length,
438
+ requiredEvidence: run.contract.requiredEvidence.length,
439
+ }
440
+ : undefined,
441
+ resultFormat: run.resultFormat ?? "text",
442
+ structuredResult: run.structuredResult,
443
+ termination: run.termination,
444
+ outcome: run.outcome,
445
+ capabilityGrant: run.capabilityGrant
446
+ ? {
447
+ version: run.capabilityGrant.version,
448
+ id: run.capabilityGrant.id,
449
+ executionPlanId: run.capabilityGrant.executionPlanId,
450
+ taskGeneration: run.capabilityGrant.taskGeneration,
451
+ issuedAt: run.capabilityGrant.issuedAt,
452
+ expiresAt: run.capabilityGrant.expiresAt,
453
+ state: run.capabilityGrant.state,
454
+ revokedAt: run.capabilityGrant.revokedAt,
455
+ revocationReason: run.capabilityGrant.revocationReason,
456
+ }
457
+ : undefined,
458
+ executionPlan: run.executionPlan
459
+ ? {
460
+ ...run.executionPlan,
461
+ target: {
462
+ ...run.executionPlan.target,
463
+ cwd: safeDisplayPath(run.executionPlan.target.cwd, ctx.cwd),
464
+ trust: { ...run.executionPlan.target.trust, sourcePath: undefined },
465
+ },
466
+ }
467
+ : undefined,
468
+ semanticSnapshot: run.semanticSnapshot
469
+ ? {
470
+ version: run.semanticSnapshot.version,
471
+ digest: run.semanticSnapshot.digest,
472
+ components: { ...run.semanticSnapshot.components },
473
+ }
474
+ : undefined,
475
+ semanticCompatibility: run.semanticCompatibility,
476
+ telemetry: run.telemetry,
308
477
  currentTask: run.currentTask ? boundedPrivateText(run.currentTask, 2 * 1024) : undefined,
309
478
  error: run.error ? boundedPrivateText(run.error, 2 * 1024) : undefined,
310
479
  target: run.target
@@ -333,6 +502,77 @@ function projectRun(run: AgentRunInspectionDetail, ctx: ExtensionContext): Recor
333
502
  };
334
503
  }
335
504
 
505
+ function projectWorkflowSummary(workflow: WorkItemLedgerSnapshot): Record<string, unknown> {
506
+ return {
507
+ workflowId: boundedPrivateText(workflow.workflowId, 256),
508
+ generation: workflow.generation,
509
+ itemCount: workflow.items.length,
510
+ states: Object.fromEntries(
511
+ [...new Set(workflow.items.map((item) => item.state))]
512
+ .sort()
513
+ .map((state) => [state, workflow.items.filter((item) => item.state === state).length]),
514
+ ),
515
+ };
516
+ }
517
+
518
+ function projectWorkflow(workflow: WorkItemLedgerSnapshot): Record<string, unknown> {
519
+ const projected = boundedProjection(workflow.items, 64, (item) => ({
520
+ id: boundedPrivateText(item.id, 256),
521
+ state: item.state,
522
+ generation: item.generation,
523
+ taskGeneration: item.taskGeneration,
524
+ dependencies: item.dependencies.map((value) => boundedPrivateText(value, 256)),
525
+ assignedAgentId: item.assignedAgentId
526
+ ? boundedPrivateText(item.assignedAgentId, 256)
527
+ : undefined,
528
+ acceptedExecutionPlanId: item.acceptedExecutionPlanId,
529
+ artifacts: item.artifacts.map((artifact) => ({
530
+ id: boundedPrivateText(artifact.id, 256),
531
+ kind: boundedPrivateText(artifact.kind, 256),
532
+ version: boundedPrivateText(artifact.version, 256),
533
+ producerTaskId: artifact.producerTaskId
534
+ ? boundedPrivateText(artifact.producerTaskId, 256)
535
+ : undefined,
536
+ generation: artifact.generation,
537
+ verified: artifact.verified,
538
+ })),
539
+ verificationAccepted: item.verificationAccepted,
540
+ stagedTreeIdentity: item.stagedTreeIdentity
541
+ ? {
542
+ version: item.stagedTreeIdentity.version,
543
+ kind: item.stagedTreeIdentity.kind,
544
+ digest: item.stagedTreeIdentity.digest,
545
+ }
546
+ : undefined,
547
+ verificationReceipt: item.verificationReceipt
548
+ ? {
549
+ version: item.verificationReceipt.version,
550
+ decision: item.verificationReceipt.decision,
551
+ targetTaskId: boundedPrivateText(item.verificationReceipt.targetTaskId, 256),
552
+ targetTaskGeneration: item.verificationReceipt.targetTaskGeneration,
553
+ targetExecutionPlanId: item.verificationReceipt.targetExecutionPlanId,
554
+ verifierTaskId: boundedPrivateText(item.verificationReceipt.verifierTaskId, 256),
555
+ verifierTaskGeneration: item.verificationReceipt.verifierTaskGeneration,
556
+ verifierExecutionPlanId: item.verificationReceipt.verifierExecutionPlanId,
557
+ treeIdentity: item.verificationReceipt.treeIdentity,
558
+ summary: boundedPrivateText(item.verificationReceipt.summary, 8 * 1024),
559
+ evidenceCount: item.verificationReceipt.evidence.length,
560
+ limitationCount: item.verificationReceipt.limitations.length,
561
+ createdAt: item.verificationReceipt.createdAt,
562
+ truncated: item.verificationReceipt.truncated,
563
+ }
564
+ : undefined,
565
+ outcomeReason: item.outcomeReason
566
+ ? boundedPrivateText(item.outcomeReason, 2 * 1024)
567
+ : undefined,
568
+ }));
569
+ return {
570
+ ...projectWorkflowSummary(workflow),
571
+ items: projected.items,
572
+ omittedItems: projected.omitted,
573
+ };
574
+ }
575
+
336
576
  function projectModels(ctx: ExtensionContext, limit: number): Record<string, unknown> {
337
577
  const scoped = ctx.scopedModels ?? [];
338
578
  const candidates =
@@ -365,13 +605,34 @@ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown>
365
605
  const resources = inspectConsultResourceSettings();
366
606
  const cwdPolicy = inspectCwdPolicySettings();
367
607
  const completion = inspectCompletionDeliverySettings();
608
+ const parallelLimit = inspectBlockingParallelLimitSettings();
609
+ const detachedLimits = inspectStatefulLimitSettings();
610
+ const transport = inspectStatefulTransportSettings();
611
+ const configuredDetachedLimits = detachedLimits.values
612
+ ? Object.fromEntries(
613
+ Object.entries(detachedLimits.values).map(([field, snapshot]) => [field, snapshot.value]),
614
+ )
615
+ : undefined;
616
+ const configuredDetachedLimitSources = detachedLimits.values
617
+ ? Object.fromEntries(
618
+ Object.entries(detachedLimits.values).map(([field, snapshot]) => [field, snapshot.source]),
619
+ )
620
+ : undefined;
368
621
  return {
369
622
  workflow,
370
623
  configuredWorkflow: configured.value,
371
624
  configuredWorkflowSource: configured.source,
372
625
  stateful,
626
+ statefulLimits: stateful.limits,
627
+ configuredTransport: transport.value,
628
+ configuredTransportSource: transport.source,
629
+ configuredStatefulLimits: configuredDetachedLimits,
630
+ configuredStatefulLimitSources: configuredDetachedLimitSources,
373
631
  configuredCompletionDelivery: completion.value,
374
632
  configuredCompletionDeliverySource: completion.source,
633
+ maxParallelTasks: runtime.getMaxParallelTasks(),
634
+ configuredMaxParallelTasks: parallelLimit.value,
635
+ configuredMaxParallelTasksSource: parallelLimit.source,
375
636
  consultResources: runtime.getConsultResourcePolicy(),
376
637
  consultationCwdPolicy: runtime.getConsultationCwdPolicy(),
377
638
  configuredConsultationCwdPolicy: cwdPolicy.consultation.value,
@@ -383,15 +644,54 @@ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown>
383
644
  consultResourcesSource: resources.source,
384
645
  settingsPath: safeDisplayPath(resources.path, process.cwd()),
385
646
  settingsError:
386
- configured.error || resources.error || cwdPolicy.error || completion.error
647
+ configured.error ||
648
+ resources.error ||
649
+ cwdPolicy.error ||
650
+ completion.error ||
651
+ parallelLimit.error ||
652
+ detachedLimits.error ||
653
+ transport.error
387
654
  ? boundedPrivateText(
388
- configured.error ?? resources.error ?? cwdPolicy.error ?? completion.error ?? "",
655
+ configured.error ??
656
+ resources.error ??
657
+ cwdPolicy.error ??
658
+ completion.error ??
659
+ parallelLimit.error ??
660
+ detachedLimits.error ??
661
+ transport.error ??
662
+ "",
389
663
  2 * 1024,
390
664
  )
391
665
  : undefined,
392
666
  };
393
667
  }
394
668
 
669
+ async function inspectInProcessCapability(): Promise<{ error?: string }> {
670
+ try {
671
+ const moduleSpecifier = "@earendil-works/pi-coding-agent";
672
+ const core = await import(moduleSpecifier);
673
+ for (const name of [
674
+ "createAgentSessionServices",
675
+ "createAgentSessionFromServices",
676
+ "resolveCliModel",
677
+ ] as const) {
678
+ if (typeof core[name] !== "function") return { error: `Pi core does not export ${name}()` };
679
+ }
680
+ return {};
681
+ } catch (error) {
682
+ return { error: error instanceof Error ? error.message : String(error) };
683
+ }
684
+ }
685
+
686
+ function inspectRpcCapability(): { error?: string } {
687
+ try {
688
+ resolvePiInvocation(["--mode", "rpc", "--no-session"]);
689
+ return {};
690
+ } catch (error) {
691
+ return { error: error instanceof Error ? error.message : String(error) };
692
+ }
693
+ }
694
+
395
695
  function availableModelCount(ctx: ExtensionContext): number {
396
696
  return (ctx.scopedModels?.length ?? 0) > 0
397
697
  ? ctx.scopedModels.length
@@ -450,6 +750,24 @@ function requiredString(value: unknown, action: string, field: string): string {
450
750
  return value;
451
751
  }
452
752
 
753
+ function optionalContextMode(value: unknown): ContextMode {
754
+ if (value === undefined) return "none";
755
+ if (value === "none" || value === "all" || value === "summary") return value;
756
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) return value;
757
+ throw new Error("subagent_inspect context must be none, all, summary, or a positive integer");
758
+ }
759
+
760
+ function optionalStringArray(value: unknown, field: string): string[] | undefined {
761
+ if (value === undefined) return undefined;
762
+ if (
763
+ !Array.isArray(value) ||
764
+ !value.every((item) => typeof item === "string" && item.length > 0)
765
+ ) {
766
+ throw new Error(`subagent_inspect ${field} must be an array of non-empty strings`);
767
+ }
768
+ return [...value];
769
+ }
770
+
453
771
  function optionalLimit(value: unknown, defaultValue: number): number {
454
772
  if (value === undefined) return defaultValue;
455
773
  if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 100) {
@@ -0,0 +1,98 @@
1
+ import * as path from "node:path";
2
+
3
+ export interface ManagedIntegrationExpectation {
4
+ taskId: string;
5
+ taskGeneration: number;
6
+ baseRepositoryGeneration: string;
7
+ dependencyVersions: Record<string, string>;
8
+ readSetVersions: Record<string, string>;
9
+ executionPlanId: string;
10
+ allowedScopes: string[];
11
+ patchDigest: string;
12
+ requiredEvidence: string[];
13
+ }
14
+
15
+ export interface ManagedIntegrationCandidate extends ManagedIntegrationExpectation {
16
+ changedPaths: string[];
17
+ evidence: Record<string, string>;
18
+ verifier: {
19
+ freshContext: boolean;
20
+ exactIntegratedTree: boolean;
21
+ status: "accepted" | "rework" | "rejected";
22
+ };
23
+ }
24
+
25
+ export interface ManagedIntegrationAcceptance {
26
+ status: "accepted";
27
+ taskId: string;
28
+ taskGeneration: number;
29
+ patchDigest: string;
30
+ executionPlanId: string;
31
+ }
32
+
33
+ export function verifyManagedIntegration(
34
+ expected: ManagedIntegrationExpectation,
35
+ candidate: ManagedIntegrationCandidate,
36
+ ): ManagedIntegrationAcceptance {
37
+ if (
38
+ candidate.taskId !== expected.taskId ||
39
+ candidate.taskGeneration !== expected.taskGeneration
40
+ ) {
41
+ throw new Error("Managed integration rejected stale task generation");
42
+ }
43
+ if (candidate.baseRepositoryGeneration !== expected.baseRepositoryGeneration) {
44
+ throw new Error("Managed integration rejected stale base repository generation");
45
+ }
46
+ if (candidate.executionPlanId !== expected.executionPlanId) {
47
+ throw new Error("Managed integration rejected stale execution plan identity");
48
+ }
49
+ if (!sameRecord(candidate.dependencyVersions, expected.dependencyVersions)) {
50
+ throw new Error("Managed integration rejected stale dependency versions");
51
+ }
52
+ if (!sameRecord(candidate.readSetVersions, expected.readSetVersions)) {
53
+ throw new Error("Managed integration rejected stale read-set versions");
54
+ }
55
+ if (candidate.patchDigest !== expected.patchDigest) {
56
+ throw new Error("Managed integration rejected patch digest mismatch");
57
+ }
58
+ const allowedScopes = expected.allowedScopes.map(normalizedPath);
59
+ if (
60
+ !candidate.changedPaths.every((changedPath) => {
61
+ const normalizedChangedPath = normalizedPath(changedPath);
62
+ return allowedScopes.some(
63
+ (scope) =>
64
+ normalizedChangedPath === scope ||
65
+ normalizedChangedPath.startsWith(`${scope}${path.sep}`),
66
+ );
67
+ })
68
+ ) {
69
+ throw new Error("Managed integration rejected a path outside the accepted scope");
70
+ }
71
+ if (expected.requiredEvidence.some((id) => !candidate.evidence[id])) {
72
+ throw new Error("Managed integration rejected missing required evidence");
73
+ }
74
+ if (!candidate.verifier.freshContext || !candidate.verifier.exactIntegratedTree) {
75
+ throw new Error("Managed integration requires a fresh verifier on the exact integrated tree");
76
+ }
77
+ if (candidate.verifier.status !== "accepted") {
78
+ throw new Error(`Managed integration verifier returned ${candidate.verifier.status}`);
79
+ }
80
+ return {
81
+ status: "accepted",
82
+ taskId: expected.taskId,
83
+ taskGeneration: expected.taskGeneration,
84
+ patchDigest: expected.patchDigest,
85
+ executionPlanId: expected.executionPlanId,
86
+ };
87
+ }
88
+
89
+ function normalizedPath(value: string): string {
90
+ if (!value || value.includes("\0")) throw new Error("Managed integration scope is invalid");
91
+ return path.resolve(path.sep, value.replaceAll("\\", path.sep));
92
+ }
93
+
94
+ function sameRecord(left: Record<string, string>, right: Record<string, string>): boolean {
95
+ const leftEntries = Object.entries(left).sort(([a], [b]) => a.localeCompare(b));
96
+ const rightEntries = Object.entries(right).sort(([a], [b]) => a.localeCompare(b));
97
+ return JSON.stringify(leftEntries) === JSON.stringify(rightEntries);
98
+ }
package/src/limits.ts CHANGED
@@ -4,6 +4,9 @@ export const DEFAULT_MAX_OUTPUT_BYTES = DEFAULT_MAX_BYTES;
4
4
  export const DEFAULT_MAX_STDERR_BYTES = 16 * 1024;
5
5
  export const DEFAULT_MAX_CONTEXT_BYTES = DEFAULT_MAX_BYTES;
6
6
  export const MAX_SUBAGENT_TIMEOUT_MS = 2_147_483_647;
7
+ export const DEFAULT_MAX_PARALLEL_TASKS = 8;
8
+ export const MAX_CONFIGURABLE_PARALLEL_TASKS = 64;
9
+ export const MAX_BLOCKING_PARALLEL_CONCURRENCY = 4;
7
10
  export const DEFAULT_MAX_MESSAGES = 200;
8
11
  export const TRUNCATION_MARKER = "\n… [truncated by pi-subagents]";
9
12
  export const TAIL_TRUNCATION_MARKER = "… [truncated by pi-subagents]\n";
@@ -0,0 +1,109 @@
1
+ import type { SingleResult } from "./runner.js";
2
+ import type { WorkItemLedgerSnapshot } from "./work-item-ledger.js";
3
+
4
+ export interface OrchestrationMetrics {
5
+ workItems: number;
6
+ completed: number;
7
+ failedOrBlocked: number;
8
+ invalidated: number;
9
+ requiredTransfers: number;
10
+ resolvedTransfers: number;
11
+ transferCoverage: number;
12
+ attempts: number;
13
+ hedgedTasks: number;
14
+ requestedTools: number;
15
+ effectiveRequestedTools: number;
16
+ permissionPrecision: number;
17
+ workerReportedVerification: number;
18
+ executorAcceptedVerification: number;
19
+ verificationRework: number;
20
+ verificationRejected: number;
21
+ verificationInvalid: number;
22
+ verificationTreeMismatch: number;
23
+ panelValidReviews?: number;
24
+ panelFailedReviews?: number;
25
+ panelBlockingObjections?: number;
26
+ panelDissent?: number;
27
+ panelSynthesisState?: string;
28
+ }
29
+
30
+ export function calculateOrchestrationMetrics(
31
+ workflow: WorkItemLedgerSnapshot | undefined,
32
+ results: SingleResult[],
33
+ panel?: {
34
+ validReviewCount: number;
35
+ failedReviewCount: number;
36
+ blockingObjectionCount: number;
37
+ dissentCount: number;
38
+ state: string;
39
+ },
40
+ ): OrchestrationMetrics {
41
+ const items = workflow?.items ?? [];
42
+ const requiredTransfers = items.reduce((sum, item) => sum + item.inputArtifacts.length, 0);
43
+ const resolvedTransfers = items.reduce(
44
+ (sum, item) => sum + Object.keys(item.inputArtifactVersions).length,
45
+ 0,
46
+ );
47
+ const requestedTools = results.reduce(
48
+ (sum, result) => sum + (result.executionPlan?.requestedTools.length ?? 0),
49
+ 0,
50
+ );
51
+ const effectiveRequestedTools = results.reduce((sum, result) => {
52
+ const plan = result.executionPlan;
53
+ if (!plan) return sum;
54
+ return (
55
+ sum +
56
+ plan.requestedTools.filter((tool) => plan.effectiveTools?.includes(tool) === true).length
57
+ );
58
+ }, 0);
59
+ return {
60
+ workItems: items.length,
61
+ completed: items.filter((item) => item.state === "completed").length,
62
+ failedOrBlocked: items.filter((item) =>
63
+ ["failed", "blocked", "needs-input", "interrupted"].includes(item.state),
64
+ ).length,
65
+ invalidated: items.filter((item) => ["stale", "invalidated"].includes(item.state)).length,
66
+ requiredTransfers,
67
+ resolvedTransfers,
68
+ transferCoverage: requiredTransfers === 0 ? 1 : resolvedTransfers / requiredTransfers,
69
+ attempts: results.reduce((sum, result) => sum + (result.attemptCount ?? 1), 0),
70
+ hedgedTasks: results.filter((result) => result.hedged).length,
71
+ requestedTools,
72
+ effectiveRequestedTools,
73
+ permissionPrecision: requestedTools === 0 ? 1 : effectiveRequestedTools / requestedTools,
74
+ workerReportedVerification: results.filter(
75
+ (result, index) =>
76
+ !items[index]?.verifierFor &&
77
+ result.structuredResult?.version === "pi-subagents:result:v2" &&
78
+ result.structuredResult.verification.some(
79
+ (verification) => verification.status === "passed",
80
+ ),
81
+ ).length,
82
+ executorAcceptedVerification: items.filter((item) => item.verificationAccepted).length,
83
+ verificationRework: items.filter(
84
+ (item) => !item.verifierFor && item.verificationReceipt?.decision === "rework",
85
+ ).length,
86
+ verificationRejected: items.filter(
87
+ (item) => !item.verifierFor && item.verificationReceipt?.decision === "reject",
88
+ ).length,
89
+ verificationInvalid: items.filter(
90
+ (item) => !item.verifierFor && item.outcomeReason === "verification-receipt-invalid",
91
+ ).length,
92
+ verificationTreeMismatch: items.filter(
93
+ (item) =>
94
+ !item.verifierFor &&
95
+ ["verification-tree-mismatch", "verification-tree-unavailable"].includes(
96
+ item.outcomeReason ?? "",
97
+ ),
98
+ ).length,
99
+ ...(panel
100
+ ? {
101
+ panelValidReviews: panel.validReviewCount,
102
+ panelFailedReviews: panel.failedReviewCount,
103
+ panelBlockingObjections: panel.blockingObjectionCount,
104
+ panelDissent: panel.dissentCount,
105
+ panelSynthesisState: panel.state,
106
+ }
107
+ : {}),
108
+ };
109
+ }