@narumitw/pi-subagents 0.49.2 → 0.51.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 (81) hide show
  1. package/README.md +313 -53
  2. package/package.json +11 -8
  3. package/src/adaptive-scheduler.ts +196 -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 +848 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +296 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +78 -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 +772 -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 +172 -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 +17 -0
  77. package/src/work-item-ledger.ts +682 -0
  78. package/src/work-item-persistence.ts +218 -0
  79. package/src/workflow-planning.ts +150 -0
  80. package/src/workflow-ui.ts +61 -0
  81. 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,52 @@ 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
+ outcomeReason: item.outcomeReason
541
+ ? boundedPrivateText(item.outcomeReason, 2 * 1024)
542
+ : undefined,
543
+ }));
544
+ return {
545
+ ...projectWorkflowSummary(workflow),
546
+ items: projected.items,
547
+ omittedItems: projected.omitted,
548
+ };
549
+ }
550
+
336
551
  function projectModels(ctx: ExtensionContext, limit: number): Record<string, unknown> {
337
552
  const scoped = ctx.scopedModels ?? [];
338
553
  const candidates =
@@ -365,13 +580,34 @@ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown>
365
580
  const resources = inspectConsultResourceSettings();
366
581
  const cwdPolicy = inspectCwdPolicySettings();
367
582
  const completion = inspectCompletionDeliverySettings();
583
+ const parallelLimit = inspectBlockingParallelLimitSettings();
584
+ const detachedLimits = inspectStatefulLimitSettings();
585
+ const transport = inspectStatefulTransportSettings();
586
+ const configuredDetachedLimits = detachedLimits.values
587
+ ? Object.fromEntries(
588
+ Object.entries(detachedLimits.values).map(([field, snapshot]) => [field, snapshot.value]),
589
+ )
590
+ : undefined;
591
+ const configuredDetachedLimitSources = detachedLimits.values
592
+ ? Object.fromEntries(
593
+ Object.entries(detachedLimits.values).map(([field, snapshot]) => [field, snapshot.source]),
594
+ )
595
+ : undefined;
368
596
  return {
369
597
  workflow,
370
598
  configuredWorkflow: configured.value,
371
599
  configuredWorkflowSource: configured.source,
372
600
  stateful,
601
+ statefulLimits: stateful.limits,
602
+ configuredTransport: transport.value,
603
+ configuredTransportSource: transport.source,
604
+ configuredStatefulLimits: configuredDetachedLimits,
605
+ configuredStatefulLimitSources: configuredDetachedLimitSources,
373
606
  configuredCompletionDelivery: completion.value,
374
607
  configuredCompletionDeliverySource: completion.source,
608
+ maxParallelTasks: runtime.getMaxParallelTasks(),
609
+ configuredMaxParallelTasks: parallelLimit.value,
610
+ configuredMaxParallelTasksSource: parallelLimit.source,
375
611
  consultResources: runtime.getConsultResourcePolicy(),
376
612
  consultationCwdPolicy: runtime.getConsultationCwdPolicy(),
377
613
  configuredConsultationCwdPolicy: cwdPolicy.consultation.value,
@@ -383,15 +619,54 @@ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown>
383
619
  consultResourcesSource: resources.source,
384
620
  settingsPath: safeDisplayPath(resources.path, process.cwd()),
385
621
  settingsError:
386
- configured.error || resources.error || cwdPolicy.error || completion.error
622
+ configured.error ||
623
+ resources.error ||
624
+ cwdPolicy.error ||
625
+ completion.error ||
626
+ parallelLimit.error ||
627
+ detachedLimits.error ||
628
+ transport.error
387
629
  ? boundedPrivateText(
388
- configured.error ?? resources.error ?? cwdPolicy.error ?? completion.error ?? "",
630
+ configured.error ??
631
+ resources.error ??
632
+ cwdPolicy.error ??
633
+ completion.error ??
634
+ parallelLimit.error ??
635
+ detachedLimits.error ??
636
+ transport.error ??
637
+ "",
389
638
  2 * 1024,
390
639
  )
391
640
  : undefined,
392
641
  };
393
642
  }
394
643
 
