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.
- package/CHANGELOG.md +27 -18
- package/README.md +9 -8
- package/dist/index.js +1968 -1081
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/child-session.ts +15 -0
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +12 -3
- package/src/commands/lifecycle.ts +7 -1
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +128 -77
- package/src/session-state.ts +64 -49
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
package/src/scout-protocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { contentParts, isRecord,
|
|
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,
|
|
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
|
-
|
|
25
|
+
isString(part.id) ? part.id : undefined;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
):
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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,
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/scout-reconstruct.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
3
|
+
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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?:
|
|
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, "
|
|
104
|
-
const AUTH_ERROR_PATTERN = /api key|auth|login|credential/
|
|
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 (!
|
|
144
|
+
if (!isRecord(value)) {
|
|
137
145
|
throw new Error("Scout response must be a JSON object.");
|
|
138
146
|
}
|
|
139
|
-
const
|
|
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 =
|
|
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 (
|
|
181
|
-
throw new
|
|
182
|
+
if (!isString(value.synthesis)) {
|
|
183
|
+
throw new TypeError("Scout synthesis must be a string.");
|
|
182
184
|
}
|
|
183
|
-
if (byteLength(
|
|
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 ?
|
|
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?:
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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:
|
|
402
|
+
model: executorModel,
|
|
352
403
|
ok: true as const,
|
|
353
404
|
selectedLabels: manifest.groups
|
|
354
405
|
.filter(
|