pi-advisor-flow 0.7.0 → 0.8.1-dev.1

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 (93) hide show
  1. package/CHANGELOG.md +27 -18
  2. package/README.md +9 -8
  3. package/dist/index.js +1968 -1081
  4. package/extensions/index.ts +2 -1
  5. package/package.json +48 -35
  6. package/src/attachments.ts +11 -10
  7. package/src/child-session.ts +15 -0
  8. package/src/commands/activation-preparation.ts +1 -0
  9. package/src/commands/activation.ts +12 -3
  10. package/src/commands/lifecycle.ts +7 -1
  11. package/src/commands/manual-command.ts +7 -1
  12. package/src/commands/manual-consultation.ts +102 -92
  13. package/src/commands/manual-progress.ts +7 -5
  14. package/src/commands/model-commands.ts +1 -1
  15. package/src/commands/model-options.ts +14 -3
  16. package/src/commands/model-picker.ts +1 -0
  17. package/src/commands/registration.ts +1 -0
  18. package/src/commands/renderers.ts +67 -55
  19. package/src/commands/runtime.ts +16 -14
  20. package/src/commands/settings-commands.ts +10 -4
  21. package/src/commands/settings-persistence.ts +16 -2
  22. package/src/commands/types.ts +1 -0
  23. package/src/commands.ts +0 -1
  24. package/src/config/args.ts +1 -1
  25. package/src/config/defaults.ts +8 -1
  26. package/src/config/schema.ts +42 -17
  27. package/src/config/state.ts +12 -3
  28. package/src/config/storage.ts +34 -23
  29. package/src/config/types.ts +16 -3
  30. package/src/config/validation.ts +33 -21
  31. package/src/config.ts +3 -0
  32. package/src/content-utils.ts +24 -4
  33. package/src/conversation.ts +16 -15
  34. package/src/git.ts +10 -3
  35. package/src/herdr-block.ts +83 -0
  36. package/src/herdr-shared.ts +108 -0
  37. package/src/herdr.ts +30 -192
  38. package/src/jev/client.ts +75 -85
  39. package/src/jev/failure.ts +19 -0
  40. package/src/jev/key-store.ts +26 -11
  41. package/src/jev/ledger.ts +58 -64
  42. package/src/jev/questions.ts +26 -23
  43. package/src/jev/state.ts +15 -12
  44. package/src/jev/transport.ts +8 -3
  45. package/src/model-stream.ts +35 -37
  46. package/src/outcomes.ts +15 -11
  47. package/src/pi-settings.ts +4 -9
  48. package/src/preferences.ts +4 -2
  49. package/src/redaction.ts +10 -12
  50. package/src/scout-context.ts +59 -60
  51. package/src/scout-curation.ts +4 -13
  52. package/src/scout-groups.ts +178 -175
  53. package/src/scout-protocol.ts +74 -70
  54. package/src/scout-reconstruct.ts +1 -1
  55. package/src/scout.ts +128 -77
  56. package/src/session-state.ts +64 -49
  57. package/src/tool-result-cap.ts +18 -17
  58. package/src/tools/consult-context.ts +7 -15
  59. package/src/tools/consultation.ts +14 -4
  60. package/src/tools/gate-policy.ts +1 -0
  61. package/src/tools/gate-protocol.ts +36 -23
  62. package/src/tools/jev-filter.ts +43 -41
  63. package/src/tools/jev-turn-gate.ts +29 -9
  64. package/src/tools/loop-gate.ts +31 -20
  65. package/src/tools/model-access.ts +46 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +36 -16
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +146 -128
  71. package/src/tools/registration.ts +12 -14
  72. package/src/tools/render-advisor-result.ts +35 -22
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +1 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +65 -59
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +5 -11
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +38 -1
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +157 -70
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +14 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +26 -19
@@ -1,4 +1,5 @@
1
- import { contentParts, isRecord, type RecordValue } from "./content-utils.ts";
1
+ import { contentParts, isRecord, isString } from "./content-utils.ts";
2
+ import type { RecordValue } from "./content-utils.ts";
2
3
  import { textFrom } from "./conversation.ts";
3
4
  import type { InvalidProtocolResult } from "./scout-types.ts";
4
5
 
