pi-advisor-flow 0.7.0 → 0.8.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.
- package/CHANGELOG.md +19 -18
- package/README.md +9 -8
- package/dist/index.js +1390 -1068
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +2 -0
- package/src/commands/lifecycle.ts +1 -0
- 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 +115 -73
- 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,22 @@
|
|
|
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
|
import { executorEffortRef, executorRef } from "./config/state.ts";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { isRecord, isString } from "./content-utils.ts";
|
|
6
|
+
import { collectTextStream, resolveConfiguredModel } from "./model-stream.ts";
|
|
7
|
+
import type {
|
|
8
|
+
CollectedTextStream,
|
|
9
|
+
ResolvedConfiguredModel,
|
|
9
10
|
} from "./model-stream.ts";
|
|
10
11
|
import { groupWire } from "./scout-groups.ts";
|
|
11
12
|
import { reconstructScoutConversation } from "./scout-reconstruct.ts";
|
|
12
13
|
import {
|
|
13
14
|
SCOUT_SELECTION_MAX_IDS,
|
|
14
15
|
SCOUT_SYNTHESIS_MAX_BYTES,
|
|
15
|
-
type ScoutManifest,
|
|
16
16
|
} from "./scout-types.ts";
|
|
17
|
+
import type { ScoutManifest } from "./scout-types.ts";
|
|
17
18
|
import { snapshotAdvisorUsage } from "./usage.ts";
|
|
19
|
+
import type { AdvisorUsageSnapshot } from "./usage.ts";
|
|
18
20
|
|
|
19
21
|
const SCOUT_TIMEOUT_MS = 30_000;
|
|
20
22
|
|
|
@@ -48,7 +50,7 @@ interface ScoutMetrics {
|
|
|
48
50
|
latencyMs: number;
|
|
49
51
|
omittedBeforeScout: number;
|
|
50
52
|
selectedCount: number;
|
|
51
|
-
usage?:
|
|
53
|
+
usage?: AdvisorUsageSnapshot;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface ScoutSelection {
|
|
@@ -100,8 +102,8 @@ const defaultDependencies: ScoutDependencies = {
|
|
|
100
102
|
collect: collectTextStream,
|
|
101
103
|
resolve: resolveConfiguredModel,
|
|
102
104
|
};
|
|
103
|
-
const byteLength = (value: string) => Buffer.byteLength(value, "
|
|
104
|
-
const AUTH_ERROR_PATTERN = /api key|auth|login|credential/
|
|
105
|
+
const byteLength = (value: string) => Buffer.byteLength(value, "utf-8");
|
|
106
|
+
const AUTH_ERROR_PATTERN = /api key|auth|login|credential/iu;
|
|
105
107
|
|
|
106
108
|
const manifestMessage = (manifest: ScoutManifest): Message => ({
|
|
107
109
|
content: [
|
|
@@ -120,6 +122,9 @@ const manifestMessage = (manifest: ScoutManifest): Message => ({
|
|
|
120
122
|
timestamp: Date.now(),
|
|
121
123
|
});
|
|
122
124
|
|
|
125
|
+
const isStringArray = (value: unknown): value is string[] =>
|
|
126
|
+
Array.isArray(value) && value.every(isString);
|
|
127
|
+
|
|
123
128
|
export const parseScoutSelection = (
|
|
124
129
|
text: string,
|
|
125
130
|
manifest: ScoutManifest
|
|
@@ -133,11 +138,10 @@ export const parseScoutSelection = (
|
|
|
133
138
|
} catch (error) {
|
|
134
139
|
throw new Error("Scout response is not a JSON object.", { cause: error });
|
|
135
140
|
}
|
|
136
|
-
if (!
|
|
141
|
+
if (!isRecord(value)) {
|
|
137
142
|
throw new Error("Scout response must be a JSON object.");
|
|
138
143
|
}
|
|
139
|
-
const
|
|
140
|
-
const keys = Object.keys(record).sort();
|
|
144
|
+
const keys = Object.keys(value).toSorted();
|
|
141
145
|
if (
|
|
142
146
|
keys.length !== 2 ||
|
|
143
147
|
keys[0] !== "selectedIds" ||
|
|
@@ -147,15 +151,10 @@ export const parseScoutSelection = (
|
|
|
147
151
|
"Scout response must contain only selectedIds and synthesis."
|
|
148
152
|
);
|
|
149
153
|
}
|
|
150
|
-
if (
|
|
151
|
-
!(
|
|
152
|
-
Array.isArray(record.selectedIds) &&
|
|
153
|
-
record.selectedIds.every((id) => typeof id === "string")
|
|
154
|
-
)
|
|
155
|
-
) {
|
|
154
|
+
if (!isStringArray(value.selectedIds)) {
|
|
156
155
|
throw new Error("Scout selectedIds must be an array of strings.");
|
|
157
156
|
}
|
|
158
|
-
const selectedIds =
|
|
157
|
+
const { selectedIds } = value;
|
|
159
158
|
if (new Set(selectedIds).size !== selectedIds.length) {
|
|
160
159
|
throw new Error("Scout selected duplicate group IDs.");
|
|
161
160
|
}
|
|
@@ -177,17 +176,17 @@ export const parseScoutSelection = (
|
|
|
177
176
|
const normalizedIds = manifest.groups
|
|
178
177
|
.filter((group) => retained.has(group.id))
|
|
179
178
|
.map((group) => group.id);
|
|
180
|
-
if (
|
|
181
|
-
throw new
|
|
179
|
+
if (!isString(value.synthesis)) {
|
|
180
|
+
throw new TypeError("Scout synthesis must be a string.");
|
|
182
181
|
}
|
|
183
|
-
if (byteLength(
|
|
182
|
+
if (byteLength(value.synthesis) > SCOUT_SYNTHESIS_MAX_BYTES) {
|
|
184
183
|
throw new Error(
|
|
185
184
|
`Scout synthesis exceeds ${SCOUT_SYNTHESIS_MAX_BYTES} UTF-8 bytes.`
|
|
186
185
|
);
|
|
187
186
|
}
|
|
188
187
|
return {
|
|
189
188
|
selectedIds: normalizedIds,
|
|
190
|
-
synthesis: knownSelectedIds.length > 0 ?
|
|
189
|
+
synthesis: knownSelectedIds.length > 0 ? value.synthesis : "",
|
|
191
190
|
};
|
|
192
191
|
};
|
|
193
192
|
|
|
@@ -212,6 +211,84 @@ const classifyResolutionError = (message: string): ScoutFallbackCategory => {
|
|
|
212
211
|
return "provider-error";
|
|
213
212
|
};
|
|
214
213
|
|
|
214
|
+
const setupAbortWatch = (
|
|
215
|
+
parentSignal: AbortSignal | undefined,
|
|
216
|
+
timeoutMs: number
|
|
217
|
+
) => {
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
let timedOut = false;
|
|
220
|
+
const abortFromParent = () => controller.abort(parentSignal?.reason);
|
|
221
|
+
parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
|
222
|
+
const timer = setTimeout(() => {
|
|
223
|
+
timedOut = true;
|
|
224
|
+
controller.abort(new Error("Scout timed out."));
|
|
225
|
+
}, timeoutMs);
|
|
226
|
+
timer.unref?.();
|
|
227
|
+
const { promise: abortPromise, reject: rejectOnAbort } =
|
|
228
|
+
Promise.withResolvers<never>();
|
|
229
|
+
const onControllerAbort = () =>
|
|
230
|
+
rejectOnAbort(controller.signal.reason ?? new Error("Scout aborted."));
|
|
231
|
+
controller.signal.addEventListener("abort", onControllerAbort, {
|
|
232
|
+
once: true,
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
abortPromise,
|
|
236
|
+
controller,
|
|
237
|
+
teardown: () => {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
240
|
+
controller.signal.removeEventListener("abort", onControllerAbort);
|
|
241
|
+
},
|
|
242
|
+
wasTimedOut: () => timedOut,
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
type ScoutStreamResult =
|
|
247
|
+
| { ok: true; streamed: CollectedTextStream }
|
|
248
|
+
| { ok: false; category: ScoutFallbackCategory; message: string };
|
|
249
|
+
|
|
250
|
+
const streamScoutResponse = async (
|
|
251
|
+
dependencies: ScoutDependencies,
|
|
252
|
+
resolved: ResolvedConfiguredModel,
|
|
253
|
+
manifest: ScoutManifest,
|
|
254
|
+
parentSignal: AbortSignal | undefined,
|
|
255
|
+
timeoutMs: number,
|
|
256
|
+
publish: (event: ScoutLifecycleEvent) => void
|
|
257
|
+
): Promise<ScoutStreamResult> => {
|
|
258
|
+
const { abortPromise, controller, teardown, wasTimedOut } = setupAbortWatch(
|
|
259
|
+
parentSignal,
|
|
260
|
+
timeoutMs
|
|
261
|
+
);
|
|
262
|
+
try {
|
|
263
|
+
const collection = dependencies.collect(resolved, {
|
|
264
|
+
messages: [manifestMessage(manifest)],
|
|
265
|
+
onChunk: (thinking, text) => {
|
|
266
|
+
if (!controller.signal.aborted) {
|
|
267
|
+
publish({ model: executorRef, text, thinking, type: "chunk" });
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
reasoning: executorEffortRef,
|
|
271
|
+
signal: controller.signal,
|
|
272
|
+
systemPrompt: SCOUT_SYSTEM,
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
streamed: await Promise.race([collection, abortPromise]),
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
280
|
+
return wasTimedOut()
|
|
281
|
+
? {
|
|
282
|
+
category: "timeout",
|
|
283
|
+
message: `Scout timed out after ${timeoutMs} ms.`,
|
|
284
|
+
ok: false,
|
|
285
|
+
}
|
|
286
|
+
: { category: "provider-error", message, ok: false };
|
|
287
|
+
} finally {
|
|
288
|
+
teardown();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
215
292
|
export const runAdvisorScout = async (
|
|
216
293
|
ctx: ExtensionContext,
|
|
217
294
|
manifest: ScoutManifest,
|
|
@@ -231,7 +308,7 @@ export const runAdvisorScout = async (
|
|
|
231
308
|
const fallback = (
|
|
232
309
|
category: ScoutFallbackCategory,
|
|
233
310
|
message: string,
|
|
234
|
-
usage?:
|
|
311
|
+
usage?: AdvisorUsageSnapshot
|
|
235
312
|
): ScoutOutcome => {
|
|
236
313
|
const outcome = {
|
|
237
314
|
category,
|
|
@@ -266,69 +343,34 @@ export const runAdvisorScout = async (
|
|
|
266
343
|
return cancelled();
|
|
267
344
|
}
|
|
268
345
|
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?.();
|
|
278
|
-
|
|
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
346
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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();
|
|
347
|
+
const streamed = await streamScoutResponse(
|
|
348
|
+
dependencies,
|
|
349
|
+
resolved,
|
|
350
|
+
manifest,
|
|
351
|
+
parentSignal,
|
|
352
|
+
timeoutMs,
|
|
353
|
+
publish
|
|
354
|
+
);
|
|
355
|
+
if (!streamed.ok) {
|
|
310
356
|
if (parentSignal?.aborted) {
|
|
311
357
|
return cancelled();
|
|
312
358
|
}
|
|
313
|
-
|
|
314
|
-
return timedOut
|
|
315
|
-
? fallback("timeout", `Scout timed out after ${timeoutMs} ms.`)
|
|
316
|
-
: fallback("provider-error", message);
|
|
359
|
+
return fallback(streamed.category, streamed.message);
|
|
317
360
|
}
|
|
318
|
-
teardown();
|
|
319
361
|
if (parentSignal?.aborted) {
|
|
320
362
|
return cancelled();
|
|
321
363
|
}
|
|
322
364
|
|
|
323
365
|
let selection: ScoutSelection;
|
|
324
366
|
try {
|
|
325
|
-
selection = parseScoutSelection(streamed.text, manifest);
|
|
367
|
+
selection = parseScoutSelection(streamed.streamed.text, manifest);
|
|
326
368
|
} catch (error) {
|
|
327
369
|
const message = error instanceof Error ? error.message : String(error);
|
|
328
370
|
return fallback(
|
|
329
|
-
streamed.text.trim() ? "invalid-selection" : "empty-response",
|
|
371
|
+
streamed.streamed.text.trim() ? "invalid-selection" : "empty-response",
|
|
330
372
|
message,
|
|
331
|
-
snapshotAdvisorUsage(streamed.usage)
|
|
373
|
+
snapshotAdvisorUsage(streamed.streamed.usage)
|
|
332
374
|
);
|
|
333
375
|
}
|
|
334
376
|
|
|
@@ -346,7 +388,7 @@ export const runAdvisorScout = async (
|
|
|
346
388
|
.filter((group) => group.required)
|
|
347
389
|
.map((group) => group.id),
|
|
348
390
|
]).size,
|
|
349
|
-
usage: snapshotAdvisorUsage(streamed.usage),
|
|
391
|
+
usage: snapshotAdvisorUsage(streamed.streamed.usage),
|
|
350
392
|
},
|
|
351
393
|
model: executorRef,
|
|
352
394
|
ok: true as const,
|
package/src/session-state.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRecordOf, isString } from "./content-utils.ts";
|
|
2
|
+
import type { JsonValue } from "./content-utils.ts";
|
|
3
|
+
import { AdvisorJevLedgerState } from "./jev/ledger.ts";
|
|
4
|
+
import type { AdvisorJevUsage } from "./jev/ledger.ts";
|
|
2
5
|
import {
|
|
3
|
-
type AdvisorUsageTotals,
|
|
4
6
|
addAdvisorUsage,
|
|
5
7
|
emptyAdvisorUsageTotals,
|
|
6
8
|
formatAdvisorUsageStatus,
|
|
7
9
|
formatAdvisorUsageTotals,
|
|
8
10
|
} from "./usage.ts";
|
|
11
|
+
import type { AdvisorUsageTotals } from "./usage.ts";
|
|
9
12
|
|
|
10
13
|
export type GateDecision = "proceed" | "revise" | "blocked";
|
|
11
14
|
export type ConsultationTrigger = "manual" | "executor-requested" | "turn-gate";
|
|
@@ -27,7 +30,7 @@ export interface AdvisorInvocationRecord {
|
|
|
27
30
|
usage?: unknown;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const WHITESPACE = /\s
|
|
33
|
+
const WHITESPACE = /\s/u;
|
|
31
34
|
const TIMESTAMP_KEYS = new Set([
|
|
32
35
|
"createdat",
|
|
33
36
|
"date",
|
|
@@ -37,7 +40,8 @@ const TIMESTAMP_KEYS = new Set([
|
|
|
37
40
|
"updatedat",
|
|
38
41
|
]);
|
|
39
42
|
const REQUEST_ID_KEYS = new Set(["correlationid", "requestid", "traceid"]);
|
|
40
|
-
const normalizedKey = (key: string) =>
|
|
43
|
+
const normalizedKey = (key: string) =>
|
|
44
|
+
key.replaceAll(/[-_]/gu, "").toLowerCase();
|
|
41
45
|
const isVolatileKey = (key: string, keys: Set<string>) =>
|
|
42
46
|
keys.has(normalizedKey(key));
|
|
43
47
|
|
|
@@ -75,15 +79,24 @@ const normalizeShellWhitespace = (command: string) => {
|
|
|
75
79
|
|
|
76
80
|
const normalizeString = (value: string) =>
|
|
77
81
|
value
|
|
78
|
-
.
|
|
79
|
-
.
|
|
82
|
+
.replaceAll(/\/(?:private\/)?tmp\/[^\s/]+/gu, "/tmp/<temporary>")
|
|
83
|
+
.replaceAll(/\/var\/folders\/[^\s/]+/gu, "/var/folders/<temporary>");
|
|
80
84
|
|
|
81
|
-
export
|
|
85
|
+
export type NormalizedToolInput =
|
|
86
|
+
| JsonValue
|
|
87
|
+
| undefined
|
|
88
|
+
| NormalizedToolInput[]
|
|
89
|
+
| { [key: string]: NormalizedToolInput };
|
|
90
|
+
|
|
91
|
+
export const normalizeToolInput = <Input>(
|
|
82
92
|
toolName: string,
|
|
83
|
-
input:
|
|
84
|
-
):
|
|
85
|
-
const visit = (
|
|
86
|
-
|
|
93
|
+
input: Input
|
|
94
|
+
): NormalizedToolInput => {
|
|
95
|
+
const visit = (
|
|
96
|
+
value: NormalizedToolInput,
|
|
97
|
+
key?: string
|
|
98
|
+
): NormalizedToolInput => {
|
|
99
|
+
if (isString(value)) {
|
|
87
100
|
if (key && isVolatileKey(key, TIMESTAMP_KEYS)) {
|
|
88
101
|
return "<timestamp>";
|
|
89
102
|
}
|
|
@@ -98,21 +111,23 @@ export const normalizeToolInput = (
|
|
|
98
111
|
if (Array.isArray(value)) {
|
|
99
112
|
return value.map((item) => visit(item));
|
|
100
113
|
}
|
|
101
|
-
if (value
|
|
102
|
-
const record = value as Record<string, unknown>;
|
|
114
|
+
if (isRecordOf(value)) {
|
|
103
115
|
return Object.fromEntries(
|
|
104
|
-
Object.keys(
|
|
105
|
-
.
|
|
106
|
-
.map((childKey) => [childKey, visit(
|
|
116
|
+
Object.keys(value)
|
|
117
|
+
.toSorted()
|
|
118
|
+
.map((childKey) => [childKey, visit(value[childKey], childKey)])
|
|
107
119
|
);
|
|
108
120
|
}
|
|
109
121
|
return value;
|
|
110
122
|
};
|
|
111
|
-
|
|
123
|
+
// SAFETY: Pi tool inputs are JSON-decoded tool arguments, so the root value is already NormalizedToolInput-shaped.
|
|
124
|
+
return visit(input as Input & NormalizedToolInput);
|
|
112
125
|
};
|
|
113
126
|
|
|
114
|
-
export const normalizedToolSignature = (
|
|
115
|
-
|
|
127
|
+
export const normalizedToolSignature = <Input>(
|
|
128
|
+
toolName: string,
|
|
129
|
+
input: Input
|
|
130
|
+
) => `${toolName}:${JSON.stringify(normalizeToolInput(toolName, input))}`;
|
|
116
131
|
|
|
117
132
|
interface RepetitionState {
|
|
118
133
|
count: number;
|
|
@@ -120,16 +135,15 @@ interface RepetitionState {
|
|
|
120
135
|
previousSignature?: string;
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
interface IssuedAdvice {
|
|
139
|
+
advice: string;
|
|
140
|
+
normalizedQuestion?: string;
|
|
141
|
+
trigger: ConsultationTrigger;
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
interface AdviceLedger {
|
|
124
145
|
draftConsultations: number;
|
|
125
|
-
issued: Map<
|
|
126
|
-
string,
|
|
127
|
-
{
|
|
128
|
-
advice: string;
|
|
129
|
-
normalizedQuestion?: string;
|
|
130
|
-
trigger: ConsultationTrigger;
|
|
131
|
-
}
|
|
132
|
-
>;
|
|
146
|
+
issued: Map<string, IssuedAdvice>;
|
|
133
147
|
lastAdvice?: string;
|
|
134
148
|
outcomes: number;
|
|
135
149
|
pending: Set<string>;
|
|
@@ -196,7 +210,7 @@ export class AdvisorSessionState {
|
|
|
196
210
|
this.#blockedReason ??= reason;
|
|
197
211
|
}
|
|
198
212
|
|
|
199
|
-
recordToolCall(toolName: string, input:
|
|
213
|
+
recordToolCall<Input>(toolName: string, input: Input, threshold: number) {
|
|
200
214
|
if (toolName === "ask_advisor") {
|
|
201
215
|
return false;
|
|
202
216
|
}
|
|
@@ -269,11 +283,11 @@ export class AdvisorSessionState {
|
|
|
269
283
|
draft = false,
|
|
270
284
|
normalizedQuestion?: string
|
|
271
285
|
) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
286
|
+
const issued: IssuedAdvice = { advice, trigger };
|
|
287
|
+
if (normalizedQuestion) {
|
|
288
|
+
issued.normalizedQuestion = normalizedQuestion;
|
|
289
|
+
}
|
|
290
|
+
this.#ledger.issued.set(id, issued);
|
|
277
291
|
this.#ledger.lastAdvice = advice;
|
|
278
292
|
if (draft) {
|
|
279
293
|
this.#ledger.draftConsultations += 1;
|
|
@@ -300,12 +314,11 @@ export class AdvisorSessionState {
|
|
|
300
314
|
return false;
|
|
301
315
|
}
|
|
302
316
|
const mentioned = paths.every((path) => {
|
|
303
|
-
const escaped = path.
|
|
304
|
-
const boundary =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return new RegExp(boundary).test(advice);
|
|
317
|
+
const escaped = path.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
318
|
+
const boundary = `(^|[\\s"'\`()\\[])${
|
|
319
|
+
escaped
|
|
320
|
+
}(?=$|[\\s"'\`),;:!?\\]]|\\.(?=\\s|$))`;
|
|
321
|
+
return new RegExp(boundary, "u").test(advice);
|
|
309
322
|
});
|
|
310
323
|
if (!mentioned) {
|
|
311
324
|
return false;
|
|
@@ -354,7 +367,7 @@ export class AdvisorSessionState {
|
|
|
354
367
|
(item) => item.kind === "gate"
|
|
355
368
|
);
|
|
356
369
|
return (
|
|
357
|
-
(["proceed", "revise", "blocked"] as
|
|
370
|
+
(["proceed", "revise", "blocked"] as const)
|
|
358
371
|
.map(
|
|
359
372
|
(decision) =>
|
|
360
373
|
[
|
|
@@ -375,15 +388,17 @@ export class AdvisorSessionState {
|
|
|
375
388
|
|
|
376
389
|
#triggersLine() {
|
|
377
390
|
return (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
391
|
+
(
|
|
392
|
+
[
|
|
393
|
+
"manual",
|
|
394
|
+
"executor-requested",
|
|
395
|
+
"turn-gate",
|
|
396
|
+
"repeated-tool-call",
|
|
397
|
+
"completion-review",
|
|
398
|
+
"custom-rule",
|
|
399
|
+
] as const
|
|
400
|
+
)
|
|
401
|
+
.filter((trigger) => this.#countTrigger(trigger) > 0)
|
|
387
402
|
.join(", ") || "none"
|
|
388
403
|
);
|
|
389
404
|
}
|