pi-workflow-engine 0.13.0 → 1.1.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 (98) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +152 -416
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +1 -1
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +17 -14
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +7 -1
  5. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
  6. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
  7. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +12 -13
  9. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
  12. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +131 -117
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +4 -18
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +3 -0
  16. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
  17. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
  18. package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
  19. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +25 -80
  20. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
  21. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
  22. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +58 -117
  23. package/.pi/extensions/pi-workflow-engine/src/engine.ts +41 -80
  24. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
  25. package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
  26. package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
  27. package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -0
  28. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
  29. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
  30. package/.pi/extensions/pi-workflow-engine/src/journal.ts +0 -12
  31. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
  32. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
  33. package/.pi/extensions/pi-workflow-engine/src/model-ref.ts +19 -0
  34. package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
  35. package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
  36. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
  37. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
  38. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
  39. package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
  40. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
  41. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
  42. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
  43. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +29 -31
  44. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +75 -139
  45. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
  46. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +3 -4
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +7 -9
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -117
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
  52. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +8 -13
  53. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
  54. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
  55. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
  56. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
  57. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
  58. package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
  59. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +17 -4
  60. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
  61. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
  62. package/.pi/extensions/pi-workflow-engine/src/types.ts +30 -0
  63. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
  64. package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
  65. package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
  68. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
  69. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -129
  70. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
  71. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
  72. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
  73. package/.pi/extensions/pi-workflow-engine/src/usage.ts +41 -34
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +162 -22
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
  79. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
  80. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
  81. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
  82. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
  83. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
  84. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
  85. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +48 -110
  86. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +64 -93
  87. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +25 -67
  88. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +19 -53
  89. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +21 -49
  90. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
  91. package/README.md +1 -1
  92. package/USAGE.md +79 -20
  93. package/package.json +1 -2
  94. package/skills/workflow-code-review-actions/SKILL.md +12 -23
  95. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
  96. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -24
  97. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
  98. package/.pi/settings.json +0 -3
@@ -9,13 +9,9 @@ import {
9
9
  type AgentAttemptResult,
10
10
  type AgentReplayPlan,
11
11
  } from "./agent-replay.ts";
12
- import {
13
- resolveAgentModel,
14
- type ResolvedAgentModel,
15
- } from "./agent-session.ts";
12
+ import { resolveAgentModel } from "./agent-session.ts";
16
13
  import type {
17
14
  AgentExecutionOptions,
18
- AgentRunTags,
19
15
  RunContext,
20
16
  } from "./agent-runner-types.ts";
21
17
  import type { AgentResumeBaseContext } from "./resume-context.ts";
@@ -33,8 +29,6 @@ export type {
33
29
  CreateAgentSession,
34
30
  RunContext,
35
31
  } from "./agent-runner-types.ts";
36
- export { resolveAgentModel } from "./agent-session.ts";
37
- export type { ResolvedAgentModel, ResolvedAgentModelRequest } from "./agent-session.ts";
38
32
 
