pi-workflow-engine 0.12.0 → 1.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 (92) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +106 -391
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
  4. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
  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 +5 -11
  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 +93 -100
  13. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
  14. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
  16. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
  17. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
  18. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
  19. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
  20. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
  21. package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
  22. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
  23. package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
  24. package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
  25. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
  26. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
  27. package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
  28. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
  29. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
  30. package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
  31. package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
  32. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
  33. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
  34. package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
  35. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
  36. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
  37. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
  38. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
  39. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
  40. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
  41. package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
  42. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
  43. package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
  52. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
  53. package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
  54. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
  55. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
  56. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
  57. package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
  58. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
  59. package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
  60. package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
  61. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
  62. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
  68. package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
  69. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
  86. package/USAGE.md +90 -17
  87. package/package.json +1 -2
  88. package/skills/workflow-code-review-actions/SKILL.md +12 -23
  89. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
  90. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
  91. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
  92. package/.pi/settings.json +0 -3
@@ -1,6 +1,7 @@
1
1
  import { isRetryableAssistantError, type AssistantMessage } from "@earendil-works/pi-ai";
2
2
  import { abortReason, throwIfAborted } from "./cancellation.ts";
3
3
  import {
4
+ lastAssistantError,
4
5
  providerUsageLimitFromMessages,
5
6
  type WorkflowProviderUsageLimitError,
6
7
  } from "./provider-usage-limit.ts";
@@ -75,42 +76,18 @@ export function providerErrorFromMessages(
75
76
  messages: readonly unknown[],
76
77
  options: { readonly pauseOnUsageLimit?: boolean } = {},
77
78
  ): WorkflowProviderError | WorkflowProviderUsageLimitError | undefined {
78
- const usageLimit = providerUsageLimitFromMessages(messages);
79
- if (usageLimit && options.pauseOnUsageLimit) return usageLimit;
80
- const message = messages.findLast(isAssistantMessage);
81
- if (!message || message.stopReason !== "error") return undefined;
82
- const errorMessage = typeof message.errorMessage === "string" && message.errorMessage.length > 0
83
- ? message.errorMessage
84
- : "Provider session ended with an unspecified error.";
85
- const retryable = isRetryableAssistantError(message as AssistantMessage);
86
- const provider = stringDetail(message.provider);
87
- const model = stringDetail(message.model);
88
- const api = stringDetail(message.api);
89
- return new WorkflowProviderError(errorMessage, {
90
- stopReason: "error",
91
- retryable,
92
- ...(provider ? { provider } : {}),
93
- ...(model ? { model } : {}),
94
- ...(api ? { api } : {}),
95
- });
96
- }
97
-
98
- function isAssistantMessage(value: unknown): value is {
99
- readonly role: "assistant";
100
- readonly stopReason?: unknown;
101
- readonly errorMessage?: unknown;
102
- readonly provider?: unknown;
103
- readonly model?: unknown;
104
- readonly api?: unknown;
105
- } {
106
- return (
107
- typeof value === "object" &&
108
- value !== null &&
109
- "role" in value &&
110
- value.role === "assistant"
79
+ if (options.pauseOnUsageLimit) {
80
+ const usageLimit = providerUsageLimitFromMessages(messages);
81
+ if (usageLimit) return usageLimit;
82
+ }
83
+ const failure = lastAssistantError(messages);
84
+ if (!failure) return undefined;
85
+ return new WorkflowProviderError(
86
+ failure.errorMessage || "Provider session ended with an unspecified error.",
87
+ {
88
+ stopReason: "error",
89
+ retryable: isRetryableAssistantError(failure.message as AssistantMessage),
90
+ ...failure.details,
91
+ },
111
92
  );
112
93
  }
113
-
114
- function stringDetail(value: unknown): string | undefined {
115
- return typeof value === "string" && value.length > 0 ? value : undefined;
116
- }
@@ -29,18 +29,18 @@ export type AgentRunnerSession = Pick<
29
29
  | "getActiveToolNames"
30
30
  | "getToolDefinition"
31
31
  | "setActiveToolsByName"
32
- | "setAutoRetryEnabled"
33
32
  | "getLastAssistantText"
34
33
  >;
35
34
 
36
35
  export type CreateAgentSession = (options: CreateAgentSessionOptions) => Promise<{ session: AgentRunnerSession }>;
37
36
 
37
+ /** Every row update after `agentQueued` addresses that row by its id; labels are display text, not unique keys. */
38
38
  export interface AgentProgress {
39
39
  agentQueued(phase: string | undefined, label: string): number;
40
- agentStart(phase: string | undefined, label: string, id?: number): void;
41
- agentTool(label: string, tool: string, id?: number): void;
42
- agentDone(label: string, id?: number): void;
43
- agentFailed(label: string, error: unknown, id?: number): void;
40
+ agentStart(id: number): void;
41
+ agentTool(id: number, tool: string): void;
42
+ agentDone(id: number): void;
43
+ agentFailed(id: number, error: unknown): void;
44
44
  event(event: WorkflowProgressEvent): void;
45
45
  log(message: string): void;
46
46
  }
@@ -83,9 +83,3 @@ export type RunContext = RunContextBase & (
83
83
  export type AgentExecutionOptions = AgentOptions & {
84
84
  readonly worktreeBaseline?: WorktreeBaseline;
85
85
  };
86
-
87
- export interface AgentRunTags {
88
- readonly [key: string]: string | number;
89
- readonly label: string;
90
- readonly phase: string;
91
- }
@@ -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;