@narumitw/pi-subagents 1.0.1 → 2.0.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 (55) hide show
  1. package/README.md +198 -188
  2. package/package.json +8 -8
  3. package/src/agents/built-ins.ts +13 -66
  4. package/src/agents/catalog.ts +19 -2
  5. package/src/agents/discovery.ts +31 -15
  6. package/src/auto-transport.ts +7 -1
  7. package/src/child-peer-bridge.ts +124 -0
  8. package/src/child-peer-tools.ts +132 -0
  9. package/src/completion-delivery.ts +19 -5
  10. package/src/completion-render.ts +189 -0
  11. package/src/completion-routing.ts +24 -0
  12. package/src/config-registration.ts +29 -4
  13. package/src/config-ui.ts +11 -17
  14. package/src/consult-registration.ts +3 -2
  15. package/src/consult-render.ts +1 -1
  16. package/src/create-stateful-transport.ts +15 -2
  17. package/src/execution-ui.ts +0 -72
  18. package/src/in-process-transport.ts +39 -7
  19. package/src/inspect-tool.ts +3 -1
  20. package/src/panel-planning.ts +2 -2
  21. package/src/panel-presets.ts +3 -0
  22. package/src/params.ts +1 -1
  23. package/src/peer-communication.ts +352 -0
  24. package/src/peer-transport.ts +49 -0
  25. package/src/persistence.ts +26 -1
  26. package/src/pi-args.ts +2 -0
  27. package/src/registry-types.ts +7 -0
  28. package/src/registry.ts +240 -41
  29. package/src/render.ts +2 -41
  30. package/src/result-contract.ts +20 -5
  31. package/src/rpc-transport.ts +56 -26
  32. package/src/runner.ts +13 -1
  33. package/src/settings.ts +4 -1
  34. package/src/spawn-idempotency.ts +2 -0
  35. package/src/stateful-agent-view.ts +3 -1
  36. package/src/stateful-guidance.ts +11 -11
  37. package/src/stateful-safety.ts +0 -45
  38. package/src/stateful-tool-params.ts +11 -3
  39. package/src/stateful.ts +359 -136
  40. package/src/subagents.ts +7 -9
  41. package/src/subprocess-transport.ts +49 -28
  42. package/src/task-path.ts +65 -0
  43. package/src/transport-ui.ts +0 -6
  44. package/src/transport.ts +2 -1
  45. package/src/usage-format.ts +42 -0
  46. package/src/workflow-ui.ts +4 -4
  47. package/src/automation-contract.ts +0 -709
  48. package/src/automation-planner.ts +0 -65
  49. package/src/automation-registration.ts +0 -137
  50. package/src/automation-tool.ts +0 -40
  51. package/src/automation.ts +0 -435
  52. package/src/execution-profiles.ts +0 -95
  53. package/src/workflow-plan-compiler.ts +0 -618
  54. package/src/workflow-plan-patch.ts +0 -636
  55. package/src/workflow-planning-benchmark.ts +0 -95
package/src/subagents.ts CHANGED
@@ -21,11 +21,8 @@ import type {
21
21
  DelegationCwdPolicy,
22
22
  SubagentSettings,
23
23
  } from "./agents/types.js";
24
- import {
25
- type AutomationRegistrationDependencies,
26
- registerSubagentAutomation,
27
- } from "./automation-registration.js";
28
24
  import { cachedModuleLoader, throwIfAborted } from "./cached-module-loader.js";