39
33
  /**
40
34
  * Run one subagent to completion in an isolated in-memory session.
@@ -54,7 +48,6 @@ export async function runAgent(
54
48
 
55
49
  const label = opts.label ?? "agent";
56
50
  const phase = opts.phase ?? "Workflow";
57
- const tags: AgentRunTags = { label, phase };
58
51
 
59
52
  return await rc.perf.time("agent.total_ms", async () => {
60
53
  throwIfAborted(rc.signal);
@@ -73,7 +66,7 @@ export async function runAgent(
73
66
  const agentRc: RunContext = { ...rc, signal: liveScope.signal };
74
67
  throwIfAborted(agentRc.signal);
75
68
  if (!isReplayEnabled(replay)) assertWorkflowBudgetAvailable(agentRc.budget);
76
- rc.progress.agentStart(opts.phase, label, rowId);
69
+ rc.progress.agentStart(rowId);
77
70
  if (replay.kind === "disabled") {
78
71
  rc.progress.log(`${label}: resume disabled for this call (${replay.reason})`);
79
72
  }
@@ -92,7 +85,7 @@ export async function runAgent(
92
85
  replay: attemptPlan,
93
86
  label,
94
87
  rowId,
95
- tags,
88
+ phase,
96
89
  admitLiveAgent: liveScope.admit,
97
90
  });
98
91
  } catch (error) {
@@ -108,23 +101,23 @@ export async function runAgent(
108
101
  providerRetries++;
109
102
  const delayMs = agentRetryDelayMs(providerRetries);
110
103
  rc.progress.log(`${label}: transient provider failure; retry ${providerRetries}/${agentRc.agentRetries} in ${delayMs}ms`);
111
- rc.perf.counter("agent.provider_retry", 1, tags);
112
- rc.perf.observe("agent.provider_retry_delay_ms", delayMs, tags);
113
- attemptPlan = { kind: "off" };
104
+ rc.perf.counter("agent.provider_retry");
105
+ rc.perf.observe("agent.provider_retry_delay_ms", delayMs);
106
+ // Keep the replay plan: the failed attempt recorded nothing, and the restart recaptures identity.
114
107
  await agentRc.retryScheduler.sleep(delayMs, agentRc.signal);
115
108
  continue;
116
109
  }
117
- const settlement = await settleAgentAttempt({ rc: agentRc, label, tags, replay: attemptPlan, outcome });
110
+ const settlement = await settleAgentAttempt({ rc: agentRc, label, outcome });
118
111
  if (settlement.kind === "retry-live") {
119
112
  attemptPlan = { kind: "off" };
120
113
  continue;
121
114
  }
122
- rc.progress.agentDone(label, rowId);
115
+ rc.progress.agentDone(rowId);
123
116
  return settlement.result;
124
117
  }
125
118
  },
126
119
  {
127
- onQueueWaitMs: (durationMs) => rc.perf.observe("agent.queue_wait_ms", durationMs, tags),
120
+ onQueueWaitMs: (durationMs) => rc.perf.observe("agent.queue_wait_ms", durationMs),
128
121
  signal: rc.signal,
129
122
  },
130
123
  );
@@ -132,13 +125,13 @@ export async function runAgent(
132
125
  let failure = error;
133
126
  if (liveScope.signal.aborted) failure = abortReason(liveScope.signal);
134
127
  if (rc.signal?.aborted) failure = abortReason(rc.signal);
135
- rc.progress.agentFailed(label, failure, rowId);
128
+ rc.progress.agentFailed(rowId, failure);
136
129
  rc.progress.log(`${label} failed: ${unknownErrorMessage(failure)}`);
137
130
  throw failure;
138
131
  } finally {
139
132
  liveScope.dispose();
140
133
  }
141
- }, tags);
134
+ });
142
135
  }
143
136
 
144
137
  function createAgentLiveScope(rc: RunContext, label: string) {
@@ -169,18 +162,19 @@ function resolveAgentRouting(
169
162
  rc: RunContext,
170
163
  opts: AgentExecutionOptions,
171
164
  label: string,
172
- ): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] } {
165
+ ): ReturnType<typeof resolveAgentModelProfile> {
173
166
  try {
174
167
  return resolveAgentModelProfile(
175
168
  {
176
169
  request: opts,
177
170
  profiles: rc.modelProfiles,
178
- resolveExplicitModel: (modelRef) => resolveAgentModel(modelRef, rc.modelRegistry, rc.hostModel).model,
171
+ resolveExplicitModel: (modelRef) => resolveAgentModel(modelRef, rc.modelRegistry),
179
172
  hostModel: rc.hostModel,
180
173
  },
181
174
  );
182
175
  } catch (error) {
183
- rc.progress.agentFailed(label, error);
176
+ // Routing runs before the call has a row; give the failure its own so it cannot land on a same-label sibling.
177
+ rc.progress.agentFailed(rc.progress.agentQueued(opts.phase, label), error);
184
178
  throw error;
185
179
  }
186
180
  }
@@ -6,11 +6,11 @@ import { normalizeWorkspaceReferences } from "./replay-path-identity.ts";
6
6
  import { unknownErrorMessage } from "./unknown-error.ts";
7
7
  import {
8
8
  captureEffectiveToolSourceIdentity,
9
+ nonEmptyString,
9
10
  type EffectiveToolSourceIdentity,
10
11
  type EffectiveToolSourceInfoLike,
11
- type ToolSourceFingerprintCache,
12
+ type ToolSourceIdentityOptions,
12
13
  } from "./tool-source-identity.ts";
13
- export type { EffectiveToolSourceIdentity, EffectiveToolSourceInfoLike } from "./tool-source-identity.ts";
14
14
 
15
15
  export interface EffectiveAgentModelLike {
16
16
  readonly provider: string;
@@ -78,14 +78,6 @@ export interface EffectiveAgentSessionIdentityOptions {
78
78
  readonly signal?: AbortSignal;
79
79
  }
80
80
 
81
- interface IdentityComponents {
82
- readonly runtimeVersion: string;
83
- readonly systemPromptFingerprint: string;
84
- readonly model: EffectiveAgentModelLike;
85
- readonly thinkingLevel: string;
86
- readonly tools: readonly EffectiveToolIdentity[];
87
- }
88
-
89
81
  /**
90
82
  * Capture the post-creation AgentSession state that determines whether a cached
91
83
  * agent result is safe to replay. Any missing or opaque executable identity
@@ -105,7 +97,13 @@ export async function captureEffectiveAgentSessionIdentity(
105
97
  const model = captureModel(session.model);
106
98
  const activeToolNames = captureActiveToolNames(session.getActiveToolNames());
107
99
  const toolInfoByName = indexToolInfo(session.getAllTools());
108
- const sourceCaptures: ToolSourceFingerprintCache = new Map();
100
+ const toolSourceOptions: ToolSourceIdentityOptions = {
101
+ runtimeVersion,
102
+ signal: options.signal,
103
+ sessionCwd,
104
+ cache: new Map(),
105
+ workspaceRoot,
106
+ };
109
107
  const tools: EffectiveToolIdentity[] = [];
110
108
 
111
109
  for (const name of activeToolNames) {
@@ -114,18 +112,10 @@ export async function captureEffectiveAgentSessionIdentity(
114
112
  if (!info) throw new Error(`active tool "${name}" is absent from the tool registry`);
115
113
  const definition = session.getToolDefinition(name);
116
114
  if (!definition) throw new Error(`active tool "${name}" has no executable definition`);
117
- tools.push(
118
- await captureToolIdentity(name, info, definition, {
119
- runtimeVersion,
120
- signal: options.signal,
121
- sessionCwd,
122
- sourceCaptures,
123
- workspaceRoot,
124
- }),
125
- );
115
+ tools.push(await captureToolIdentity(name, info, definition, toolSourceOptions));
126
116
  }
127
117
 
128
- const components: IdentityComponents = {
118
+ const components = {
129
119
  runtimeVersion,
130
120
  systemPromptFingerprint: hashIdentity(
131
121
  normalizeWorkspaceReferences(systemPrompt, { sessionCwd, workspaceRoot }),
@@ -147,19 +137,11 @@ export async function captureEffectiveAgentSessionIdentity(
147
137
  }
148
138
  }
149
139
 
150
- interface ToolCaptureOptions {
151
- readonly runtimeVersion: string;
152
- readonly sessionCwd: string;
153
- readonly workspaceRoot: string;
154
- readonly sourceCaptures: ToolSourceFingerprintCache;
155
- readonly signal?: AbortSignal;
156
- }
157
-
158
140
  async function captureToolIdentity(
159
141
  name: string,
160
142
  info: EffectiveToolInfoLike,
161
143
  definition: EffectiveToolDefinitionLike,
162
- options: ToolCaptureOptions,
144
+ options: ToolSourceIdentityOptions,
163
145
  ): Promise<EffectiveToolIdentity> {
164
146
  if (nonEmptyString(info.name, `registry name for active tool "${name}"`) !== name) {
165
147
  throw new Error(`tool registry returned a mismatched definition for "${name}"`);
@@ -175,13 +157,7 @@ async function captureToolIdentity(
175
157
  definition.prepareArguments === undefined
176
158
  ? null
177
159
  : inspectableFunctionSource(definition.prepareArguments, `argument-preparation handler for active tool "${name}"`);
178
- const source = await captureEffectiveToolSourceIdentity(info.sourceInfo, {
179
- runtimeVersion: options.runtimeVersion,
180
- sessionCwd: options.sessionCwd,
181
- workspaceRoot: options.workspaceRoot,
182
- cache: options.sourceCaptures,
183
- signal: options.signal,
184
- });
160
+ const source = await captureEffectiveToolSourceIdentity(info.sourceInfo, options);
185
161
 
186
162
  return {
187
163
  name,
@@ -244,12 +220,6 @@ function optionalString(value: string | undefined, label: string): string | null
244
220
  return stringValue(value, label);
245
221
  }
246
222
 
247
- function nonEmptyString(value: string, label: string): string {
248
- const result = stringValue(value, label);
249
- if (result.length === 0) throw new Error(`${label} is empty`);
250
- return result;
251
- }
252
-
253
223
  function stringValue(value: string, label: string): string {
254
224
  if (typeof value !== "string") throw new Error(`${label} is not a string`);
255
225
  return value;
@@ -8,12 +8,17 @@ type HostProviderRegistry = Pick<
8
8
  | "getRegisteredProviderConfig"
9
9
  | "getRegisteredProviderIds"
10
10
  | "isUsingOAuth"
11
- >;
11
+ > & {
12
+ /** pi 1.x keeps providers registered as objects (`pi.registerProvider(provider)`) apart from config providers. */
13
+ getRegisteredNativeProvider?(providerId: string): unknown;
14
+ };
15
+
16
+ type ChildModelRuntime = ModelRuntime & { registerNativeProvider?(provider: unknown): void };
12
17
 
