pi-advisor-flow 0.5.5 → 0.6.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 +28 -1
- package/README.md +1 -1
- package/dist/index.js +5786 -0
- package/extensions/index.ts +9 -9
- package/package.json +16 -10
- package/src/{untracked.ts → attachments.ts} +2 -5
- package/src/commands/activation-preparation.ts +6 -6
- package/src/commands/activation.ts +7 -7
- package/src/commands/lifecycle.ts +5 -5
- package/src/commands/manual-command.ts +13 -9
- package/src/commands/manual-consultation.ts +8 -8
- package/src/commands/manual-progress.ts +3 -3
- package/src/commands/model-commands.ts +6 -6
- package/src/commands/model-options.ts +4 -4
- package/src/commands/model-picker.ts +2 -2
- package/src/commands/registration.ts +8 -8
- package/src/commands/renderers.ts +5 -5
- package/src/commands/runtime.ts +8 -8
- package/src/commands/settings-commands.ts +8 -8
- package/src/commands/settings-persistence.ts +4 -4
- package/src/commands/types.ts +5 -13
- package/src/commands.ts +2 -2
- package/src/config/args.ts +3 -3
- package/src/config/defaults.ts +3 -3
- package/src/config/schema.ts +322 -0
- package/src/config/state.ts +2 -2
- package/src/config/storage.ts +113 -103
- package/src/config/types.ts +1 -1
- package/src/config/validation.ts +61 -181
- package/src/config.ts +7 -7
- package/src/content-utils.ts +30 -0
- package/src/conversation.ts +13 -168
- package/src/git.ts +4 -18
- package/src/herdr.ts +6 -8
- package/src/model-stream.ts +5 -8
- package/src/outcomes.ts +2 -5
- package/src/pi-settings.ts +59 -0
- package/src/preferences.ts +2 -2
- package/src/redaction.ts +50 -0
- package/src/scout-context.ts +121 -485
- package/src/scout-curation.ts +84 -0
- package/src/scout-groups.ts +485 -0
- package/src/scout-protocol.ts +122 -0
- package/src/scout-reconstruct.ts +44 -0
- package/src/scout-types.ts +66 -0
- package/src/scout.ts +53 -63
- package/src/session-state.ts +119 -76
- package/src/tool-result-cap.ts +88 -0
- package/src/tools/consult-context.ts +155 -0
- package/src/tools/consultation.ts +71 -220
- package/src/tools/gate-policy.ts +5 -5
- package/src/tools/gate-protocol.ts +2 -2
- package/src/tools/loop-gate.ts +70 -61
- package/src/tools/prompts.ts +4 -7
- package/src/tools/register-ask-advisor.ts +9 -14
- package/src/tools/register-lifecycle.ts +7 -7
- package/src/tools/register-outcome.ts +5 -5
- package/src/tools/register-renderers.ts +5 -5
- package/src/tools/registration.ts +10 -10
- package/src/tools/render-advisor-result.ts +11 -8
- package/src/tools/render-common.ts +25 -13
- package/src/tools/scout-status.ts +5 -5
- package/src/tools/session.ts +1 -1
- package/src/tools/types.ts +7 -7
- package/src/tools.ts +9 -9
- package/src/ui/manual-dialog-render.ts +10 -3
- package/src/ui/manual-dialog.ts +4 -9
- package/src/ui/model-selector.ts +7 -6
- package/src/ui/settings-formatting.ts +2 -2
- package/src/ui/settings-items.ts +4 -4
- package/src/ui/settings-mutations.ts +2 -2
- package/src/ui/settings-selector.ts +5 -5
- package/src/ui/text-setting-submenu.ts +6 -1
- package/src/ui/types.ts +2 -2
- package/src/ui.ts +4 -4
- package/src/usage.ts +1 -6
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ScoutManifest } from "./scout-types.ts";
|
|
2
|
+
|
|
3
|
+
const prefixWithinCharBudget = (value: string, maxChars: number) => {
|
|
4
|
+
let result = "";
|
|
5
|
+
for (const character of value) {
|
|
6
|
+
if (result.length + character.length > maxChars) {
|
|
7
|
+
break;
|
|
8
|
+
}
|
|
9
|
+
result += character;
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const reconstructScoutConversation = (
|
|
15
|
+
manifest: ScoutManifest,
|
|
16
|
+
selectedIds: string[],
|
|
17
|
+
synthesis?: string,
|
|
18
|
+
maxChars = Number.MAX_SAFE_INTEGER
|
|
19
|
+
): string => {
|
|
20
|
+
const selected = new Set(selectedIds);
|
|
21
|
+
const evidence = manifest.groups
|
|
22
|
+
.filter((group) => group.required || selected.has(group.id))
|
|
23
|
+
.sort((left, right) => left.originalIndex - right.originalIndex)
|
|
24
|
+
.map((group) => group.content);
|
|
25
|
+
const evidenceText = evidence.join("\n\n");
|
|
26
|
+
if (maxChars <= 0) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
if (evidenceText.length >= maxChars) {
|
|
30
|
+
return prefixWithinCharBudget(evidenceText, maxChars);
|
|
31
|
+
}
|
|
32
|
+
const inference = synthesis?.trim()
|
|
33
|
+
? `[Scout synthesis — untrusted, non-authoritative inference; not evidence]\n${synthesis.trim()}`
|
|
34
|
+
: undefined;
|
|
35
|
+
if (!inference) {
|
|
36
|
+
return evidenceText;
|
|
37
|
+
}
|
|
38
|
+
const separator = evidenceText ? "\n\n" : "";
|
|
39
|
+
const remaining = maxChars - evidenceText.length - separator.length;
|
|
40
|
+
if (remaining <= 0) {
|
|
41
|
+
return evidenceText;
|
|
42
|
+
}
|
|
43
|
+
return `${evidenceText}${separator}${prefixWithinCharBudget(inference, remaining)}`;
|
|
44
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Shared Scout manifest types and transport limits. */
|
|
2
|
+
|
|
3
|
+
import type { AdvisorToolPolicies } from "./config/types.ts";
|
|
4
|
+
|
|
5
|
+
export const SCOUT_MANIFEST_MAX_BYTES = 64 * 1024;
|
|
6
|
+
export const SCOUT_MANIFEST_MAX_GROUPS = 64;
|
|
7
|
+
export const SCOUT_GROUP_MAX_BYTES = 24 * 1024;
|
|
8
|
+
export const SCOUT_LABEL_MAX_CHARS = 160;
|
|
9
|
+
export const SCOUT_SELECTION_MAX_IDS = 32;
|
|
10
|
+
export const SCOUT_SYNTHESIS_MAX_BYTES = 4 * 1024;
|
|
11
|
+
|
|
12
|
+
export type ScoutGroupKind =
|
|
13
|
+
| "compaction"
|
|
14
|
+
| "user"
|
|
15
|
+
| "assistant"
|
|
16
|
+
| "tool-exchange"
|
|
17
|
+
| "pending-invocation";
|
|
18
|
+
|
|
19
|
+
export interface ScoutContextGroup {
|
|
20
|
+
bytes: number;
|
|
21
|
+
content: string;
|
|
22
|
+
id: string;
|
|
23
|
+
kind: ScoutGroupKind;
|
|
24
|
+
label: string;
|
|
25
|
+
originalIndex: number;
|
|
26
|
+
required: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ScoutManifest {
|
|
30
|
+
availableBytes: number;
|
|
31
|
+
availableCount: number;
|
|
32
|
+
groups: ScoutContextGroup[];
|
|
33
|
+
omittedBytes: number;
|
|
34
|
+
omittedCount: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ScoutManifestResult =
|
|
38
|
+
| { ok: true; manifest: ScoutManifest }
|
|
39
|
+
| {
|
|
40
|
+
ok: false;
|
|
41
|
+
reason: "invalid-protocol" | "required-group-overflow";
|
|
42
|
+
message: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The invalid-protocol failure shape produced by protocol passes. */
|
|
46
|
+
export interface InvalidProtocolResult {
|
|
47
|
+
message: string;
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: "invalid-protocol";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BuildScoutManifestOptions {
|
|
53
|
+
currentInvocationId?: string;
|
|
54
|
+
/** @deprecated Use maxManifestBytes for the Scout transport budget. */
|
|
55
|
+
maxBytes?: number;
|
|
56
|
+
/** Maximum reconstructed Advisor conversation characters. */
|
|
57
|
+
maxConversationChars?: number;
|
|
58
|
+
maxGroupBytes?: number;
|
|
59
|
+
maxGroups?: number;
|
|
60
|
+
/** Maximum serialized group-manifest bytes sent to Scout. */
|
|
61
|
+
maxManifestBytes?: number;
|
|
62
|
+
policies?: AdvisorToolPolicies;
|
|
63
|
+
redact?: boolean;
|
|
64
|
+
toolResultMaxBytes?: number;
|
|
65
|
+
toolResultMaxLines?: number;
|
|
66
|
+
}
|
package/src/scout.ts
CHANGED
|
@@ -1,21 +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
|
-
import { executorEffortRef, executorRef } from "./config/state.
|
|
3
|
+
import { executorEffortRef, executorRef } from "./config/state.ts";
|
|
4
4
|
import {
|
|
5
5
|
type CollectedTextStream,
|
|
6
6
|
collectTextStream,
|
|
7
7
|
type ResolvedConfiguredModel,
|
|
8
8
|
resolveConfiguredModel,
|
|
9
|
-
} from "./model-stream.
|
|
9
|
+
} from "./model-stream.ts";
|
|
10
|
+
import { groupWire } from "./scout-groups.ts";
|
|
11
|
+
import { reconstructScoutConversation } from "./scout-reconstruct.ts";
|
|
10
12
|
import {
|
|
11
|
-
reconstructScoutConversation,
|
|
12
13
|
SCOUT_SELECTION_MAX_IDS,
|
|
13
14
|
SCOUT_SYNTHESIS_MAX_BYTES,
|
|
14
15
|
type ScoutManifest,
|
|
15
|
-
} from "./scout-
|
|
16
|
-
import { snapshotAdvisorUsage } from "./usage.
|
|
16
|
+
} from "./scout-types.ts";
|
|
17
|
+
import { snapshotAdvisorUsage } from "./usage.ts";
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
const SCOUT_TIMEOUT_MS = 30_000;
|
|
19
20
|
|
|
20
21
|
export const SCOUT_SYSTEM = [
|
|
21
22
|
"You are Scout, a context curator serving a separate engineering Advisor.",
|
|
@@ -30,7 +31,7 @@ export const SCOUT_SYSTEM = [
|
|
|
30
31
|
"Do not use Markdown fences or add any other keys or prose.",
|
|
31
32
|
].join(" ");
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
type ScoutFallbackCategory =
|
|
34
35
|
| "required-group-overflow"
|
|
35
36
|
| "invalid-protocol"
|
|
36
37
|
| "missing-model"
|
|
@@ -41,7 +42,7 @@ export type ScoutFallbackCategory =
|
|
|
41
42
|
| "invalid-selection"
|
|
42
43
|
| "timeout";
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
interface ScoutMetrics {
|
|
45
46
|
availableCount: number;
|
|
46
47
|
inputBytes: number;
|
|
47
48
|
latencyMs: number;
|
|
@@ -106,14 +107,7 @@ const manifestMessage = (manifest: ScoutManifest): Message => ({
|
|
|
106
107
|
content: [
|
|
107
108
|
{
|
|
108
109
|
text: JSON.stringify({
|
|
109
|
-
groups: manifest.groups.map(
|
|
110
|
-
bytes: group.bytes,
|
|
111
|
-
content: group.content,
|
|
112
|
-
id: group.id,
|
|
113
|
-
kind: group.kind,
|
|
114
|
-
label: group.label,
|
|
115
|
-
required: group.required,
|
|
116
|
-
})),
|
|
110
|
+
groups: manifest.groups.map(groupWire),
|
|
117
111
|
omittedBeforeScout: {
|
|
118
112
|
bytes: manifest.omittedBytes,
|
|
119
113
|
groups: manifest.omittedCount,
|
|
@@ -225,15 +219,36 @@ export const runAdvisorScout = async (
|
|
|
225
219
|
onEvent?: (event: ScoutLifecycleEvent) => void,
|
|
226
220
|
timeoutMs = SCOUT_TIMEOUT_MS,
|
|
227
221
|
dependencies: ScoutDependencies = defaultDependencies
|
|
228
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: cancellation, timeout, provider, and schema outcomes remain explicitly distinct.
|
|
229
222
|
): Promise<ScoutOutcome> => {
|
|
230
223
|
const startedAt = Date.now();
|
|
231
224
|
const publish = (event: ScoutLifecycleEvent) => {
|
|
232
225
|
onEvent?.(event);
|
|
233
226
|
};
|
|
234
|
-
|
|
227
|
+
const cancelled = (): ScoutOutcome => {
|
|
235
228
|
publish({ type: "cancelled" });
|
|
236
229
|
return { cancelled: true, ok: false };
|
|
230
|
+
};
|
|
231
|
+
const fallback = (
|
|
232
|
+
category: ScoutFallbackCategory,
|
|
233
|
+
message: string,
|
|
234
|
+
usage?: unknown
|
|
235
|
+
): ScoutOutcome => {
|
|
236
|
+
const outcome = {
|
|
237
|
+
category,
|
|
238
|
+
message,
|
|
239
|
+
metrics:
|
|
240
|
+
usage === undefined
|
|
241
|
+
? baseMetrics(manifest, startedAt)
|
|
242
|
+
: { ...baseMetrics(manifest, startedAt), usage },
|
|
243
|
+
model: executorRef,
|
|
244
|
+
ok: false as const,
|
|
245
|
+
};
|
|
246
|
+
publish({ outcome, type: "fallback" });
|
|
247
|
+
return outcome;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (parentSignal?.aborted) {
|
|
251
|
+
return cancelled();
|
|
237
252
|
}
|
|
238
253
|
|
|
239
254
|
let resolved: ResolvedConfiguredModel;
|
|
@@ -241,24 +256,14 @@ export const runAdvisorScout = async (
|
|
|
241
256
|
resolved = await dependencies.resolve(ctx, executorRef, "Scout");
|
|
242
257
|
} catch (error) {
|
|
243
258
|
if (parentSignal?.aborted) {
|
|
244
|
-
|
|
245
|
-
return { cancelled: true, ok: false };
|
|
259
|
+
return cancelled();
|
|
246
260
|
}
|
|
247
261
|
const message = error instanceof Error ? error.message : String(error);
|
|
248
|
-
|
|
249
|
-
category: classifyResolutionError(message),
|
|
250
|
-
message,
|
|
251
|
-
metrics: baseMetrics(manifest, startedAt),
|
|
252
|
-
model: executorRef,
|
|
253
|
-
ok: false as const,
|
|
254
|
-
};
|
|
255
|
-
publish({ outcome, type: "fallback" });
|
|
256
|
-
return outcome;
|
|
262
|
+
return fallback(classifyResolutionError(message), message);
|
|
257
263
|
}
|
|
258
264
|
|
|
259
265
|
if (parentSignal?.aborted) {
|
|
260
|
-
|
|
261
|
-
return { cancelled: true, ok: false };
|
|
266
|
+
return cancelled();
|
|
262
267
|
}
|
|
263
268
|
publish({ model: executorRef, type: "call" });
|
|
264
269
|
const controller = new AbortController();
|
|
@@ -280,6 +285,12 @@ export const runAdvisorScout = async (
|
|
|
280
285
|
once: true,
|
|
281
286
|
});
|
|
282
287
|
});
|
|
288
|
+
const teardown = () => {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
291
|
+
controller.signal.removeEventListener("abort", onControllerAbort);
|
|
292
|
+
};
|
|
293
|
+
|
|
283
294
|
let streamed: CollectedTextStream;
|
|
284
295
|
try {
|
|
285
296
|
const collection = dependencies.collect(resolved, {
|
|
@@ -295,30 +306,18 @@ export const runAdvisorScout = async (
|
|
|
295
306
|
});
|
|
296
307
|
streamed = await Promise.race([collection, abortPromise]);
|
|
297
308
|
} catch (error) {
|
|
298
|
-
|
|
299
|
-
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
300
|
-
controller.signal.removeEventListener("abort", onControllerAbort);
|
|
309
|
+
teardown();
|
|
301
310
|
if (parentSignal?.aborted) {
|
|
302
|
-
|
|
303
|
-
return { cancelled: true, ok: false };
|
|
311
|
+
return cancelled();
|
|
304
312
|
}
|
|
305
313
|
const message = error instanceof Error ? error.message : String(error);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
metrics: baseMetrics(manifest, startedAt),
|
|
310
|
-
model: executorRef,
|
|
311
|
-
ok: false as const,
|
|
312
|
-
};
|
|
313
|
-
publish({ outcome, type: "fallback" });
|
|
314
|
-
return outcome;
|
|
314
|
+
return timedOut
|
|
315
|
+
? fallback("timeout", `Scout timed out after ${timeoutMs} ms.`)
|
|
316
|
+
: fallback("provider-error", message);
|
|
315
317
|
}
|
|
316
|
-
|
|
317
|
-
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
318
|
-
controller.signal.removeEventListener("abort", onControllerAbort);
|
|
318
|
+
teardown();
|
|
319
319
|
if (parentSignal?.aborted) {
|
|
320
|
-
|
|
321
|
-
return { cancelled: true, ok: false };
|
|
320
|
+
return cancelled();
|
|
322
321
|
}
|
|
323
322
|
|
|
324
323
|
let selection: ScoutSelection;
|
|
@@ -326,20 +325,11 @@ export const runAdvisorScout = async (
|
|
|
326
325
|
selection = parseScoutSelection(streamed.text, manifest);
|
|
327
326
|
} catch (error) {
|
|
328
327
|
const message = error instanceof Error ? error.message : String(error);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
? ("invalid-selection" as const)
|
|
332
|
-
: ("empty-response" as const),
|
|
328
|
+
return fallback(
|
|
329
|
+
streamed.text.trim() ? "invalid-selection" : "empty-response",
|
|
333
330
|
message,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
usage: snapshotAdvisorUsage(streamed.usage),
|
|
337
|
-
},
|
|
338
|
-
model: executorRef,
|
|
339
|
-
ok: false as const,
|
|
340
|
-
};
|
|
341
|
-
publish({ outcome, type: "fallback" });
|
|
342
|
-
return outcome;
|
|
331
|
+
snapshotAdvisorUsage(streamed.usage)
|
|
332
|
+
);
|
|
343
333
|
}
|
|
344
334
|
|
|
345
335
|
const outcome = {
|
package/src/session-state.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
emptyAdvisorUsageTotals,
|
|
5
5
|
formatAdvisorUsageStatus,
|
|
6
6
|
formatAdvisorUsageTotals,
|
|
7
|
-
} from "./usage.
|
|
7
|
+
} from "./usage.ts";
|
|
8
8
|
|
|
9
9
|
export type GateDecision = "proceed" | "revise" | "blocked";
|
|
10
10
|
export type ConsultationTrigger = "manual" | "executor-requested";
|
|
@@ -12,8 +12,8 @@ export type GateTrigger =
|
|
|
12
12
|
| "repeated-tool-call"
|
|
13
13
|
| "completion-review"
|
|
14
14
|
| "custom-rule";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
type AdvisorTrigger = ConsultationTrigger | GateTrigger;
|
|
16
|
+
type ExecutionEffect = "continued" | "tool-blocked" | "session-blocked";
|
|
17
17
|
|
|
18
18
|
export interface AdvisorInvocationRecord {
|
|
19
19
|
cost?: number;
|
|
@@ -113,46 +113,64 @@ export const normalizeToolInput = (
|
|
|
113
113
|
export const normalizedToolSignature = (toolName: string, input: unknown) =>
|
|
114
114
|
`${toolName}:${JSON.stringify(normalizeToolInput(toolName, input))}`;
|
|
115
115
|
|
|
116
|
+
interface RepetitionState {
|
|
117
|
+
count: number;
|
|
118
|
+
interventions: number;
|
|
119
|
+
previousSignature?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface AdviceLedger {
|
|
123
|
+
draftConsultations: number;
|
|
124
|
+
issued: Map<string, { advice: string; trigger: ConsultationTrigger }>;
|
|
125
|
+
lastAdvice?: string;
|
|
126
|
+
outcomes: number;
|
|
127
|
+
pending: Set<string>;
|
|
128
|
+
reported: Set<string>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface UsageState {
|
|
132
|
+
invocations: AdvisorInvocationRecord[];
|
|
133
|
+
totals: AdvisorUsageTotals;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const freshRepetition = (): RepetitionState => ({
|
|
137
|
+
count: 0,
|
|
138
|
+
interventions: 0,
|
|
139
|
+
});
|
|
140
|
+
const freshAdviceLedger = (): AdviceLedger => ({
|
|
141
|
+
draftConsultations: 0,
|
|
142
|
+
issued: new Map(),
|
|
143
|
+
outcomes: 0,
|
|
144
|
+
pending: new Set(),
|
|
145
|
+
reported: new Set(),
|
|
146
|
+
});
|
|
147
|
+
const freshUsage = (): UsageState => ({
|
|
148
|
+
invocations: [],
|
|
149
|
+
totals: emptyAdvisorUsageTotals(),
|
|
150
|
+
});
|
|
151
|
+
|
|
116
152
|
export class AdvisorSessionState {
|
|
117
|
-
#
|
|
118
|
-
#repetitions = 0;
|
|
153
|
+
#repetition = freshRepetition();
|
|
119
154
|
#blockedReason?: string;
|
|
120
|
-
#
|
|
121
|
-
#
|
|
155
|
+
#ledger = freshAdviceLedger();
|
|
156
|
+
#usage = freshUsage();
|
|
122
157
|
#consumedCalls = 0;
|
|
123
|
-
readonly #issuedAdvice = new Map<
|
|
124
|
-
string,
|
|
125
|
-
{ advice: string; trigger: ConsultationTrigger }
|
|
126
|
-
>();
|
|
127
|
-
readonly #pendingAdvice = new Set<string>();
|
|
128
|
-
readonly #reportedAdvice = new Set<string>();
|
|
129
|
-
#draftConsultations = 0;
|
|
130
|
-
#outcomes = 0;
|
|
131
|
-
#lastAdvice?: string;
|
|
132
|
-
#usage = emptyAdvisorUsageTotals();
|
|
133
158
|
|
|
134
159
|
resetTask() {
|
|
135
|
-
this.#
|
|
136
|
-
this.#repetitions = 0;
|
|
160
|
+
this.#repetition = freshRepetition();
|
|
137
161
|
this.#blockedReason = undefined;
|
|
138
|
-
this.#
|
|
139
|
-
this.#
|
|
162
|
+
this.#ledger = freshAdviceLedger();
|
|
163
|
+
this.#usage = freshUsage();
|
|
140
164
|
this.#consumedCalls = 0;
|
|
141
|
-
this.#issuedAdvice.clear();
|
|
142
|
-
this.#pendingAdvice.clear();
|
|
143
|
-
this.#reportedAdvice.clear();
|
|
144
|
-
this.#draftConsultations = 0;
|
|
145
|
-
this.#outcomes = 0;
|
|
146
|
-
this.#lastAdvice = undefined;
|
|
147
|
-
this.#usage = emptyAdvisorUsageTotals();
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
clearBlocked() {
|
|
151
168
|
this.#blockedReason = undefined;
|
|
152
169
|
}
|
|
153
170
|
resetRepetition() {
|
|
154
|
-
this
|
|
155
|
-
this.#
|
|
171
|
+
// Cumulative interventions feed the session summary and must survive this reset.
|
|
172
|
+
this.#repetition.count = 0;
|
|
173
|
+
this.#repetition.previousSignature = undefined;
|
|
156
174
|
}
|
|
157
175
|
get blocked() {
|
|
158
176
|
return this.#blockedReason !== undefined;
|
|
@@ -169,13 +187,15 @@ export class AdvisorSessionState {
|
|
|
169
187
|
return false;
|
|
170
188
|
}
|
|
171
189
|
const signature = normalizedToolSignature(toolName, input);
|
|
172
|
-
this.#
|
|
173
|
-
signature === this.#previousSignature
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
this.#repetition.count =
|
|
191
|
+
signature === this.#repetition.previousSignature
|
|
192
|
+
? this.#repetition.count + 1
|
|
193
|
+
: 1;
|
|
194
|
+
this.#repetition.previousSignature = signature;
|
|
195
|
+
if (this.#repetition.count < threshold) {
|
|
176
196
|
return false;
|
|
177
197
|
}
|
|
178
|
-
this.#
|
|
198
|
+
this.#repetition.interventions += 1;
|
|
179
199
|
return true;
|
|
180
200
|
}
|
|
181
201
|
|
|
@@ -196,17 +216,17 @@ export class AdvisorSessionState {
|
|
|
196
216
|
|
|
197
217
|
/** Returns a copy of cumulative direct Advisor usage for this session. */
|
|
198
218
|
get usageTotals(): AdvisorUsageTotals {
|
|
199
|
-
return { ...this.#usage };
|
|
219
|
+
return { ...this.#usage.totals };
|
|
200
220
|
}
|
|
201
221
|
|
|
202
222
|
/** Returns the footer-ready direct Advisor usage status for this session. */
|
|
203
223
|
usageStatus() {
|
|
204
|
-
return formatAdvisorUsageStatus(this.#usage);
|
|
224
|
+
return formatAdvisorUsageStatus(this.#usage.totals);
|
|
205
225
|
}
|
|
206
226
|
|
|
207
227
|
recordInvocation(record: AdvisorInvocationRecord) {
|
|
208
|
-
this.#invocations.push(record);
|
|
209
|
-
addAdvisorUsage(this.#usage, record.usage);
|
|
228
|
+
this.#usage.invocations.push(record);
|
|
229
|
+
addAdvisorUsage(this.#usage.totals, record.usage);
|
|
210
230
|
}
|
|
211
231
|
issueAdvice(
|
|
212
232
|
id: string,
|
|
@@ -214,14 +234,15 @@ export class AdvisorSessionState {
|
|
|
214
234
|
trigger: ConsultationTrigger,
|
|
215
235
|
draft = false
|
|
216
236
|
) {
|
|
217
|
-
this.#
|
|
218
|
-
this.#lastAdvice = advice;
|
|
237
|
+
this.#ledger.issued.set(id, { advice, trigger });
|
|
238
|
+
this.#ledger.lastAdvice = advice;
|
|
219
239
|
if (draft) {
|
|
220
|
-
this.#draftConsultations += 1;
|
|
240
|
+
this.#ledger.draftConsultations += 1;
|
|
221
241
|
}
|
|
222
242
|
}
|
|
223
243
|
claimTrackedFiles(paths: string[]) {
|
|
224
|
-
|
|
244
|
+
const advice = this.#ledger.lastAdvice;
|
|
245
|
+
if (!advice || paths.length === 0) {
|
|
225
246
|
return false;
|
|
226
247
|
}
|
|
227
248
|
const mentioned = paths.every((path) => {
|
|
@@ -230,38 +251,38 @@ export class AdvisorSessionState {
|
|
|
230
251
|
"(^|[\\s\\\"'`()\\[])" +
|
|
231
252
|
escaped +
|
|
232
253
|
"(?=$|[\\s\\\"'`),;:!?\\]]|\\.(?=\\s|$))";
|
|
233
|
-
return new RegExp(boundary).test(
|
|
254
|
+
return new RegExp(boundary).test(advice);
|
|
234
255
|
});
|
|
235
256
|
if (!mentioned) {
|
|
236
257
|
return false;
|
|
237
258
|
}
|
|
238
|
-
this.#lastAdvice = undefined;
|
|
259
|
+
this.#ledger.lastAdvice = undefined;
|
|
239
260
|
return true;
|
|
240
261
|
}
|
|
241
262
|
|
|
242
263
|
reserveAdvice(id: string) {
|
|
243
|
-
if (this.#
|
|
264
|
+
if (this.#ledger.reported.has(id) || this.#ledger.pending.has(id)) {
|
|
244
265
|
return;
|
|
245
266
|
}
|
|
246
|
-
const advice = this.#
|
|
267
|
+
const advice = this.#ledger.issued.get(id);
|
|
247
268
|
if (!advice) {
|
|
248
269
|
return;
|
|
249
270
|
}
|
|
250
|
-
this.#
|
|
271
|
+
this.#ledger.pending.add(id);
|
|
251
272
|
return advice;
|
|
252
273
|
}
|
|
253
274
|
|
|
254
275
|
commitAdvice(id: string) {
|
|
255
|
-
if (!this.#
|
|
276
|
+
if (!this.#ledger.pending.delete(id)) {
|
|
256
277
|
return false;
|
|
257
278
|
}
|
|
258
|
-
this.#
|
|
259
|
-
this.#outcomes += 1;
|
|
279
|
+
this.#ledger.reported.add(id);
|
|
280
|
+
this.#ledger.outcomes += 1;
|
|
260
281
|
return true;
|
|
261
282
|
}
|
|
262
283
|
|
|
263
284
|
releaseAdvice(id: string) {
|
|
264
|
-
this.#
|
|
285
|
+
this.#ledger.pending.delete(id);
|
|
265
286
|
}
|
|
266
287
|
|
|
267
288
|
/** Compatibility helper for synchronous callers that can commit immediately. */
|
|
@@ -274,17 +295,11 @@ export class AdvisorSessionState {
|
|
|
274
295
|
return advice;
|
|
275
296
|
}
|
|
276
297
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
const markdown = this.#invocations.filter(
|
|
282
|
-
(item) => item.kind === "markdown"
|
|
298
|
+
#decisionsLine() {
|
|
299
|
+
const gates = this.#usage.invocations.filter(
|
|
300
|
+
(item) => item.kind === "gate"
|
|
283
301
|
);
|
|
284
|
-
|
|
285
|
-
const countTrigger = (trigger: AdvisorTrigger) =>
|
|
286
|
-
this.#invocations.filter((item) => item.trigger === trigger).length;
|
|
287
|
-
const decisions =
|
|
302
|
+
return (
|
|
288
303
|
(["proceed", "revise", "blocked"] as GateDecision[])
|
|
289
304
|
.map(
|
|
290
305
|
(decision) =>
|
|
@@ -295,32 +310,60 @@ export class AdvisorSessionState {
|
|
|
295
310
|
)
|
|
296
311
|
.filter(([, count]) => count > 0)
|
|
297
312
|
.map(([decision, count]) => `${count} ${decision}`)
|
|
298
|
-
.join(", ") || "none"
|
|
313
|
+
.join(", ") || "none"
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#countTrigger(trigger: AdvisorTrigger) {
|
|
318
|
+
return this.#usage.invocations.filter((item) => item.trigger === trigger)
|
|
319
|
+
.length;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#triggersLine() {
|
|
323
|
+
return (
|
|
324
|
+
[
|
|
325
|
+
"manual",
|
|
326
|
+
"executor-requested",
|
|
327
|
+
"repeated-tool-call",
|
|
328
|
+
"completion-review",
|
|
329
|
+
"custom-rule",
|
|
330
|
+
]
|
|
331
|
+
.filter((trigger) => this.#countTrigger(trigger as AdvisorTrigger) > 0)
|
|
332
|
+
.join(", ") || "none"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
summary(limit: number | undefined) {
|
|
337
|
+
const { invocations, totals } = this.#usage;
|
|
338
|
+
if (invocations.length === 0 && this.#repetition.interventions === 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const markdown = invocations.filter((item) => item.kind === "markdown");
|
|
342
|
+
const gates = invocations.filter((item) => item.kind === "gate");
|
|
299
343
|
const effects = (effect: ExecutionEffect) =>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const failures = this.#invocations
|
|
344
|
+
invocations.filter((item) => item.executionEffect === effect).length;
|
|
345
|
+
const failures = invocations
|
|
303
346
|
.filter((item) => item.failure)
|
|
304
347
|
.map((item) => item.failure);
|
|
305
348
|
const models =
|
|
306
|
-
[
|
|
307
|
-
|
|
308
|
-
|
|
349
|
+
[...new Set(invocations.map((item) => item.model).filter(Boolean))].join(
|
|
350
|
+
", "
|
|
351
|
+
) || "unknown";
|
|
309
352
|
const budget =
|
|
310
353
|
limit === undefined
|
|
311
354
|
? `${this.#consumedCalls} used; unlimited remaining`
|
|
312
355
|
: `${this.#consumedCalls} / ${limit} used; ${Math.max(0, limit - this.#consumedCalls)} remaining`;
|
|
313
356
|
return [
|
|
314
357
|
"[Session Advisor Summary]",
|
|
315
|
-
`Consultations: ${markdown.length} Markdown (${countTrigger("manual")} manual, ${countTrigger("executor-requested")} executor-requested), automatic gates: ${gates.length}`,
|
|
316
|
-
`Triggers: ${
|
|
358
|
+
`Consultations: ${markdown.length} Markdown (${this.#countTrigger("manual")} manual, ${this.#countTrigger("executor-requested")} executor-requested), automatic gates: ${gates.length}`,
|
|
359
|
+
`Triggers: ${this.#triggersLine()}`,
|
|
317
360
|
`Models: ${models}`,
|
|
318
361
|
`Budget: ${budget}`,
|
|
319
|
-
`Usage: ${formatAdvisorUsageTotals(
|
|
320
|
-
`Markdown advice: ${markdown.length} responses (${this.#draftConsultations} with drafts)`,
|
|
321
|
-
`Outcome reports: ${this.#outcomes}`,
|
|
322
|
-
`Gate decisions: ${
|
|
323
|
-
`Loop matching: normalized tool signatures; ${this.#
|
|
362
|
+
`Usage: ${formatAdvisorUsageTotals(totals)}`,
|
|
363
|
+
`Markdown advice: ${markdown.length} responses (${this.#ledger.draftConsultations} with drafts)`,
|
|
364
|
+
`Outcome reports: ${this.#ledger.outcomes}`,
|
|
365
|
+
`Gate decisions: ${this.#decisionsLine()}`,
|
|
366
|
+
`Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
|
|
324
367
|
`Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,
|
|
325
368
|
`Failures: ${failures.length ? failures.join(", ") : "none"}`,
|
|
326
369
|
].join("\n");
|