25
+ import { renderCompletionMessage, SUBAGENT_COMPLETION_MESSAGE_TYPE } from "./completion-render.js";
29
26
  import {
30
27
  type ConfigRegistrationDependencies,
31
28
  registerSubagentConfigCommand,
@@ -60,13 +57,13 @@ type BlockingExecutionModule = Pick<typeof import("./execution.js"), "executeSub
60
57
  export interface SubagentsDependencies {
61
58
  loadBlockingExecution?: () => Promise<BlockingExecutionModule>;
62
59
  loadStatefulTransport?: () => Promise<SubagentTransport>;
63
- automation?: AutomationRegistrationDependencies;
64
60
  config?: ConfigRegistrationDependencies;
65
61
  consult?: ConsultRegistrationDependencies;
66
62
  inspect?: InspectRegistrationDependencies;
67
63
  }
68
64
 
69
65
  export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies = {}) {
66
+ pi.registerMessageRenderer(SUBAGENT_COMPLETION_MESSAGE_TYPE, renderCompletionMessage);
70
67
  const loadBlockingExecution = cachedModuleLoader(
71
68
  dependencies.loadBlockingExecution ?? (() => import("./execution.js")),
72
69
  );
@@ -78,9 +75,6 @@ export default function (pi: ExtensionAPI, dependencies: SubagentsDependencies =
78
75
  const refreshBlockingCatalog = blockingEnabled
79
76
  ? registerBlockingSubagent(pi, () => currentSettings, loadBlockingExecution)
80
77
  : () => undefined;
81
- if (blockingEnabled) {
82
- registerSubagentAutomation(pi, { getSettings: () => currentSettings }, dependencies.automation);
83
- }
84
78
  let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
85
79
  let refreshConsultCatalog: (catalog: string) => void = () => undefined;
86
80
 
@@ -225,7 +219,11 @@ function registerBlockingSubagent(
225
219
  ].join(" ");
226
220
  const promptGuidelines = () => [
227
221
  "Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
222
+ "The main agent retains overall planning, immediate critical-path work, integration, final verification, and the final answer.",
228
223
  "Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work the main agent can perform directly.",
224
+ "One ordinary implementation worker should not replace work the main agent can perform directly; use a blocking single only when intentional synchronous isolation or a user-requested specialist justifies waiting.",
225
+ "Keep ordinary planning in the main agent, or use explicit workflow mode when a genuine dependency graph requires caller-authored orchestration.",
226
+ "Keep ordinary review in the main agent with a review skill and deterministic checks; reserve panel mode or custom verifier agents for consequential independent verification.",
229
227
  "Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
230
228
  "Use a blocking subagent single, parallel, chain, workflow, panel, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
231
229
  `If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the configured max ${resolveBlockingMaxParallelTasks(getSettings())}, and avoid write-heavy implementation touching the same files or shared state.`,
@@ -310,7 +308,6 @@ function appendAgentCatalog(baseDescription: string, catalog: string): string {
310
308
 
311
309
  export { parsePositiveInteger } from "./execution/runtime-policy.js";
312
310
  export { buildPiArgs } from "./pi-args.js";
313
- export { formatTokens, formatUsageStats } from "./render.js";
314
311
  export {
315
312
  DEFAULT_CONSULT_RESOURCE_POLICY,
316
313
  DEFAULT_CONSULTATION_CWD_POLICY,
@@ -339,3 +336,4 @@ export {
339
336
  updateDelegationWorkflowSetting,
340
337
  updateStatefulLimitSetting,
341
338
  } from "./settings.js";
339
+ export { formatTokens, formatUsageStats } from "./usage-format.js";
@@ -1,5 +1,11 @@
1
1
  import { discoverAgents } from "./agents/discovery.js";
2
2
  import type { AgentConfig, SubagentSettings, SubagentThinkingLevel } from "./agents/types.js";
3
+ import {
4
+ CHILD_PEER_TOOL_NAMES,
5
+ childPeerBridgePath,
6
+ type PeerTransportRuntime,
7
+ peerBridgeEnvironment,
8
+ } from "./peer-transport.js";
3
9
  import { resolvePiPromptResources } from "./prompt-resources.js";
4
10
  import type { ManagedAgent, TurnOutcome } from "./registry.js";
5
11
  import { getResultFinalOutput, runSingleAgent, type SubagentDetails } from "./runner.js";
@@ -17,6 +23,7 @@ export function resolveStatefulSubprocessThinkingLevel(
17
23
 
18
24
  export interface SubprocessTransportOptions {
19
25
  getSettings?: () => SubagentSettings | undefined;
26
+ peerRuntime?: PeerTransportRuntime;
20
27
  }
21
28
 
22
29
  export class SubprocessTransport implements SubagentTransport {
@@ -54,35 +61,49 @@ export class SubprocessTransport implements SubagentTransport {
54
61
  projectAgentsDir: discovery.projectAgentsDir,
55
62
  results,
56
63
  });
57
- const single = await runSingleAgent(
58
- record.cwd,
59
- discovery.agents,
60
- record.agent,
61
- boundedTask.text,
62
- undefined,
63
- undefined,
64
- signal,
65
- resolveStatefulSubprocessThinkingLevel(discovery.agents, record),
66
- record.currentTimeoutMs ?? record.timeoutMs ?? resolveStatefulTurnTimeout(agent),
67
- undefined,
68
- makeDetails,
69
- undefined,
70
- {
71
- projectTrust,
72
- ...(record.executionPlan ? { tools: record.executionPlan.effectiveTools } : {}),
73
- appendSystemPromptPaths: promptResources?.appendSystemPromptPaths,
74
- timeoutResultFormat: record.resultFormat,
75
- turnLimits: {
76
- idleTimeoutMs: record.currentIdleTimeoutMs ?? record.idleTimeoutMs,
77
- maxTurns: record.currentMaxTurns ?? record.maxTurns,
78
- maxToolCalls: record.currentMaxToolCalls ?? record.maxToolCalls,
64
+ const credentials = this.options.peerRuntime
65
+ ? await this.options.peerRuntime.issueCredentials(
66
+ record.id,
67
+ record.currentTurnGeneration ?? record.turnGeneration ?? 1,
68
+ )
69
+ : undefined;
70
+ let single: Awaited<ReturnType<typeof runSingleAgent>>;
71
+ try {
72
+ single = await runSingleAgent(
73
+ record.cwd,
74
+ discovery.agents,
75
+ record.agent,
76
+ boundedTask.text,
77
+ undefined,
78
+ undefined,
79
+ signal,
80
+ resolveStatefulSubprocessThinkingLevel(discovery.agents, record),
81
+ record.currentTimeoutMs ?? record.timeoutMs ?? resolveStatefulTurnTimeout(agent),
82
+ undefined,
83
+ makeDetails,
84
+ undefined,
85
+ {
86
+ projectTrust,
87
+ ...(record.executionPlan ? { tools: record.executionPlan.effectiveTools } : {}),
88
+ appendSystemPromptPaths: promptResources?.appendSystemPromptPaths,
89
+ extensionPaths: credentials ? [childPeerBridgePath()] : undefined,
90
+ additionalTools: credentials ? [...CHILD_PEER_TOOL_NAMES] : undefined,
91
+ env: credentials ? peerBridgeEnvironment(credentials) : undefined,
92
+ timeoutResultFormat: record.resultFormat,
93
+ turnLimits: {
94
+ idleTimeoutMs: record.currentIdleTimeoutMs ?? record.idleTimeoutMs,
95
+ maxTurns: record.currentMaxTurns ?? record.maxTurns,
96
+ maxToolCalls: record.currentMaxToolCalls ?? record.maxToolCalls,
97
+ },
98
+ resultFormat: record.resultFormat,
99
+ contract: record.contract,
100
+ executionPlan: record.executionPlan,
101
+ displayTask: task,
79
102
  },
80
- resultFormat: record.resultFormat,
81
- contract: record.contract,
82
- executionPlan: record.executionPlan,
83
- displayTask: task,
84
- },
85
- );
103
+ );
104
+ } finally {
105
+ this.options.peerRuntime?.revoke(record.id);
106
+ }
86
107
  const settledAt = Date.now();
87
108
  const telemetry: TransportTelemetry = {
88
109
  ...starting,
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const ROOT_TASK_PATH = "/root";
4
+ export const MAX_TASK_NAME_LENGTH = 128;
5
+ export const MAX_TASK_PATH_LENGTH = 2_048;
6
+
7
+ const TASK_NAME_PATTERN = /^[a-z0-9_]+$/u;
8
+
9
+ export function validateTaskName(value: string): string {
10
+ if (!value) throw new Error("Subagent taskName must not be empty");
11
+ if (value === "root" || value === "." || value === "..") {
12
+ throw new Error(`Subagent taskName ${JSON.stringify(value)} is reserved`);
13
+ }
14
+ if (value.length > MAX_TASK_NAME_LENGTH) {
15
+ throw new Error(`Subagent taskName cannot exceed ${MAX_TASK_NAME_LENGTH} characters`);
16
+ }
17
+ if (value.includes("/")) throw new Error("Subagent taskName must not contain `/`");
18
+ if (!TASK_NAME_PATTERN.test(value)) {
19
+ throw new Error(
20
+ "Subagent taskName must use only lowercase ASCII letters, digits, and underscores",
21
+ );
22
+ }
23
+ return value;
24
+ }
25
+
26
+ export function validateTaskPath(value: string): string {
27
+ if (!value.startsWith(`${ROOT_TASK_PATH}/`) && value !== ROOT_TASK_PATH) {
28
+ throw new Error("Canonical subagent task paths must start with `/root`");
29
+ }
30
+ if (value.length > MAX_TASK_PATH_LENGTH) {
31
+ throw new Error(
32
+ `Canonical subagent task paths cannot exceed ${MAX_TASK_PATH_LENGTH} characters`,
33
+ );
34
+ }
35
+ if (value.endsWith("/") || value.includes("//")) {
36
+ throw new Error("Canonical subagent task paths must not contain empty segments");
37
+ }
38
+ for (const segment of value.slice(ROOT_TASK_PATH.length + 1).split("/")) {
39
+ if (segment) validateTaskName(segment);
40
+ }
41
+ return value;
42
+ }
43
+
44
+ export function joinTaskPath(parentPath: string, taskName: string): string {
45
+ const parent = validateTaskPath(parentPath);
46
+ const name = validateTaskName(taskName);
47
+ return validateTaskPath(`${parent}/${name}`);
48
+ }
49
+
50
+ export function resolveTaskPath(senderPath: string, reference: string): string {
51
+ if (!reference) throw new Error("Subagent task path reference must not be empty");
52
+ if (reference.startsWith("/")) return validateTaskPath(reference);
53
+ const sender = validateTaskPath(senderPath);
54
+ if (reference.endsWith("/")) {
55
+ throw new Error("Relative subagent task paths must not end with `/`");
56
+ }
57
+ let path = sender;
58
+ for (const segment of reference.split("/")) path = joinTaskPath(path, segment);
59
+ return path;
60
+ }
61
+
62
+ export function deriveTaskName(agentId: string): string {
63
+ const digest = createHash("sha256").update(agentId).digest("hex").slice(0, 32);
64
+ return `agent_${digest}`;
65
+ }
@@ -148,12 +148,6 @@ export function responsivenessSetupScreen(runtime: TransportUiRuntime) {
148
148
  description: "Choose separately whether an idle root resumes for synthesis",
149
149
  to: "settings" as const,
150
150
  },
151
- {
152
- id: "thinking",
153
- label: "Thinking profiles",
154
- description: "Preview explicit Fast, Balanced, or Deep per-agent defaults",
155
- to: "execution-profiles" as const,
156
- },
157
151
  { id: "back", label: "Back", action: "back" as const },
158
152
  ],
159
153
  hint: "back" as const,
package/src/transport.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ManagedAgent, TurnOutcome } from "./registry.js";
1
+ import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
2
2
  import type { TransportProgressCallback } from "./transport-types.js";
3
3
 
4
4
  export interface SubagentTransport {
@@ -9,6 +9,7 @@ export interface SubagentTransport {
9
9
  signal: AbortSignal,
10
10
  onProgress?: TransportProgressCallback,
11
11
  ): Promise<TurnOutcome>;
12
+ deliverMessage?(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean>;
12
13
  release?(agent: ManagedAgent): Promise<void>;
13
14
  shutdown?(): Promise<void>;
14
15
  }
@@ -0,0 +1,42 @@
1
+ import type { SubagentThinkingLevel } from "./agents/types.js";
2
+ import { safeLine } from "./render-common.js";
3
+
4
+ export function formatTokens(count: number): string {
5
+ if (count < 1000) return count.toString();
6
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
7
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
8
+ return `${(count / 1000000).toFixed(1)}M`;
9
+ }
10
+
11
+ export function formatUsageStats(
12
+ usage: {
13
+ input: number;
14
+ output: number;
15
+ cacheRead: number;
16
+ cacheWrite: number;
17
+ cost: number;
18
+ contextTokens?: number;
19
+ turns?: number;
20
+ },
21
+ model?: string,
22
+ thinkingLevel?: SubagentThinkingLevel,
23
+ actualProvider?: string,
24
+ actualModel?: string,
25
+ ): string {
26
+ const parts: string[] = [];
27
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
28
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
29
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
30
+ if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
31
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
32
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
33
+ if (usage.contextTokens && usage.contextTokens > 0)
34
+ parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
35
+ const safeProvider = actualProvider ? safeLine(actualProvider, "", 256) : undefined;
36
+ const safeModel = actualModel ? safeLine(actualModel, "", 256) : undefined;
37
+ const actual =
38
+ safeProvider && safeModel ? `${safeProvider}/${safeModel}` : (safeModel ?? safeProvider);
39
+ if (actual ?? model) parts.push(actual ?? safeLine(model, "", 256));
40
+ if (thinkingLevel) parts.push(`requested-thinking:${safeLine(thinkingLevel, "", 128)}`);
41
+ return parts.join(" ");
42
+ }
@@ -46,15 +46,15 @@ function workflowEffects(current: DelegationWorkflow, next: DelegationWorkflow):
46
46
  if (blockingEnabled(current) !== blockingEnabled(next)) {
47
47
  effects.push(
48
48
  blockingEnabled(next)
49
- ? "Add blocking `subagent`, explicit `subagent_auto`, and read-only `subagent_consult`"
50
- : "Remove blocking `subagent`, explicit `subagent_auto`, and read-only `subagent_consult`",
49
+ ? "Add blocking `subagent` and read-only `subagent_consult`"
50
+ : "Remove blocking `subagent` and read-only `subagent_consult`",
51
51
  );
52
52
  }
53
53
  if (asyncEnabled(current) !== asyncEnabled(next)) {
54
54
  effects.push(
55
55
  asyncEnabled(next)
56
- ? "Add reusable async lifecycle tools"
57
- : "Remove reusable async lifecycle tools",
56
+ ? "Add async `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`"
57
+ : "Remove async `subagent_spawn`, `subagent_send`, `subagent_manage`, and `subagent_mailbox`",
58
58
  );
59
59
  }
60
60
  return effects;