13
18
  /** Mirror the host's live provider and selected-model auth state into a child runtime. */
14
19
  export async function synchronizeWorkflowModelRuntime(input: {
15
20
  readonly host: HostProviderRegistry;
16
- readonly child: ModelRuntime;
21
+ readonly child: ChildModelRuntime;
17
22
  readonly selectedModel: Model<Api> | undefined;
18
23
  readonly removeChildOnlyProviders: boolean;
19
24
  }): Promise<void> {
@@ -28,9 +33,16 @@ export async function synchronizeWorkflowModelRuntime(input: {
28
33
 
29
34
  for (const providerId of hostProviderIds) {
30
35
  const config = host.getRegisteredProviderConfig(providerId);
31
- if (!config) continue;
32
- child.unregisterProvider(providerId);
33
- child.registerProvider(providerId, config);
36
+ if (config) {
37
+ child.unregisterProvider(providerId);
38
+ child.registerProvider(providerId, config);
39
+ continue;
40
+ }
41
+ const native = host.getRegisteredNativeProvider?.(providerId);
42
+ if (native && child.registerNativeProvider) {
43
+ child.unregisterProvider(providerId);
44
+ child.registerNativeProvider(native);
45
+ }
34
46
  }
35
47
 
36
48
  const selectedProvider = selectedModel?.provider;
@@ -1,10 +1,12 @@
1
- import type { Api, Model } from "@earendil-works/pi-ai";
1
+ import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
2
2
  import {
3
3
  createAgentSessionFromServices,
4
4
  createAgentSessionServices,
5
+ createCodemodeExtension,
5
6
  defineTool,
6
7
  SessionManager,
7
8
  type CreateAgentSessionOptions,
9
+ type CreateAgentSessionServicesOptions,
8
10
  type ModelRegistry,
9
11
  type Skill,
10
12
  type ToolDefinition,
@@ -12,7 +14,7 @@ import {
12
14
  import { prepareAgentSkillResources } from "./agent-skills.ts";
13
15
  import type {
14
16
  AgentExecutionOptions,
15
- AgentRunTags,
17
+ AgentProgress,
16
18
  AgentRunnerSession,
17
19
  RunContext,
18
20
  } from "./agent-runner-types.ts";
@@ -23,26 +25,26 @@ import {
23
25
  } from "./structured-output.ts";
24
26
  import { providerErrorFromMessages } from "./agent-retry.ts";
25
27
  import { synchronizeWorkflowModelRuntime } from "./agent-session-providers.ts";
28
+ import { hostToolProxy, isMcpToolName, WorkflowHostToolUnavailableError, type HostToolBridge } from "./host-tools.ts";
26
29
  import { matchesAgentToolHint, WorkflowToolHintUnavailableError } from "./tool-capabilities.ts";
27
- import type { AgentToolHint } from "./types.ts";
30
+ import { parseAgentModelRef } from "./model-ref.ts";
31
+ import { truncateText } from "./text.ts";
32
+ import { WORKFLOW_TOOL_NAME, type AgentToolHint } from "./types.ts";
28
33
 
29
34
  export const FINAL_TOOL = "final_answer";
35
+ /** pi's codemode tool; SDK sessions only have it when their resource loader registers the extension. */
36
+ export const CODEMODE_TOOL = "codemode";
30
37
 
31
38
  const SCHEMA_REPROMPT =
32
39
  `You ended your turn without calling the ${FINAL_TOOL} tool, so no result was recorded. ` +
33
40
  `Call ${FINAL_TOOL} now with your final answer as its arguments. Do not reply with plain text.`;
34
41
  const DEFAULT_SEARCH_BASE_TOOLS = ["read", "bash", "grep", "find", "ls"];
35
-
36
- export interface ResolvedAgentModelRequest {
37
- readonly ref: string;
38
- readonly provider: string;
39
- readonly id: string;
40
- }
41
-
42
- export interface ResolvedAgentModel {
43
- readonly model: Model<Api> | undefined;
44
- readonly requested: ResolvedAgentModelRequest | undefined;
45
- }
42
+ const RETRY_REASON_CHARS = 160;
43
+ const RETRY_DISABLED_WARNING =
44
+ "pi auto-retry is disabled (retry.enabled: false in pi settings), so subagents will not retry failed provider turns. " +
45
+ "pi-workflow-engine 0.12.0 and 0.13.0 wrote that setting into ~/.pi/agent/settings.json; remove it unless you disabled retry on purpose.";
46
+ /** Runs (keyed by their shared progress sink) already warned about disabled pi auto-retry. */
47
+ const retryDisabledWarnedRuns = new WeakSet<AgentProgress>();
46
48
 
47
49
  export interface AgentSessionHandle {
48
50
  readonly session: AgentRunnerSession;
@@ -51,19 +53,14 @@ export interface AgentSessionHandle {
51
53
  structuredResult(): unknown;
52
54
  }
53
55
 
54
- export function resolveAgentModel(
55
- modelRef: string | undefined,
56
- modelRegistry: Pick<ModelRegistry, "find">,
57
- hostModel: Model<Api> | undefined,
58
- ): ResolvedAgentModel {
59
- if (modelRef === undefined) return { model: hostModel, requested: undefined };
60
-
56
+ /** Resolve an explicit `agent({ model })` ref; unknown refs fail instead of falling back to another model. */
57
+ export function resolveAgentModel(modelRef: string, modelRegistry: Pick<ModelRegistry, "find">): Model<Api> {
61
58
  const parsed = parseAgentModelRef(modelRef);
62
59
  const found = modelRegistry.find(parsed.provider, parsed.id);
63
60
  if (!found) {
64
61
  throw new Error(`Agent model "${modelRef}" not found (resolved as ${parsed.provider}/${parsed.id}).`);
65
62
  }
66
- return { model: found, requested: parsed };
63
+ return found;
67
64
  }
68
65
 
69
66
  export async function openAgentSession(input: {
@@ -73,9 +70,8 @@ export async function openAgentSession(input: {
73
70
  readonly cwd: string;
74
71
  readonly model: Model<Api> | undefined;
75
72
  readonly label: string;
76
- readonly tags: AgentRunTags;
77
73
  }): Promise<AgentSessionHandle> {
78
- const { rc, prompt, opts, cwd, model, label, tags } = input;
74
+ const { rc, prompt, opts, cwd, model, label } = input;
79
75
  let captured = false;
80
76
  let structuredResult: unknown;
81
77
  const customTools: ToolDefinition[] = opts.schema
@@ -86,6 +82,8 @@ export async function openAgentSession(input: {
86
82
  description:
87
83
  "Return your final structured answer. This MUST be your last action — do not write a normal reply after calling it.",
88
84
  parameters: opts.schema,
85
+ // The agent's own terminal action: a codemode script must not answer on its behalf.
86
+ exposure: "model-only",
89
87
  async execute(_toolCallId, params) {
90
88
  captured = true;
91
89
  structuredResult = params;
@@ -94,32 +92,29 @@ export async function openAgentSession(input: {
94
92
  }),
95
93
  ]
96
94
  : [];
95
+ customTools.push(...requestedHostTools(opts, rc.hostTools));
97
96
  let session: AgentRunnerSession | undefined;
98
97
  try {
99
98
  throwIfAborted(rc.signal);
100
- const resourceInput = { rc, prompt, opts, cwd, model, customTools, label };
101
- const resources = rc.createSession
102
- ? await prepareAgentSessionResources(resourceInput)
103
- : await rc.perf.time(
104
- "agent.session_resources_ms",
105
- () => prepareAgentSessionResources(resourceInput),
106
- tags,
107
- );
99
+ const resources = await rc.perf.time(
100
+ "agent.session_resources_ms",
101
+ () => prepareAgentSessionResources({ rc, prompt, opts, cwd, model, customTools, label }),
102
+ );
108
103
  const toolSelection = buildToolSelection(opts, resources.selectedSkills.length > 0);
109
104
  session = (
110
105
  await rc.perf.time(
111
106
  "agent.create_session_ms",
112
107
  () => resources.createSession(toolSelection.sessionOptions),
113
- tags,
114
108
  )
115
109
  ).session;
116
- session.setAutoRetryEnabled(false);
117
110
  const matchedToolHints = toolSelection.toolHints.length === 0
118
111
  ? new Set<AgentToolHint>()
119
112
  : applyDynamicToolHints(session, toolSelection);
113
+ // A nested run would get its own concurrency cap and budget, so subagents may start one only when allowlisted.
114
+ if (!opts.tools?.includes(WORKFLOW_TOOL_NAME)) withholdWorkflowTool(session);
120
115
  if (opts.requireToolHints) {
121
116
  const missing = toolSelection.toolHints.filter((hint) => !matchedToolHints.has(hint));
122
- if (missing.length > 0) throw new WorkflowToolHintUnavailableError(missing);
117
+ if (missing.length > 0) throw new WorkflowToolHintUnavailableError(missing, { hostToolsReachable: rc.hostTools !== undefined });
123
118
  }
124
119
  throwIfAborted(rc.signal);
125
120
  return {
@@ -129,11 +124,8 @@ export async function openAgentSession(input: {
129
124
  structuredResult: () => structuredResult,
130
125
  };
131
126
  } catch (error) {
132
- if (session) {
133
- const activeSession = session;
134
- session = undefined;
135
- rc.perf.timeSync("agent.dispose_ms", () => activeSession.dispose(), tags);
136
- }
127
+ const created = session;
128
+ if (created) rc.perf.timeSync("agent.dispose_ms", () => created.dispose());
137
129
  throw error;
138
130
  }
139
131
  }
@@ -145,87 +137,78 @@ export async function promptAgentSession(input: {
145
137
  readonly opts: AgentExecutionOptions;
146
138
  readonly label: string;
147
139
  readonly rowId: number;
148
- readonly tags: AgentRunTags;
140
+ readonly phase: string;
149
141
  }): Promise<unknown> {
150
- const { rc, handle, prompt, opts, label, rowId, tags } = input;
142
+ const { rc, handle, prompt, opts, label, rowId, phase } = input;
151
143
  const { session } = handle;
144
+ throwIfAborted(rc.signal);
145
+ // pi drops a retried turn's failed message from session.messages; keep it so usage covers every attempt.
146
+ const retriedFailures: AssistantMessage[] = [];
147
+ let lastFailure: AssistantMessage | undefined;
152
148
  const unsubscribe = session.subscribe((event) => {
153
- if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
154
- rc.progress.agentTool(label, event.toolName, rowId);
149
+ if (event.type === "tool_execution_start" && event.toolName !== FINAL_TOOL) {
150
+ rc.progress.agentTool(rowId, event.toolName);
151
+ } else if (event.type === "message_end" && event.message.role === "assistant" && event.message.stopReason === "error") {
152
+ lastFailure = event.message;
153
+ } else if (event.type === "auto_retry_start") {
154
+ if (lastFailure) retriedFailures.push(lastFailure);
155
+ lastFailure = undefined;
156
+ const reason = truncateText(event.errorMessage, RETRY_REASON_CHARS);
157
+ rc.progress.log(
158
+ `${label}: transient provider failure (${reason}); retrying turn ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms`,
159
+ );
160
+ rc.perf.counter("agent.turn_retry");
155
161
  }
156
162
  });
163
+ const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
164
+ const promptSession = async (text: string) => {
165
+ await rc.perf.time("agent.prompt_ms", () => raceWithAbort(() => session.prompt(text), rc.signal));
166
+ const failure = providerErrorFromMessages(session.messages, {
167
+ pauseOnUsageLimit: rc.pauseOnProviderUsageLimit,
168
+ });
169
+ if (failure) throw failure;
170
+ };
157
171
 
158
172
  try {
159
- const finalPrompt = opts.schema
160
- ? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
161
- : prompt;
162
- throwIfAborted(rc.signal);
163
- const unlinkPromptAbort = linkSessionAbort(rc.signal, session);
164
- try {
165
- try {
166
- const promptSession = async (text: string) => {
167
- await rc.perf.time("agent.prompt_ms", () => raceWithAbort(() => session.prompt(text), rc.signal), tags);
168
- const failure = providerErrorFromMessages(session.messages, {
169
- pauseOnUsageLimit: rc.pauseOnProviderUsageLimit,
170
- });
171
- if (failure) throw failure;
172
- };
173
- await promptSession(finalPrompt);
174
- if (opts.schema) {
175
- for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
176
- throwIfAborted(rc.signal);
177
- session.setActiveToolsByName([FINAL_TOOL]);
178
- rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${MAX_SCHEMA_REPAIR_ATTEMPTS})`);
179
- rc.perf.counter("agent.structured_reprompt", 1, tags);
180
- await promptSession(SCHEMA_REPROMPT);
181
- }
182
- }
183
-
173
+ await promptSession(
174
+ opts.schema ? `${prompt}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.` : prompt,
175
+ );
176
+ if (opts.schema && !handle.hasStructuredResult()) {
177
+ const activeTools = session.getActiveToolNames();
178
+ for (let attempt = 0; !handle.hasStructuredResult() && attempt < MAX_SCHEMA_REPAIR_ATTEMPTS; attempt++) {
184
179
  throwIfAborted(rc.signal);
185
- return rc.perf.timeSync(
186
- "agent.extract_result_ms",
187
- () => {
188
- if (!opts.schema) {
189
- return session.getLastAssistantText() ?? "";
190
- }
191
- if (!handle.hasStructuredResult()) {
192
- rc.progress.log(`${label}: no structured answer returned`);
193
- rc.perf.counter("agent.structured_missing", 1, tags);
194
- throw new WorkflowStructuredOutputError(label, MAX_SCHEMA_REPAIR_ATTEMPTS);
195
- }
196
- return handle.structuredResult();
197
- },
198
- tags,
199
- );
200
- } finally {
201
- rc.usage.recordAgentSession({ label, phase: tags.phase, messages: session.messages });
180
+ session.setActiveToolsByName([FINAL_TOOL]);
181
+ rc.progress.log(`${label}: no final answer; re-prompting (${attempt + 1}/${MAX_SCHEMA_REPAIR_ATTEMPTS})`);
182
+ rc.perf.counter("agent.structured_reprompt");
183
+ await promptSession(SCHEMA_REPROMPT);
202
184
  }
203
- } finally {
204
- unlinkPromptAbort();
185
+ // Narrowing also rebuilds pi's system prompt; restore both so replay validation sees the captured identity.
186
+ session.setActiveToolsByName(activeTools);
205
187
  }
188
+
189
+ throwIfAborted(rc.signal);
190
+ return rc.perf.timeSync(
191
+ "agent.extract_result_ms",
192
+ () => {
193
+ if (!opts.schema) {
194
+ return session.getLastAssistantText() ?? "";
195
+ }
196
+ if (!handle.hasStructuredResult()) {
197
+ rc.progress.log(`${label}: no structured answer returned`);
198
+ rc.perf.counter("agent.structured_missing");
199
+ throw new WorkflowStructuredOutputError(label, MAX_SCHEMA_REPAIR_ATTEMPTS);
200
+ }
201
+ return handle.structuredResult();
202
+ },
203
+ );
206
204
  } finally {
205
+ // Detach first: recording publishes usage to the UI and must not be able to skip listener cleanup.
206
+ unlinkPromptAbort();
207
207
  unsubscribe();
208
+ rc.usage.recordAgentSession({ label, phase, messages: [...retriedFailures, ...session.messages] });
208
209
  }
209
210
  }
210
211
 
211
- function parseAgentModelRef(modelRef: string): ResolvedAgentModelRequest {
212
- const normalized = modelRef.trim();
213
- if (normalized.length === 0) {
214
- throw new Error('Invalid agent model ref: expected a bare model id or "provider/id".');
215
- }
216
- if (normalized !== modelRef) {
217
- throw new Error(`Invalid agent model ref "${modelRef}": remove leading or trailing whitespace.`);
218
- }
219
-
220
- const slash = modelRef.indexOf("/");
221
- if (slash === -1) return { ref: modelRef, provider: "anthropic", id: modelRef };
222
- const provider = modelRef.slice(0, slash);
223
- const id = modelRef.slice(slash + 1);
224
- if (provider.length === 0 || id.length === 0 || id.startsWith("/")) {
225
- throw new Error(`Invalid agent model ref "${modelRef}": expected "provider/id".`);
226
- }
227
- return { ref: modelRef, provider, id };
228
- }
229
212
 
230
213
  interface AgentSessionResources {
231
214
  readonly selectedSkills: readonly Skill[];
@@ -270,7 +253,7 @@ async function prepareAgentSessionResources(input: {
270
253
  const preparedSkills = prepareAgentSkillResources(skillOptions);
271
254
  const services = await createAgentSessionServices({
272
255
  cwd,
273
- resourceLoaderOptions: preparedSkills.resourceLoaderOptions,
256
+ resourceLoaderOptions: { ...preparedSkills.resourceLoaderOptions, ...codemodeExtension(opts) },
274
257
  });
275
258
  await synchronizeWorkflowModelRuntime({
276
259
  host: rc.modelRegistry,
@@ -282,6 +265,10 @@ async function prepareAgentSessionResources(input: {
282
265
  for (const diagnostic of services.diagnostics) {
283
266
  rc.progress.log(`${label}: session ${diagnostic.type}: ${diagnostic.message}`);
284
267
  }
268
+ if (!services.settingsManager.getRetryEnabled() && !retryDisabledWarnedRuns.has(rc.progress)) {
269
+ retryDisabledWarnedRuns.add(rc.progress);
270
+ rc.progress.log(RETRY_DISABLED_WARNING);
271
+ }
285
272
  const selectedSkills = preparedSkills.resolve(services.resourceLoader);
286
273
  return {
287
274
  selectedSkills,
@@ -293,6 +280,36 @@ async function prepareAgentSessionResources(input: {
293
280
  };
294
281
  }
295
282
 
283
+ /**
284
+ * SDK sessions load none of pi's built-in extensions, so codemode is registered only for agents whose
285
+ * allowlist names it. Scripts can call just the agent's other allowed tools. Mode "on" keeps those
286
+ * tools declared whatever the user's `codemode.mode`, so the author's allowlist stays what the model sees.
287
+ */
288
+ function codemodeExtension(opts: AgentExecutionOptions): Pick<SessionResourceLoaderOptions, "extensionFactories"> {
289
+ if (!opts.tools?.includes(CODEMODE_TOOL)) return {};
290
+ return { extensionFactories: [{ name: CODEMODE_TOOL, factory: createCodemodeExtension({ mode: "on" }), replaceable: true }] };
291
+ }
292
+
293
+ type SessionResourceLoaderOptions = NonNullable<CreateAgentSessionServicesOptions["resourceLoaderOptions"]>;
294
+
295
+ /**
296
+ * Proxies for the host MCP tools this agent asked for: those named in its allowlist, and open-world
297
+ * research tools when it hints `external-search`. Local `search` hints never bridge, because host tools
298
+ * run in the host session's working directory, not the agent's (an isolated worktree, for example).
299
+ * A named tool the run cannot reach fails the agent, like `requireToolHints`, rather than letting it
300
+ * work without a tool its prompt may rely on.
301
+ */
302
+ function requestedHostTools(opts: AgentExecutionOptions, bridge: HostToolBridge | undefined): ToolDefinition[] {
303
+ const named = new Set((opts.tools ?? []).filter(isMcpToolName));
304
+ const externalSearch = opts.toolHints?.includes("external-search") ?? false;
305
+ if (named.size === 0 && !externalSearch) return [];
306
+ const available = bridge?.tools() ?? [];
307
+ const selected = available.filter((tool) => named.has(tool.name) || (externalSearch && matchesAgentToolHint(tool, "external-search")));
308
+ const missing = [...named].filter((name) => !selected.some((tool) => tool.name === name));
309
+ if (missing.length > 0) throw new WorkflowHostToolUnavailableError(missing, bridge !== undefined);
310
+ return bridge ? selected.map((tool) => hostToolProxy(tool, bridge)) : [];
311
+ }
312
+
296
313
  function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerSession): () => void {
297
314
  if (!signal) return () => {};
298
315
  const onAbort = () => {
@@ -322,16 +339,8 @@ function buildToolSelection(opts: AgentExecutionOptions, skillsEnabled: boolean)
322
339
  ? ["read"]
323
340
  : undefined;
324
341
  const activeTools = buildToolList(opts, skillsEnabled, fallback);
325
- if (toolHints.length === 0) {
326
- return {
327
- sessionOptions: { tools: activeTools ? [...activeTools] : undefined },
328
- activeTools,
329
- toolHints,
330
- };
331
- }
332
-
333
342
  return {
334
- sessionOptions: { noTools: "builtin" },
343
+ sessionOptions: toolHints.length === 0 ? { tools: activeTools } : { noTools: "builtin" },
335
344
  activeTools,
336
345
  toolHints,
337
346
  };
@@ -350,11 +359,16 @@ function buildToolList(
350
359
  return allow;
351
360
  }
352
361
 
362
+ function withholdWorkflowTool(session: AgentRunnerSession): void {
363
+ const active = session.getActiveToolNames();
364
+ if (active.includes(WORKFLOW_TOOL_NAME)) session.setActiveToolsByName(active.filter((name) => name !== WORKFLOW_TOOL_NAME));
365
+ }
366
+
353
367
  function applyDynamicToolHints(
354
368
  session: AgentRunnerSession,
355
369
  selection: ToolSelection,
356
370
  ): ReadonlySet<AgentToolHint> {
357
- const active = new Set(selection.activeTools ?? []);
371
+ const active = new Set(selection.activeTools);
358
372
  const matched = new Set<AgentToolHint>();
359
373
  for (const tool of session.getAllTools()) {
360
374
  for (const hint of selection.toolHints) {