644
+ async function inspectInProcessCapability(): Promise<{ error?: string }> {
645
+ try {
646
+ const moduleSpecifier = "@earendil-works/pi-coding-agent";
647
+ const core = await import(moduleSpecifier);
648
+ for (const name of [
649
+ "createAgentSessionServices",
650
+ "createAgentSessionFromServices",
651
+ "resolveCliModel",
652
+ ] as const) {
653
+ if (typeof core[name] !== "function") return { error: `Pi core does not export ${name}()` };
654
+ }
655
+ return {};
656
+ } catch (error) {
657
+ return { error: error instanceof Error ? error.message : String(error) };
658
+ }
659
+ }
660
+
661
+ function inspectRpcCapability(): { error?: string } {
662
+ try {
663
+ resolvePiInvocation(["--mode", "rpc", "--no-session"]);
664
+ return {};
665
+ } catch (error) {
666
+ return { error: error instanceof Error ? error.message : String(error) };
667
+ }
668
+ }
669
+
395
670
  function availableModelCount(ctx: ExtensionContext): number {
396
671
  return (ctx.scopedModels?.length ?? 0) > 0
397
672
  ? ctx.scopedModels.length
@@ -450,6 +725,24 @@ function requiredString(value: unknown, action: string, field: string): string {
450
725
  return value;
451
726
  }
452
727
 
728
+ function optionalContextMode(value: unknown): ContextMode {
729
+ if (value === undefined) return "none";
730
+ if (value === "none" || value === "all" || value === "summary") return value;
731
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) return value;
732
+ throw new Error("subagent_inspect context must be none, all, summary, or a positive integer");
733
+ }
734
+
735
+ function optionalStringArray(value: unknown, field: string): string[] | undefined {
736
+ if (value === undefined) return undefined;
737
+ if (
738
+ !Array.isArray(value) ||
739
+ !value.every((item) => typeof item === "string" && item.length > 0)
740
+ ) {
741
+ throw new Error(`subagent_inspect ${field} must be an array of non-empty strings`);
742
+ }
743
+ return [...value];
744
+ }
745
+
453
746
  function optionalLimit(value: unknown, defaultValue: number): number {
454
747
  if (value === undefined) return defaultValue;
455
748
  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,78 @@
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
+ panelValidReviews?: number;
18
+ panelFailedReviews?: number;
19
+ panelBlockingObjections?: number;
20
+ panelDissent?: number;
21
+ panelSynthesisState?: string;
22
+ }
23
+
24
+ export function calculateOrchestrationMetrics(
25
+ workflow: WorkItemLedgerSnapshot | undefined,
26
+ results: SingleResult[],
27
+ panel?: {
28
+ validReviewCount: number;
29
+ failedReviewCount: number;
30
+ blockingObjectionCount: number;
31
+ dissentCount: number;
32
+ state: string;
33
+ },
34
+ ): OrchestrationMetrics {
35
+ const items = workflow?.items ?? [];
36
+ const requiredTransfers = items.reduce((sum, item) => sum + item.inputArtifacts.length, 0);
37
+ const resolvedTransfers = items.reduce(
38
+ (sum, item) => sum + Object.keys(item.inputArtifactVersions).length,
39
+ 0,
40
+ );
41
+ const requestedTools = results.reduce(
42
+ (sum, result) => sum + (result.executionPlan?.requestedTools.length ?? 0),
43
+ 0,
44
+ );
45
+ const effectiveRequestedTools = results.reduce((sum, result) => {
46
+ const plan = result.executionPlan;
47
+ if (!plan) return sum;
48
+ return (
49
+ sum +
50
+ plan.requestedTools.filter((tool) => plan.effectiveTools?.includes(tool) === true).length
51
+ );
52
+ }, 0);
53
+ return {
54
+ workItems: items.length,
55
+ completed: items.filter((item) => item.state === "completed").length,
56
+ failedOrBlocked: items.filter((item) =>
57
+ ["failed", "blocked", "needs-input", "interrupted"].includes(item.state),
58
+ ).length,
59
+ invalidated: items.filter((item) => ["stale", "invalidated"].includes(item.state)).length,
60
+ requiredTransfers,
61
+ resolvedTransfers,
62
+ transferCoverage: requiredTransfers === 0 ? 1 : resolvedTransfers / requiredTransfers,
63
+ attempts: results.reduce((sum, result) => sum + (result.attemptCount ?? 1), 0),
64
+ hedgedTasks: results.filter((result) => result.hedged).length,
65
+ requestedTools,
66
+ effectiveRequestedTools,
67
+ permissionPrecision: requestedTools === 0 ? 1 : effectiveRequestedTools / requestedTools,
68
+ ...(panel
69
+ ? {
70
+ panelValidReviews: panel.validReviewCount,
71
+ panelFailedReviews: panel.failedReviewCount,
72
+ panelBlockingObjections: panel.blockingObjectionCount,
73
+ panelDissent: panel.dissentCount,
74
+ panelSynthesisState: panel.state,
75
+ }
76
+ : {}),
77
+ };
78
+ }