pi-advisor-flow 0.6.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 +34 -17
- package/README.md +10 -8
- package/dist/index.js +2852 -868
- package/extensions/index.ts +2 -1
- package/package.json +55 -40
- 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 +103 -79
- 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 +64 -2
- package/src/commands/types.ts +2 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +83 -1
- package/src/config/schema.ts +152 -15
- package/src/config/state.ts +80 -2
- package/src/config/storage.ts +38 -22
- package/src/config/types.ts +45 -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 +338 -0
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +267 -0
- package/src/jev/ledger.ts +228 -0
- package/src/jev/questions.ts +122 -0
- package/src/jev/state.ts +50 -0
- package/src/jev/transport.ts +63 -0
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +20 -12
- 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 +148 -39
- 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 +204 -0
- package/src/tools/jev-turn-gate.ts +203 -0
- package/src/tools/loop-gate.ts +32 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/outage-notifier.ts +30 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +73 -17
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +149 -73
- package/src/tools/registration.ts +15 -0
- package/src/tools/render-advisor-result.ts +77 -35
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +11 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +350 -0
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +62 -0
- 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 +200 -2
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +169 -47
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +25 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +29 -20
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,13 +1,17 @@
|
|
|
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";
|
|
1
5
|
import {
|
|
2
|
-
type AdvisorUsageTotals,
|
|
3
6
|
addAdvisorUsage,
|
|
4
7
|
emptyAdvisorUsageTotals,
|
|
5
8
|
formatAdvisorUsageStatus,
|
|
6
9
|
formatAdvisorUsageTotals,
|
|
7
10
|
} from "./usage.ts";
|
|
11
|
+
import type { AdvisorUsageTotals } from "./usage.ts";
|
|
8
12
|
|
|
9
13
|
export type GateDecision = "proceed" | "revise" | "blocked";
|
|
10
|
-
export type ConsultationTrigger = "manual" | "executor-requested";
|
|
14
|
+
export type ConsultationTrigger = "manual" | "executor-requested" | "turn-gate";
|
|
11
15
|
export type GateTrigger =
|
|
12
16
|
| "repeated-tool-call"
|
|
13
17
|
| "completion-review"
|
|
@@ -26,7 +30,7 @@ export interface AdvisorInvocationRecord {
|
|
|
26
30
|
usage?: unknown;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
const WHITESPACE = /\s
|
|
33
|
+
const WHITESPACE = /\s/u;
|
|
30
34
|
const TIMESTAMP_KEYS = new Set([
|
|
31
35
|
"createdat",
|
|
32
36
|
"date",
|
|
@@ -36,7 +40,8 @@ const TIMESTAMP_KEYS = new Set([
|
|
|
36
40
|
"updatedat",
|
|
37
41
|
]);
|
|
38
42
|
const REQUEST_ID_KEYS = new Set(["correlationid", "requestid", "traceid"]);
|
|
39
|
-
const normalizedKey = (key: string) =>
|
|
43
|
+
const normalizedKey = (key: string) =>
|
|
44
|
+
key.replaceAll(/[-_]/gu, "").toLowerCase();
|
|
40
45
|
const isVolatileKey = (key: string, keys: Set<string>) =>
|
|
41
46
|
keys.has(normalizedKey(key));
|
|
42
47
|
|
|
@@ -74,15 +79,24 @@ const normalizeShellWhitespace = (command: string) => {
|
|
|
74
79
|
|
|
75
80
|
const normalizeString = (value: string) =>
|
|
76
81
|
value
|
|
77
|
-
.
|
|
78
|
-
.
|
|
82
|
+
.replaceAll(/\/(?:private\/)?tmp\/[^\s/]+/gu, "/tmp/<temporary>")
|
|
83
|
+
.replaceAll(/\/var\/folders\/[^\s/]+/gu, "/var/folders/<temporary>");
|
|
79
84
|
|
|
80
|
-
export
|
|
85
|
+
export type NormalizedToolInput =
|
|
86
|
+
| JsonValue
|
|
87
|
+
| undefined
|
|
88
|
+
| NormalizedToolInput[]
|
|
89
|
+
| { [key: string]: NormalizedToolInput };
|
|
90
|
+
|
|
91
|
+
export const normalizeToolInput = <Input>(
|
|
81
92
|
toolName: string,
|
|
82
|
-
input:
|
|
83
|
-
):
|
|
84
|
-
const visit = (
|
|
85
|
-
|
|
93
|
+
input: Input
|
|
94
|
+
): NormalizedToolInput => {
|
|
95
|
+
const visit = (
|
|
96
|
+
value: NormalizedToolInput,
|
|
97
|
+
key?: string
|
|
98
|
+
): NormalizedToolInput => {
|
|
99
|
+
if (isString(value)) {
|
|
86
100
|
if (key && isVolatileKey(key, TIMESTAMP_KEYS)) {
|
|
87
101
|
return "<timestamp>";
|
|
88
102
|
}
|
|
@@ -97,21 +111,23 @@ export const normalizeToolInput = (
|
|
|
97
111
|
if (Array.isArray(value)) {
|
|
98
112
|
return value.map((item) => visit(item));
|
|
99
113
|
}
|
|
100
|
-
if (value
|
|
101
|
-
const record = value as Record<string, unknown>;
|
|
114
|
+
if (isRecordOf(value)) {
|
|
102
115
|
return Object.fromEntries(
|
|
103
|
-
Object.keys(
|
|
104
|
-
.
|
|
105
|
-
.map((childKey) => [childKey, visit(
|
|
116
|
+
Object.keys(value)
|
|
117
|
+
.toSorted()
|
|
118
|
+
.map((childKey) => [childKey, visit(value[childKey], childKey)])
|
|
106
119
|
);
|
|
107
120
|
}
|
|
108
121
|
return value;
|
|
109
122
|
};
|
|
110
|
-
|
|
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);
|
|
111
125
|
};
|
|
112
126
|
|
|
113
|
-
export const normalizedToolSignature = (
|
|
114
|
-
|
|
127
|
+
export const normalizedToolSignature = <Input>(
|
|
128
|
+
toolName: string,
|
|
129
|
+
input: Input
|
|
130
|
+
) => `${toolName}:${JSON.stringify(normalizeToolInput(toolName, input))}`;
|
|
115
131
|
|
|
116
132
|
interface RepetitionState {
|
|
117
133
|
count: number;
|
|
@@ -119,9 +135,15 @@ interface RepetitionState {
|
|
|
119
135
|
previousSignature?: string;
|
|
120
136
|
}
|
|
121
137
|
|
|
138
|
+
interface IssuedAdvice {
|
|
139
|
+
advice: string;
|
|
140
|
+
normalizedQuestion?: string;
|
|
141
|
+
trigger: ConsultationTrigger;
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
interface AdviceLedger {
|
|
123
145
|
draftConsultations: number;
|
|
124
|
-
issued: Map<string,
|
|
146
|
+
issued: Map<string, IssuedAdvice>;
|
|
125
147
|
lastAdvice?: string;
|
|
126
148
|
outcomes: number;
|
|
127
149
|
pending: Set<string>;
|
|
@@ -155,6 +177,9 @@ export class AdvisorSessionState {
|
|
|
155
177
|
#ledger = freshAdviceLedger();
|
|
156
178
|
#usage = freshUsage();
|
|
157
179
|
#consumedCalls = 0;
|
|
180
|
+
readonly #jev = new AdvisorJevLedgerState();
|
|
181
|
+
#sessionTurnOrdinal = 0;
|
|
182
|
+
#turnsSinceConsultation = 0;
|
|
158
183
|
|
|
159
184
|
resetTask() {
|
|
160
185
|
this.#repetition = freshRepetition();
|
|
@@ -162,6 +187,9 @@ export class AdvisorSessionState {
|
|
|
162
187
|
this.#ledger = freshAdviceLedger();
|
|
163
188
|
this.#usage = freshUsage();
|
|
164
189
|
this.#consumedCalls = 0;
|
|
190
|
+
this.#jev.reset();
|
|
191
|
+
this.#sessionTurnOrdinal = 0;
|
|
192
|
+
this.#turnsSinceConsultation = 0;
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
clearBlocked() {
|
|
@@ -182,7 +210,7 @@ export class AdvisorSessionState {
|
|
|
182
210
|
this.#blockedReason ??= reason;
|
|
183
211
|
}
|
|
184
212
|
|
|
185
|
-
recordToolCall(toolName: string, input:
|
|
213
|
+
recordToolCall<Input>(toolName: string, input: Input, threshold: number) {
|
|
186
214
|
if (toolName === "ask_advisor") {
|
|
187
215
|
return false;
|
|
188
216
|
}
|
|
@@ -214,6 +242,26 @@ export class AdvisorSessionState {
|
|
|
214
242
|
return this.#consumedCalls;
|
|
215
243
|
}
|
|
216
244
|
|
|
245
|
+
/** Counts one completed executor turn on both turn counters. The ordinal
|
|
246
|
+
* feeds the filter override window and summary positions and is never reset
|
|
247
|
+
* by anything; turnsSinceConsultation resets on every consultation. */
|
|
248
|
+
recordCompletedTurn() {
|
|
249
|
+
this.#sessionTurnOrdinal += 1;
|
|
250
|
+
this.#turnsSinceConsultation += 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
resetTurnsSinceConsultation() {
|
|
254
|
+
this.#turnsSinceConsultation = 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get sessionTurnOrdinal() {
|
|
258
|
+
return this.#sessionTurnOrdinal;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get turnsSinceConsultation() {
|
|
262
|
+
return this.#turnsSinceConsultation;
|
|
263
|
+
}
|
|
264
|
+
|
|
217
265
|
/** Returns a copy of cumulative direct Advisor usage for this session. */
|
|
218
266
|
get usageTotals(): AdvisorUsageTotals {
|
|
219
267
|
return { ...this.#usage.totals };
|
|
@@ -232,26 +280,45 @@ export class AdvisorSessionState {
|
|
|
232
280
|
id: string,
|
|
233
281
|
advice: string,
|
|
234
282
|
trigger: ConsultationTrigger,
|
|
235
|
-
draft = false
|
|
283
|
+
draft = false,
|
|
284
|
+
normalizedQuestion?: string
|
|
236
285
|
) {
|
|
237
|
-
|
|
286
|
+
const issued: IssuedAdvice = { advice, trigger };
|
|
287
|
+
if (normalizedQuestion) {
|
|
288
|
+
issued.normalizedQuestion = normalizedQuestion;
|
|
289
|
+
}
|
|
290
|
+
this.#ledger.issued.set(id, issued);
|
|
238
291
|
this.#ledger.lastAdvice = advice;
|
|
239
292
|
if (draft) {
|
|
240
293
|
this.#ledger.draftConsultations += 1;
|
|
241
294
|
}
|
|
242
295
|
}
|
|
296
|
+
|
|
297
|
+
/** Returns earlier advice for an exactly-matching normalized question. */
|
|
298
|
+
reattachedAdviceFor(
|
|
299
|
+
normalizedQuestion: string | undefined
|
|
300
|
+
): string | undefined {
|
|
301
|
+
if (!normalizedQuestion) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
for (const entry of this.#ledger.issued.values()) {
|
|
305
|
+
if (entry.normalizedQuestion === normalizedQuestion) {
|
|
306
|
+
return entry.advice;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
243
311
|
claimTrackedFiles(paths: string[]) {
|
|
244
312
|
const advice = this.#ledger.lastAdvice;
|
|
245
313
|
if (!advice || paths.length === 0) {
|
|
246
314
|
return false;
|
|
247
315
|
}
|
|
248
316
|
const mentioned = paths.every((path) => {
|
|
249
|
-
const escaped = path.
|
|
250
|
-
const boundary =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
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);
|
|
255
322
|
});
|
|
256
323
|
if (!mentioned) {
|
|
257
324
|
return false;
|
|
@@ -300,7 +367,7 @@ export class AdvisorSessionState {
|
|
|
300
367
|
(item) => item.kind === "gate"
|
|
301
368
|
);
|
|
302
369
|
return (
|
|
303
|
-
(["proceed", "revise", "blocked"] as
|
|
370
|
+
(["proceed", "revise", "blocked"] as const)
|
|
304
371
|
.map(
|
|
305
372
|
(decision) =>
|
|
306
373
|
[
|
|
@@ -321,21 +388,62 @@ export class AdvisorSessionState {
|
|
|
321
388
|
|
|
322
389
|
#triggersLine() {
|
|
323
390
|
return (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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)
|
|
332
402
|
.join(", ") || "none"
|
|
333
403
|
);
|
|
334
404
|
}
|
|
335
405
|
|
|
406
|
+
recordJevFilterAllowed() {
|
|
407
|
+
this.#jev.recordFilterAllowed();
|
|
408
|
+
}
|
|
409
|
+
recordJevFilterSkipped(repeat: boolean, normalizedQuestion?: string) {
|
|
410
|
+
this.#jev.recordFilterSkipped(
|
|
411
|
+
repeat,
|
|
412
|
+
normalizedQuestion,
|
|
413
|
+
this.#sessionTurnOrdinal
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
get lastJevSkip() {
|
|
417
|
+
return this.#jev.lastSkip;
|
|
418
|
+
}
|
|
419
|
+
recordJevFilterOverride() {
|
|
420
|
+
this.#jev.recordFilterOverride();
|
|
421
|
+
}
|
|
422
|
+
recordJevFilterFailure() {
|
|
423
|
+
this.#jev.recordFilterFailure();
|
|
424
|
+
}
|
|
425
|
+
recordJevFilterUsage(usage: AdvisorJevUsage) {
|
|
426
|
+
this.#jev.recordFilterUsage(usage);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
recordJevGateCheck(usage?: AdvisorJevUsage) {
|
|
430
|
+
this.#jev.recordGateCheck(usage);
|
|
431
|
+
}
|
|
432
|
+
recordJevGateConsultation() {
|
|
433
|
+
this.#jev.recordGateConsultation();
|
|
434
|
+
}
|
|
435
|
+
recordJevGateFailure() {
|
|
436
|
+
this.#jev.recordGateFailure();
|
|
437
|
+
}
|
|
438
|
+
|
|
336
439
|
summary(limit: number | undefined) {
|
|
337
440
|
const { invocations, totals } = this.#usage;
|
|
338
|
-
|
|
441
|
+
const jevLines = this.#jev.summaryLines(invocations);
|
|
442
|
+
if (
|
|
443
|
+
invocations.length === 0 &&
|
|
444
|
+
this.#repetition.interventions === 0 &&
|
|
445
|
+
jevLines.length === 0
|
|
446
|
+
) {
|
|
339
447
|
return;
|
|
340
448
|
}
|
|
341
449
|
const markdown = invocations.filter((item) => item.kind === "markdown");
|
|
@@ -366,6 +474,7 @@ export class AdvisorSessionState {
|
|
|
366
474
|
`Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
|
|
367
475
|
`Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,
|
|
368
476
|
`Failures: ${failures.length ? failures.join(", ") : "none"}`,
|
|
477
|
+
...jevLines,
|
|
369
478
|
].join("\n");
|
|
370
479
|
}
|
|
371
480
|
}
|
package/src/tool-result-cap.ts
CHANGED
|
@@ -14,6 +14,24 @@ export interface ToolResultTruncation {
|
|
|
14
14
|
|
|
15
15
|
const OMITTED_MARKER = "[... omitted tool-result section ...]";
|
|
16
16
|
|
|
17
|
+
const collect = (
|
|
18
|
+
candidates: string[],
|
|
19
|
+
maxEntries: number,
|
|
20
|
+
maxContentBytes: number
|
|
21
|
+
) => {
|
|
22
|
+
const selected: string[] = [];
|
|
23
|
+
let used = 0;
|
|
24
|
+
for (const line of candidates.slice(0, maxEntries)) {
|
|
25
|
+
const next = used + byteLength(line) + (selected.length ? 1 : 0);
|
|
26
|
+
if (next > maxContentBytes) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
selected.push(line);
|
|
30
|
+
used = next;
|
|
31
|
+
}
|
|
32
|
+
return selected;
|
|
33
|
+
};
|
|
34
|
+
|
|
17
35
|
export const capToolResult = (
|
|
18
36
|
value: string,
|
|
19
37
|
maxLines = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES,
|
|
@@ -53,23 +71,6 @@ export const capToolResult = (
|
|
|
53
71
|
}
|
|
54
72
|
const headCount = Math.floor((maxLines - 1) / 2);
|
|
55
73
|
const tailCount = maxLines - 1 - headCount;
|
|
56
|
-
const collect = (
|
|
57
|
-
candidates: string[],
|
|
58
|
-
maxEntries: number,
|
|
59
|
-
maxContentBytes: number
|
|
60
|
-
) => {
|
|
61
|
-
const selected: string[] = [];
|
|
62
|
-
let used = 0;
|
|
63
|
-
for (const line of candidates.slice(0, maxEntries)) {
|
|
64
|
-
const next = used + byteLength(line) + (selected.length ? 1 : 0);
|
|
65
|
-
if (next > maxContentBytes) {
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
selected.push(line);
|
|
69
|
-
used = next;
|
|
70
|
-
}
|
|
71
|
-
return selected;
|
|
72
|
-
};
|
|
73
74
|
const availableBytes = maxBytes - markerBytes - 2;
|
|
74
75
|
const head = collect(lines, headCount, Math.floor(availableBytes / 2));
|
|
75
76
|
const tail = collect(
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type UntrackedAttachment,
|
|
6
|
-
} from "../attachments.ts";
|
|
2
|
+
|
|
3
|
+
import { readTrackedFiles, readUntrackedFiles } from "../attachments.ts";
|
|
4
|
+
import type { UntrackedAttachment } from "../attachments.ts";
|
|
7
5
|
import {
|
|
8
6
|
advisorGitContextMaxCharsRef,
|
|
9
7
|
advisorGitContextRef,
|
|
@@ -13,19 +11,13 @@ import {
|
|
|
13
11
|
advisorUntrackedContentRef,
|
|
14
12
|
contextMaxCharsRef,
|
|
15
13
|
} from "../config/state.ts";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
collectGitContext,
|
|
19
|
-
type GitContextLevel,
|
|
20
|
-
} from "../git.ts";
|
|
14
|
+
import { clampGitContextLevel, collectGitContext } from "../git.ts";
|
|
15
|
+
import type { GitContextLevel } from "../git.ts";
|
|
21
16
|
import { readProjectPreferences } from "../preferences.ts";
|
|
22
17
|
import { redactAndCapText, redactSecrets } from "../redaction.ts";
|
|
23
|
-
import {
|
|
24
|
-
runAdvisorScout,
|
|
25
|
-
type ScoutLifecycleEvent,
|
|
26
|
-
type ScoutOutcome,
|
|
27
|
-
} from "../scout.ts";
|
|
28
18
|
import { curateAdvisorConversation } from "../scout-curation.ts";
|
|
19
|
+
import { runAdvisorScout } from "../scout.ts";
|
|
20
|
+
import type { ScoutLifecycleEvent, ScoutOutcome } from "../scout.ts";
|
|
29
21
|
import {
|
|
30
22
|
advisorGitContextBudget,
|
|
31
23
|
advisorRepositoryContext,
|