@@ -12,7 +13,7 @@ export const invalid = (message: string): InvalidProtocolResult => ({
12
13
  export interface ToolCallIndex {
13
14
  callOwners: Map<string, { index: number; name: string }>;
14
15
  latestUserIndex: number;
15
- resultsByCall: Map<string, Array<{ entry: RecordValue; index: number }>>;
16
+ resultsByCall: Map<string, { entry: RecordValue; index: number }[]>;
16
17
  }
17
18
 
18
19
  export const toolCalls = (message: RecordValue) =>
@@ -21,51 +22,51 @@ export const toolCalls = (message: RecordValue) =>
21
22
  );
22
23
 
23
24
  export const toolCallId = (part: RecordValue) =>
24
- typeof part.id === "string" ? part.id : undefined;
25
+ isString(part.id) ? part.id : undefined;
25
26
 
26
- /** First entry pass: indexes assistant tool calls and their results, tracking
27
- * the latest user message. Returns an invalid result on protocol violations. */
28
- export const indexToolCalls = (
29
- entries: unknown[]
30
- ): { index: ToolCallIndex; ok: true } | InvalidProtocolResult => {
31
- const callOwners = new Map<string, { index: number; name: string }>();
32
- const resultsByCall = new Map<
33
- string,
34
- Array<{ entry: RecordValue; index: number }>
35
- >();
36
- const state = { latestUserIndex: -1 };
37
- for (let index = 0; index < entries.length; index += 1) {
38
- const failure = indexEntry(
39
- entries[index] as unknown as RecordValue,
40
- index,
41
- callOwners,
42
- resultsByCall,
43
- state
44
- );
45
- if (failure) {
46
- return failure;
27
+ const indexAssistantCalls = (
28
+ message: RecordValue,
29
+ index: number,
30
+ callOwners: Map<string, { index: number; name: string }>
31
+ ): InvalidProtocolResult | undefined => {
32
+ for (const call of toolCalls(message)) {
33
+ const id = toolCallId(call);
34
+ if (!id || callOwners.has(id)) {
35
+ return invalid(
36
+ `Assistant tool calls at context entry ${index} have missing or duplicate IDs.`
37
+ );
47
38
  }
39
+ callOwners.set(id, {
40
+ index,
41
+ name: isString(call.name) ? call.name : "unknown",
42
+ });
48
43
  }
49
- for (const [id, results] of resultsByCall) {
50
- if (results.length > 1) {
51
- return invalid(`Tool call ${id} has duplicate result messages.`);
52
- }
44
+ return undefined;
45
+ };
46
+
47
+ const indexToolResult = (
48
+ entry: RecordValue,
49
+ message: RecordValue,
50
+ index: number,
51
+ resultsByCall: Map<string, { entry: RecordValue; index: number }[]>
52
+ ): InvalidProtocolResult | undefined => {
53
+ const id = message.toolCallId;
54
+ if (!isString(id)) {
55
+ return invalid(
56
+ `Tool result at context entry ${index} has no tool-call ID.`
57
+ );
53
58
  }
54
- return {
55
- index: {
56
- callOwners,
57
- latestUserIndex: state.latestUserIndex,
58
- resultsByCall,
59
- },
60
- ok: true,
61
- };
59
+ const results = resultsByCall.get(id) ?? [];
60
+ results.push({ entry, index });
61
+ resultsByCall.set(id, results);
62
+ return undefined;
62
63
  };
63
64
 
64
65
  const indexEntry = (
65
66
  entry: RecordValue,
66
67
  index: number,
67
68
  callOwners: Map<string, { index: number; name: string }>,
68
- resultsByCall: Map<string, Array<{ entry: RecordValue; index: number }>>,
69
+ resultsByCall: Map<string, { entry: RecordValue; index: number }[]>,
69
70
  state: { latestUserIndex: number }
70
71
  ): InvalidProtocolResult | undefined => {
71
72
  if (entry.type !== "message" || !isRecord(entry.message)) {
@@ -83,40 +84,43 @@ const indexEntry = (
83
84
  return undefined;
84
85
  };
85
86
 
86
- const indexToolResult = (
87
- entry: RecordValue,
88
- message: RecordValue,
89
- index: number,
90
- resultsByCall: Map<string, Array<{ entry: RecordValue; index: number }>>
91
- ): InvalidProtocolResult | undefined => {
92
- const id = message.toolCallId;
93
- if (typeof id !== "string") {
94
- return invalid(
95
- `Tool result at context entry ${index} has no tool-call ID.`
96
- );
97
- }
98
- const results = resultsByCall.get(id) ?? [];
99
- results.push({ entry, index });
100
- resultsByCall.set(id, results);
101
- return undefined;
102
- };
103
-
104
- const indexAssistantCalls = (
105
- message: RecordValue,
106
- index: number,
107
- callOwners: Map<string, { index: number; name: string }>
108
- ): InvalidProtocolResult | undefined => {
109
- for (const call of toolCalls(message)) {
110
- const id = toolCallId(call);
111
- if (!id || callOwners.has(id)) {
112
- return invalid(
113
- `Assistant tool calls at context entry ${index} have missing or duplicate IDs.`
87
+ /** First entry pass: indexes assistant tool calls and their results, tracking
88
+ * the latest user message. Returns an invalid result on protocol violations. */
89
+ export const indexToolCalls = (
90
+ entries: unknown[]
91
+ ): { index: ToolCallIndex; ok: true } | InvalidProtocolResult => {
92
+ const callOwners = new Map<string, { index: number; name: string }>();
93
+ const resultsByCall = new Map<
94
+ string,
95
+ { entry: RecordValue; index: number }[]
96
+ >();
97
+ const state = { latestUserIndex: -1 };
98
+ for (let index = 0; index < entries.length; index += 1) {
99
+ const entry = entries[index];
100
+ if (isRecord(entry)) {
101
+ const failure = indexEntry(
102
+ entry,
103
+ index,
104
+ callOwners,
105
+ resultsByCall,
106
+ state
114
107
  );
108
+ if (failure) {
109
+ return failure;
110
+ }
115
111
  }
116
- callOwners.set(id, {
117
- index,
118
- name: typeof call.name === "string" ? call.name : "unknown",
119
- });
120
112
  }
121
- return undefined;
113
+ for (const [id, results] of resultsByCall) {
114
+ if (results.length > 1) {
115
+ return invalid(`Tool call ${id} has duplicate result messages.`);
116
+ }
117
+ }
118
+ return {
119
+ index: {
120
+ callOwners,
121
+ latestUserIndex: state.latestUserIndex,
122
+ resultsByCall,
123
+ },
124
+ ok: true,
125
+ };
122
126
  };
@@ -20,7 +20,7 @@ export const reconstructScoutConversation = (
20
20
  const selected = new Set(selectedIds);
21
21
  const evidence = manifest.groups
22
22
  .filter((group) => group.required || selected.has(group.id))
23
- .sort((left, right) => left.originalIndex - right.originalIndex)
23
+ .toSorted((left, right) => left.originalIndex - right.originalIndex)
24
24
  .map((group) => group.content);
25
25
  const evidenceText = evidence.join("\n\n");
26
26
  if (maxChars <= 0) {
package/src/scout.ts CHANGED
@@ -1,20 +1,25 @@
1
1
  import type { Message } from "@earendil-works/pi-ai/compat";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { executorEffortRef, executorRef } from "./config/state.ts";
3
+
4
4
  import {
5
- type CollectedTextStream,
6
- collectTextStream,
7
- type ResolvedConfiguredModel,
8
- resolveConfiguredModel,
5
+ effectiveExecutorEffort,
6
+ effectiveExecutorRef,
7
+ } from "./child-session.ts";
8
+ import { isRecord, isString } from "./content-utils.ts";
9
+ import { collectTextStream, resolveConfiguredModel } from "./model-stream.ts";
10
+ import type {
11
+ CollectedTextStream,
12
+ ResolvedConfiguredModel,
9
13
  } from "./model-stream.ts";
10
14
  import { groupWire } from "./scout-groups.ts";
11
15
  import { reconstructScoutConversation } from "./scout-reconstruct.ts";
12
16
  import {
13
17
  SCOUT_SELECTION_MAX_IDS,
14
18
  SCOUT_SYNTHESIS_MAX_BYTES,
15
- type ScoutManifest,
16
19
  } from "./scout-types.ts";
20
+ import type { ScoutManifest } from "./scout-types.ts";
17
21
  import { snapshotAdvisorUsage } from "./usage.ts";
22
+ import type { AdvisorUsageSnapshot } from "./usage.ts";
18
23
 
19
24
  const SCOUT_TIMEOUT_MS = 30_000;
20
25
 
@@ -48,7 +53,7 @@ interface ScoutMetrics {
48
53
  latencyMs: number;
49
54
  omittedBeforeScout: number;
50
55
  selectedCount: number;
51
- usage?: unknown;
56
+ usage?: AdvisorUsageSnapshot;
52
57
  }
53
58
 
54
59
  export interface ScoutSelection {
@@ -100,8 +105,8 @@ const defaultDependencies: ScoutDependencies = {
100
105
  collect: collectTextStream,
101
106
  resolve: resolveConfiguredModel,
102
107
  };
103
- const byteLength = (value: string) => Buffer.byteLength(value, "utf8");
104
- const AUTH_ERROR_PATTERN = /api key|auth|login|credential/i;
108
+ const byteLength = (value: string) => Buffer.byteLength(value, "utf-8");
109
+ const AUTH_ERROR_PATTERN = /api key|auth|login|credential/iu;
105
110
 
106
111
  const manifestMessage = (manifest: ScoutManifest): Message => ({
107
112
  content: [
@@ -120,6 +125,9 @@ const manifestMessage = (manifest: ScoutManifest): Message => ({
120
125
  timestamp: Date.now(),
121
126
  });
122
127
 
128
+ const isStringArray = (value: unknown): value is string[] =>
129
+ Array.isArray(value) && value.every(isString);
130
+
123
131
  export const parseScoutSelection = (
124
132
  text: string,
125
133
  manifest: ScoutManifest
@@ -133,11 +141,10 @@ export const parseScoutSelection = (
133
141
  } catch (error) {
134
142
  throw new Error("Scout response is not a JSON object.", { cause: error });
135
143
  }
136
- if (!value || typeof value !== "object" || Array.isArray(value)) {
144
+ if (!isRecord(value)) {
137
145
  throw new Error("Scout response must be a JSON object.");
138
146
  }
139
- const record = value as Record<string, unknown>;
140
- const keys = Object.keys(record).sort();
147
+ const keys = Object.keys(value).toSorted();
141
148
  if (
142
149
  keys.length !== 2 ||
143
150
  keys[0] !== "selectedIds" ||
@@ -147,15 +154,10 @@ export const parseScoutSelection = (
147
154
  "Scout response must contain only selectedIds and synthesis."
148
155
  );
149
156
  }
150
- if (
151
- !(
152
- Array.isArray(record.selectedIds) &&
153
- record.selectedIds.every((id) => typeof id === "string")
154
- )
155
- ) {
157
+ if (!isStringArray(value.selectedIds)) {
156
158
  throw new Error("Scout selectedIds must be an array of strings.");
157
159
  }
158
- const selectedIds = record.selectedIds as string[];
160
+ const { selectedIds } = value;
159
161
  if (new Set(selectedIds).size !== selectedIds.length) {
160
162
  throw new Error("Scout selected duplicate group IDs.");
161
163
  }
@@ -177,17 +179,17 @@ export const parseScoutSelection = (
177
179
  const normalizedIds = manifest.groups
178
180
  .filter((group) => retained.has(group.id))
179
181
  .map((group) => group.id);
180
- if (typeof record.synthesis !== "string") {
181
- throw new Error("Scout synthesis must be a string.");
182
+ if (!isString(value.synthesis)) {
183
+ throw new TypeError("Scout synthesis must be a string.");
182
184
  }
183
- if (byteLength(record.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
185
+ if (byteLength(value.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
184
186
  throw new Error(
185
187
  `Scout synthesis exceeds ${SCOUT_SYNTHESIS_MAX_BYTES} UTF-8 bytes.`
186
188
  );
187
189
  }
188
190
  return {
189
191
  selectedIds: normalizedIds,
190
- synthesis: knownSelectedIds.length > 0 ? record.synthesis : "",
192
+ synthesis: knownSelectedIds.length > 0 ? value.synthesis : "",
191
193
  };
192
194
  };
193
195
 
@@ -212,6 +214,86 @@ const classifyResolutionError = (message: string): ScoutFallbackCategory => {
212
214
  return "provider-error";
213
215
  };
214
216
 
217
+ const setupAbortWatch = (
218
+ parentSignal: AbortSignal | undefined,
219
+ timeoutMs: number
220
+ ) => {
221
+ const controller = new AbortController();
222
+ let timedOut = false;
223
+ const abortFromParent = () => controller.abort(parentSignal?.reason);
224
+ parentSignal?.addEventListener("abort", abortFromParent, { once: true });
225
+ const timer = setTimeout(() => {
226
+ timedOut = true;
227
+ controller.abort(new Error("Scout timed out."));
228
+ }, timeoutMs);
229
+ timer.unref?.();
230
+ const { promise: abortPromise, reject: rejectOnAbort } =
231
+ Promise.withResolvers<never>();
232
+ const onControllerAbort = () =>
233
+ rejectOnAbort(controller.signal.reason ?? new Error("Scout aborted."));
234
+ controller.signal.addEventListener("abort", onControllerAbort, {
235
+ once: true,
236
+ });
237
+ return {
238
+ abortPromise,
239
+ controller,
240
+ teardown: () => {
241
+ clearTimeout(timer);
242
+ parentSignal?.removeEventListener("abort", abortFromParent);
243
+ controller.signal.removeEventListener("abort", onControllerAbort);
244
+ },
245
+ wasTimedOut: () => timedOut,
246
+ };
247
+ };
248
+
249
+ type ScoutStreamResult =
250
+ | { ok: true; streamed: CollectedTextStream }
251
+ | { ok: false; category: ScoutFallbackCategory; message: string };
252
+
253
+ const streamScoutResponse = async (
254
+ dependencies: ScoutDependencies,
255
+ resolved: ResolvedConfiguredModel,
256
+ executorModel: string,
257
+ executorEffort: string | undefined,
258
+ manifest: ScoutManifest,
259
+ parentSignal: AbortSignal | undefined,
260
+ timeoutMs: number,
261
+ publish: (event: ScoutLifecycleEvent) => void
262
+ ): Promise<ScoutStreamResult> => {
263
+ const { abortPromise, controller, teardown, wasTimedOut } = setupAbortWatch(
264
+ parentSignal,
265
+ timeoutMs
266
+ );
267
+ try {
268
+ const collection = dependencies.collect(resolved, {
269
+ messages: [manifestMessage(manifest)],
270
+ onChunk: (thinking, text) => {
271
+ if (!controller.signal.aborted) {
272
+ publish({ model: executorModel, text, thinking, type: "chunk" });
273
+ }
274
+ },
275
+ reasoning: executorEffort,
276
+ signal: controller.signal,
277
+ systemPrompt: SCOUT_SYSTEM,
278
+ });
279
+ return {
280
+ ok: true,
281
+ streamed: await Promise.race([collection, abortPromise]),
282
+ };
283
+ } catch (error) {
284
+ const message = error instanceof Error ? error.message : String(error);
285
+ return wasTimedOut()
286
+ ? {
287
+ category: "timeout",
288
+ message: `Scout timed out after ${timeoutMs} ms.`,
289
+ ok: false,
290
+ }
291
+ : { category: "provider-error", message, ok: false };
292
+ } finally {
293
+ teardown();
294
+ }
295
+ };
296
+
215
297
  export const runAdvisorScout = async (
216
298
  ctx: ExtensionContext,
217
299
  manifest: ScoutManifest,
@@ -231,7 +313,7 @@ export const runAdvisorScout = async (
231
313
  const fallback = (
232
314
  category: ScoutFallbackCategory,
233
315
  message: string,
234
- usage?: unknown
316
+ usage?: AdvisorUsageSnapshot
235
317
  ): ScoutOutcome => {
236
318
  const outcome = {
237
319
  category,
@@ -240,7 +322,7 @@ export const runAdvisorScout = async (
240
322
  usage === undefined
241
323
  ? baseMetrics(manifest, startedAt)
242
324
  : { ...baseMetrics(manifest, startedAt), usage },
243
- model: executorRef,
325
+ model: effectiveExecutorRef(ctx),
244
326
  ok: false as const,
245
327
  };
246
328
  publish({ outcome, type: "fallback" });
@@ -251,9 +333,11 @@ export const runAdvisorScout = async (
251
333
  return cancelled();
252
334
  }
253
335
 
336
+ const executorModel = effectiveExecutorRef(ctx);
337
+ const executorEffort = effectiveExecutorEffort(ctx);
254
338
  let resolved: ResolvedConfiguredModel;
255
339
  try {
256
- resolved = await dependencies.resolve(ctx, executorRef, "Scout");
340
+ resolved = await dependencies.resolve(ctx, executorModel, "Scout");
257
341
  } catch (error) {
258
342
  if (parentSignal?.aborted) {
259
343
  return cancelled();
@@ -265,70 +349,37 @@ export const runAdvisorScout = async (
265
349
  if (parentSignal?.aborted) {
266
350
  return cancelled();
267
351
  }
268
- publish({ model: executorRef, type: "call" });
269
- const controller = new AbortController();
270
- let timedOut = false;
271
- const abortFromParent = () => controller.abort(parentSignal?.reason);
272
- parentSignal?.addEventListener("abort", abortFromParent, { once: true });
273
- const timer = setTimeout(() => {
274
- timedOut = true;
275
- controller.abort(new Error("Scout timed out."));
276
- }, timeoutMs);
277
- timer.unref?.();
352
+ publish({ model: executorModel, type: "call" });
278
353
 
279
- let rejectOnAbort: ((reason?: unknown) => void) | undefined;
280
- const onControllerAbort = () =>
281
- rejectOnAbort?.(controller.signal.reason ?? new Error("Scout aborted."));
282
- const abortPromise = new Promise<never>((_resolve, reject) => {
283
- rejectOnAbort = reject;
284
- controller.signal.addEventListener("abort", onControllerAbort, {
285
- once: true,
286
- });
287
- });
288
- const teardown = () => {
289
- clearTimeout(timer);
290
- parentSignal?.removeEventListener("abort", abortFromParent);
291
- controller.signal.removeEventListener("abort", onControllerAbort);
292
- };
293
-
294
- let streamed: CollectedTextStream;
295
- try {
296
- const collection = dependencies.collect(resolved, {
297
- messages: [manifestMessage(manifest)],
298
- onChunk: (thinking, text) => {
299
- if (!controller.signal.aborted) {
300
- publish({ model: executorRef, text, thinking, type: "chunk" });
301
- }
302
- },
303
- reasoning: executorEffortRef,
304
- signal: controller.signal,
305
- systemPrompt: SCOUT_SYSTEM,
306
- });
307
- streamed = await Promise.race([collection, abortPromise]);
308
- } catch (error) {
309
- teardown();
354
+ const streamed = await streamScoutResponse(
355
+ dependencies,
356
+ resolved,
357
+ executorModel,
358
+ executorEffort,
359
+ manifest,
360
+ parentSignal,
361
+ timeoutMs,
362
+ publish
363
+ );
364
+ if (!streamed.ok) {
310
365
  if (parentSignal?.aborted) {
311
366
  return cancelled();
312
367
  }
313
- const message = error instanceof Error ? error.message : String(error);
314
- return timedOut
315
- ? fallback("timeout", `Scout timed out after ${timeoutMs} ms.`)
316
- : fallback("provider-error", message);
368
+ return fallback(streamed.category, streamed.message);
317
369
  }
318
- teardown();
319
370
  if (parentSignal?.aborted) {
320
371
  return cancelled();
321
372
  }
322
373
 
323
374
  let selection: ScoutSelection;
324
375
  try {
325
- selection = parseScoutSelection(streamed.text, manifest);
376
+ selection = parseScoutSelection(streamed.streamed.text, manifest);
326
377
  } catch (error) {
327
378
  const message = error instanceof Error ? error.message : String(error);
328
379
  return fallback(
329
- streamed.text.trim() ? "invalid-selection" : "empty-response",
380
+ streamed.streamed.text.trim() ? "invalid-selection" : "empty-response",
330
381
  message,
331
- snapshotAdvisorUsage(streamed.usage)
382
+ snapshotAdvisorUsage(streamed.streamed.usage)
332
383
  );
333
384
  }
334
385
 
@@ -346,9 +397,9 @@ export const runAdvisorScout = async (
346
397
  .filter((group) => group.required)
347
398
  .map((group) => group.id),
348
399
  ]).size,
349
- usage: snapshotAdvisorUsage(streamed.usage),
400
+ usage: snapshotAdvisorUsage(streamed.streamed.usage),
350
401
  },
351
- model: executorRef,
402
+ model: executorModel,
352
403
  ok: true as const,
353
404
  selectedLabels: manifest.groups
354
405
  .filter(