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.
Files changed (76) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +1 -1
  3. package/dist/index.js +5786 -0
  4. package/extensions/index.ts +9 -9
  5. package/package.json +16 -10
  6. package/src/{untracked.ts → attachments.ts} +2 -5
  7. package/src/commands/activation-preparation.ts +6 -6
  8. package/src/commands/activation.ts +7 -7
  9. package/src/commands/lifecycle.ts +5 -5
  10. package/src/commands/manual-command.ts +13 -9
  11. package/src/commands/manual-consultation.ts +8 -8
  12. package/src/commands/manual-progress.ts +3 -3
  13. package/src/commands/model-commands.ts +6 -6
  14. package/src/commands/model-options.ts +4 -4
  15. package/src/commands/model-picker.ts +2 -2
  16. package/src/commands/registration.ts +8 -8
  17. package/src/commands/renderers.ts +5 -5
  18. package/src/commands/runtime.ts +8 -8
  19. package/src/commands/settings-commands.ts +8 -8
  20. package/src/commands/settings-persistence.ts +4 -4
  21. package/src/commands/types.ts +5 -13
  22. package/src/commands.ts +2 -2
  23. package/src/config/args.ts +3 -3
  24. package/src/config/defaults.ts +3 -3
  25. package/src/config/schema.ts +322 -0
  26. package/src/config/state.ts +2 -2
  27. package/src/config/storage.ts +113 -103
  28. package/src/config/types.ts +1 -1
  29. package/src/config/validation.ts +61 -181
  30. package/src/config.ts +7 -7
  31. package/src/content-utils.ts +30 -0
  32. package/src/conversation.ts +13 -168
  33. package/src/git.ts +4 -18
  34. package/src/herdr.ts +6 -8
  35. package/src/model-stream.ts +5 -8
  36. package/src/outcomes.ts +2 -5
  37. package/src/pi-settings.ts +59 -0
  38. package/src/preferences.ts +2 -2
  39. package/src/redaction.ts +50 -0
  40. package/src/scout-context.ts +121 -485
  41. package/src/scout-curation.ts +84 -0
  42. package/src/scout-groups.ts +485 -0
  43. package/src/scout-protocol.ts +122 -0
  44. package/src/scout-reconstruct.ts +44 -0
  45. package/src/scout-types.ts +66 -0
  46. package/src/scout.ts +53 -63
  47. package/src/session-state.ts +119 -76
  48. package/src/tool-result-cap.ts +88 -0
  49. package/src/tools/consult-context.ts +155 -0
  50. package/src/tools/consultation.ts +71 -220
  51. package/src/tools/gate-policy.ts +5 -5
  52. package/src/tools/gate-protocol.ts +2 -2
  53. package/src/tools/loop-gate.ts +70 -61
  54. package/src/tools/prompts.ts +4 -7
  55. package/src/tools/register-ask-advisor.ts +9 -14
  56. package/src/tools/register-lifecycle.ts +7 -7
  57. package/src/tools/register-outcome.ts +5 -5
  58. package/src/tools/register-renderers.ts +5 -5
  59. package/src/tools/registration.ts +10 -10
  60. package/src/tools/render-advisor-result.ts +11 -8
  61. package/src/tools/render-common.ts +25 -13
  62. package/src/tools/scout-status.ts +5 -5
  63. package/src/tools/session.ts +1 -1
  64. package/src/tools/types.ts +7 -7
  65. package/src/tools.ts +9 -9
  66. package/src/ui/manual-dialog-render.ts +10 -3
  67. package/src/ui/manual-dialog.ts +4 -9
  68. package/src/ui/model-selector.ts +7 -6
  69. package/src/ui/settings-formatting.ts +2 -2
  70. package/src/ui/settings-items.ts +4 -4
  71. package/src/ui/settings-mutations.ts +2 -2
  72. package/src/ui/settings-selector.ts +5 -5
  73. package/src/ui/text-setting-submenu.ts +6 -1
  74. package/src/ui/types.ts +2 -2
  75. package/src/ui.ts +4 -4
  76. 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.js";
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.js";
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-context.js";
16
- import { snapshotAdvisorUsage } from "./usage.js";
16
+ } from "./scout-types.ts";
17
+ import { snapshotAdvisorUsage } from "./usage.ts";
17
18
 
18
- export const SCOUT_TIMEOUT_MS = 30_000;
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
- export type ScoutFallbackCategory =
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
- export interface ScoutMetrics {
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((group) => ({
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
- if (parentSignal?.aborted) {
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
- publish({ type: "cancelled" });
245
- return { cancelled: true, ok: false };
259
+ return cancelled();
246
260
  }
247
261
  const message = error instanceof Error ? error.message : String(error);
248
- const outcome = {
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
- publish({ type: "cancelled" });
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
- clearTimeout(timer);
299
- parentSignal?.removeEventListener("abort", abortFromParent);
300
- controller.signal.removeEventListener("abort", onControllerAbort);
309
+ teardown();
301
310
  if (parentSignal?.aborted) {
302
- publish({ type: "cancelled" });
303
- return { cancelled: true, ok: false };
311
+ return cancelled();
304
312
  }
305
313
  const message = error instanceof Error ? error.message : String(error);
306
- const outcome = {
307
- category: timedOut ? ("timeout" as const) : ("provider-error" as const),
308
- message: timedOut ? `Scout timed out after ${timeoutMs} ms.` : message,
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
- clearTimeout(timer);
317
- parentSignal?.removeEventListener("abort", abortFromParent);
318
- controller.signal.removeEventListener("abort", onControllerAbort);
318
+ teardown();
319
319
  if (parentSignal?.aborted) {
320
- publish({ type: "cancelled" });
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
- const outcome = {
330
- category: streamed.text.trim()
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
- metrics: {
335
- ...baseMetrics(manifest, startedAt),
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 = {
@@ -4,7 +4,7 @@ import {
4
4
  emptyAdvisorUsageTotals,
5
5
  formatAdvisorUsageStatus,
6
6
  formatAdvisorUsageTotals,
7
- } from "./usage.js";
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
- export type AdvisorTrigger = ConsultationTrigger | GateTrigger;
16
- export type ExecutionEffect = "continued" | "tool-blocked" | "session-blocked";
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
- #previousSignature?: string;
118
- #repetitions = 0;
153
+ #repetition = freshRepetition();
119
154
  #blockedReason?: string;
120
- #invocations: AdvisorInvocationRecord[] = [];
121
- #loopInterventions = 0;
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.#previousSignature = undefined;
136
- this.#repetitions = 0;
160
+ this.#repetition = freshRepetition();
137
161
  this.#blockedReason = undefined;
138
- this.#invocations = [];
139
- this.#loopInterventions = 0;
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.#previousSignature = undefined;
155
- this.#repetitions = 0;
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.#repetitions =
173
- signature === this.#previousSignature ? this.#repetitions + 1 : 1;
174
- this.#previousSignature = signature;
175
- if (this.#repetitions < threshold) {
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.#loopInterventions += 1;
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.#issuedAdvice.set(id, { advice, trigger });
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
- if (!this.#lastAdvice || paths.length === 0) {
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(this.#lastAdvice as string);
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.#reportedAdvice.has(id) || this.#pendingAdvice.has(id)) {
264
+ if (this.#ledger.reported.has(id) || this.#ledger.pending.has(id)) {
244
265
  return;
245
266
  }
246
- const advice = this.#issuedAdvice.get(id);
267
+ const advice = this.#ledger.issued.get(id);
247
268
  if (!advice) {
248
269
  return;
249
270
  }
250
- this.#pendingAdvice.add(id);
271
+ this.#ledger.pending.add(id);
251
272
  return advice;
252
273
  }
253
274
 
254
275
  commitAdvice(id: string) {
255
- if (!this.#pendingAdvice.delete(id)) {
276
+ if (!this.#ledger.pending.delete(id)) {
256
277
  return false;
257
278
  }
258
- this.#reportedAdvice.add(id);
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.#pendingAdvice.delete(id);
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
- summary(limit: number | undefined) {
278
- if (this.#invocations.length === 0 && this.#loopInterventions === 0) {
279
- return;
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
- const gates = this.#invocations.filter((item) => item.kind === "gate");
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
- this.#invocations.filter((item) => item.executionEffect === effect)
301
- .length;
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
- ...new Set(this.#invocations.map((item) => item.model).filter(Boolean)),
308
- ].join(", ") || "unknown";
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: ${["manual", "executor-requested", "repeated-tool-call", "completion-review", "custom-rule"].filter((trigger) => countTrigger(trigger as AdvisorTrigger) > 0).join(", ") || "none"}`,
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(this.#usage)}`,
320
- `Markdown advice: ${markdown.length} responses (${this.#draftConsultations} with drafts)`,
321
- `Outcome reports: ${this.#outcomes}`,
322
- `Gate decisions: ${decisions}`,
323
- `Loop matching: normalized tool signatures; ${this.#loopInterventions} gate intervention${this.#loopInterventions === 1 ? "" : "s"}`,
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");