pi-advisor-flow 0.3.1 → 0.3.4

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 CHANGED
@@ -4,6 +4,37 @@ All notable changes to this project are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.3.4
8
+
9
+ ### Changed
10
+
11
+ - Updated the development toolchain and Pi compatibility test matrix to the current supported patch releases.
12
+ - Added pull-request validation and made release tags wait for frozen-install, typecheck, test, lint, package, and security-audit checks.
13
+ - Kept Bun security auditing outside the publish workflow so an audit failure prevents a new tag instead of invalidating an existing release tag.
14
+ - Added a package allowlist check for the published tarball and retained npm provenance publishing.
15
+
16
+ ### Security
17
+
18
+ - Confirmed the Socket URL-string findings for `advisor-preferences.md` and `notification.show` are intentional command/path identifiers, not network endpoints.
19
+ - Retained the constrained filesystem access required for explicit repository-file handoff; no new Socket issues were reported for 0.3.3.
20
+
21
+ ## 0.3.3
22
+
23
+ ### Added
24
+
25
+ - Added **EXPERIMENTAL** Advisor Scout, an opt-in conversation curator that uses the configured Executor model before every Advisor invocation. It keeps selected evidence verbatim, labels its synthesis as untrusted inference, reports separate usage and latency, and falls back to the original context without changing Advisor or gate safety behavior. The experiment adapts the context-boundary idea from the [FastContext paper](https://arxiv.org/html/2606.14066v1); it is not the paper's repository explorer and does not claim its reported effect size.
26
+
27
+ ### Fixed
28
+
29
+ - Kept Scout history within the Advisor's remaining context budget, including zero-budget behavior, and kept pending Advisor drafts and attachment paths outside Scout's input.
30
+ - Included the current repeated tool invocation in automatic-gate Scout context.
31
+
32
+ ## 0.3.2
33
+
34
+ ### Release status
35
+
36
+ - Not published because the release validation workflow failed three tests before publication.
37
+
7
38
  ## 0.3.1
8
39
 
9
40
  ### Changed
package/README.md CHANGED
@@ -20,10 +20,11 @@ The idea is simple: keep implementation on a fast model and borrow frontier reas
20
20
  - **Separate model and reasoning controls** for the Executor and Advisor.
21
21
  - **Privacy controls** for conversation history, repository context, explicit tracked/untracked file handoff, tool results, secret redaction, and outcome logging.
22
22
  - **Optional persistent activation, Simple mode, session summaries, and Herdr integration.**
23
+ - **EXPERIMENTAL Advisor Scout** that uses the configured Executor model to curate conversation evidence before every Advisor call.
23
24
 
24
25
  ## Install
25
26
 
26
- Current release: **0.3.1**. Requires Pi 0.84.1 or later and is compatible with Herdr 0.8.0. The extension installs no dependencies of its own; Pi supplies its runtime modules.
27
+ Current release: **0.3.4**. Requires Pi 0.84.1 or later and is compatible with Herdr 0.8.0. The extension installs no dependencies of its own; Pi supplies its runtime modules.
27
28
 
28
29
  ```bash
29
30
  # npm
@@ -56,13 +57,26 @@ You can also enable the flow and select both models at once:
56
57
 
57
58
  1. The Executor investigates the task and forms its own candidate direction.
58
59
  2. For a consequential decision, stalled attempt, or final review, it calls `ask_advisor` with the reconstructed conversation and allowed repository context.
59
- 3. The Advisor returns a concise review. It may challenge assumptions, identify risks, or recommend the next verification step.
60
- 4. The Executor decides what to adopt, performs the work, and validates the result.
60
+ 3. When Experimental Advisor Scout is enabled, the configured Executor model selects relevant conversation groups and writes a short, explicitly untrusted synthesis.
61
+ 4. The Advisor receives selected verbatim evidence, required current-request context, and the unchanged deterministic repository, preference, draft, and attachment regions.
62
+ 5. The Executor decides what to adopt, performs the work, and validates the result.
61
63
 
62
64
  A normal consultation never blocks execution. The optional automatic loop gate is different: it evaluates repeated tool calls and applies the configured failure policy when the Advisor says to revise, reports a block, is unavailable, or returns an invalid decision.
63
65
 
64
66
  Successful calls return an opaque `adviceId`. If global outcome logging is enabled, the Executor can call `record_advisor_outcome` once to record whether the advice was adopted and whether final validation passed.
65
67
 
68
+ ### Experimental Advisor Scout
69
+
70
+ Experimental Advisor Scout is off by default. Enable `Experimental Advisor Scout` in the advanced `/advisor-settings` screen or set `"advisorScoutEnabled": true` in the global `advisor.json`.
71
+
72
+ Scout runs before `ask_advisor`, `/advisor-manual`, and automatic Advisor gates. It uses the configured Executor model and Executor reasoning effort in a separate model call. This adds cost and latency. The compact result shows the model, selection counts, and elapsed time; `Ctrl+O` shows bounded selected labels and the synthesis.
73
+
74
+ Scout receives a bounded manifest of conversation and tool-history groups after the normal tool disclosure, result-cap, and redaction policies are applied. The manifest and reconstructed Scout conversation share the Advisor's remaining context budget after repository context; a zero remaining budget produces no history groups. For a pending `ask_advisor` call, Scout receives only the allowlisted question and Git-context preference, never the draft or explicit attachment paths. Scout does not receive the deterministic Git context, draft, project preferences, or explicit tracked and untracked attachments. Those regions are appended later through their existing consent and cap rules.
75
+
76
+ A Scout timeout, provider error, missing model or authentication, or invalid response produces a visible fallback. The Advisor then receives the original uncurated conversation. Cancelling the parent operation stops both Scout and Advisor work and does not start fallback. Scout usage is displayed separately and does not spend an Advisor call from the session budget.
77
+
78
+ This experiment adapts the context-boundary idea from Zhang et al., ["FastContext: Training Efficient Repository Explorer for Coding Agents"](https://arxiv.org/html/2606.14066v1). It is not a reproduction of FastContext. The paper describes an on-demand repository explorer with read, glob, and grep tools. pi-advisor Scout curates conversation history only, and the paper's reported effect sizes do not apply to this feature.
79
+
66
80
  ## Commands
67
81
 
68
82
  | Command | Purpose |
@@ -79,6 +93,8 @@ The Executor calls `ask_advisor({})` for a general review. It can pass a targete
79
93
 
80
94
  Advisor context can include user messages, tool calls, tool results, and repository information. Secret redaction is off by default, and tools without an explicit disclosure policy default to full context. Review the privacy settings before using the extension with sensitive work.
81
95
 
96
+ When Experimental Advisor Scout is enabled, the Executor model provider also receives bounded Advisor-eligible conversation history. Scout does not receive the deterministic repository, draft, preference, or explicit-file regions described below.
97
+
82
98
  Repository context is configurable from no access through changed-file summaries to a capped patch. When context is disabled or its budget is zero, the Advisor is told it was withheld rather than shown an apparently clean tree. Explicit tracked and untracked file contents require separate global opt-ins; attachments are capped, redacted when configured, and sent as untrusted data.
83
99
 
84
100
  ## Documentation
@@ -7,6 +7,7 @@ import {
7
7
  parseAutomaticDecision as parseAutomaticDecisionImplementation,
8
8
  registerAdvisorTool,
9
9
  runAdvisorGate as runAdvisorGateImplementation,
10
+ ScoutStatusManager,
10
11
  } from "../src/tools.js";
11
12
 
12
13
  export type { AdvisorConfig, GateFailureMode } from "../src/config.js";
@@ -31,9 +32,10 @@ export const runAdvisorGate = (
31
32
 
32
33
  export default function (pi: ExtensionAPI) {
33
34
  const sessionState = new AdvisorSessionState();
35
+ const scoutStatus = new ScoutStatusManager();
34
36
  setHerdrBlockedEmitter((active, label) =>
35
37
  pi.events.emit("herdr:blocked", { active, label })
36
38
  );
37
- registerAdvisorTool(pi, sessionState);
38
- registerCommands(pi, { sessionState });
39
+ registerAdvisorTool(pi, sessionState, { statusManager: scoutStatus });
40
+ registerCommands(pi, { sessionState, statusManager: scoutStatus });
39
41
  }
package/package.json CHANGED
@@ -1,24 +1,60 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.3.1",
4
- "author": "Philip Brembeck",
3
+ "version": "0.3.4",
4
+ "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "pi-coding-agent",
9
+ "advisor",
10
+ "pi-advisor",
11
+ "herdr"
12
+ ],
13
+ "homepage": "https://github.com/philipbrembeck/pi-advisor",
5
14
  "repository": {
6
15
  "type": "git",
7
16
  "url": "https://github.com/philipbrembeck/pi-advisor.git"
8
17
  },
18
+ "license": "MIT",
19
+ "author": "Philip Brembeck",
20
+ "type": "module",
9
21
  "main": "extensions/index.ts",
22
+ "types": "extensions/index.ts",
23
+ "files": [
24
+ "extensions",
25
+ "src",
26
+ "CHANGELOG.md",
27
+ "LICENSE",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "format": "bunx ultracite fix --linter-enabled=false",
32
+ "lint": "bunx ultracite check",
33
+ "lint:fix": "bunx ultracite fix",
34
+ "package:check": "node scripts/check-package.mjs",
35
+ "prepare": "husky",
36
+ "test": "bun test",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "lint-staged": {
40
+ "*.{json,jsonc,ts}": "bun run lint:fix --"
41
+ },
42
+ "overrides": {
43
+ "brace-expansion": "5.0.9",
44
+ "undici": "8.9.0"
45
+ },
10
46
  "devDependencies": {
11
- "@biomejs/biome": "2.5.3",
12
- "@earendil-works/pi-ai": "^0.84.1",
13
- "@earendil-works/pi-coding-agent": "^0.84.1",
14
- "@earendil-works/pi-tui": "^0.84.1",
47
+ "@biomejs/biome": "2.5.8",
48
+ "@earendil-works/pi-ai": "^0.84.2",
49
+ "@earendil-works/pi-coding-agent": "^0.84.2",
50
+ "@earendil-works/pi-tui": "^0.84.2",
15
51
  "@types/node": "^20.11.0",
16
52
  "bun-types": "^1.0.0",
17
53
  "husky": "^9.1.7",
18
- "lint-staged": "^17.1.0",
19
- "typebox": "^1.1.38",
54
+ "lint-staged": "^17.3.0",
55
+ "typebox": "^1.3.14",
20
56
  "typescript": "^5.3.3",
21
- "ultracite": "7.9.4"
57
+ "ultracite": "7.10.4"
22
58
  },
23
59
  "peerDependencies": {
24
60
  "@earendil-works/pi-ai": "^0.84.1",
@@ -40,46 +76,10 @@
40
76
  "optional": true
41
77
  }
42
78
  },
43
- "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
44
- "files": [
45
- "extensions",
46
- "src",
47
- "CHANGELOG.md",
48
- "LICENSE",
49
- "README.md"
50
- ],
51
- "homepage": "https://github.com/philipbrembeck/pi-advisor",
52
- "keywords": [
53
- "pi-package",
54
- "pi-extension",
55
- "pi-coding-agent",
56
- "advisor",
57
- "pi-advisor",
58
- "herdr"
59
- ],
60
- "license": "MIT",
61
- "lint-staged": {
62
- "*.{json,jsonc,ts}": "bun run lint:fix --"
63
- },
64
- "overrides": {
65
- "brace-expansion": "5.0.9",
66
- "undici": "8.9.0"
67
- },
68
79
  "pi": {
69
80
  "extensions": [
70
81
  "./extensions/index.ts"
71
82
  ],
72
83
  "image": "https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/hero.png"
73
- },
74
- "scripts": {
75
- "test": "bun test",
76
- "typecheck": "tsc --noEmit",
77
- "format": "bunx ultracite fix --linter-enabled=false",
78
- "lint": "bunx ultracite check",
79
- "lint:fix": "bunx ultracite fix",
80
- "package:check": "npm pack --dry-run --json >/dev/null",
81
- "prepare": "husky"
82
- },
83
- "type": "module",
84
- "types": "extensions/index.ts"
84
+ }
85
85
  }
package/src/commands.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  setAdvisorPlanGateRef,
36
36
  setAdvisorRedactSecretsRef,
37
37
  setAdvisorRef,
38
+ setAdvisorScoutEnabledRef,
38
39
  setAdvisorSessionSummaryRef,
39
40
  setAdvisorToolPoliciesRef,
40
41
  setAdvisorToolResultMaxBytesRef,
@@ -49,15 +50,18 @@ import {
49
50
  splitRef,
50
51
  } from "./config.js";
51
52
  import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
53
+ import type { ScoutLifecycleEvent } from "./scout.js";
52
54
  import type { AdvisorSessionState } from "./session-state.js";
53
55
  import {
54
56
  adviceForDisplay,
57
+ appendScoutLifecycleEntry,
55
58
  consultAdvisor,
56
59
  advisorSessionState as defaultAdvisorSessionState,
57
60
  hasSoundVerdict,
58
61
  renderAdvisorCallBox,
59
62
  renderAdvisorResponseHeader,
60
63
  resolveAdvisorRequest,
64
+ ScoutStatusManager,
61
65
  } from "./tools.js";
62
66
  import {
63
67
  type AdvisorSettings,
@@ -132,7 +136,9 @@ const CONTEXT_PRESETS: ContextPreset[] = [
132
136
  type ManualConsult = (
133
137
  ctx: ExtensionContext,
134
138
  question?: string,
135
- signal?: AbortSignal
139
+ signal?: AbortSignal,
140
+ onChunk?: (thinking: string, text: string) => void,
141
+ onScout?: (event: ScoutLifecycleEvent) => void
136
142
  ) => Promise<{
137
143
  markdown: string;
138
144
  thinkingText: string;
@@ -161,24 +167,50 @@ export const registerCommands = (
161
167
  dependencies: {
162
168
  consult?: ManualConsult;
163
169
  sessionState?: AdvisorSessionState;
170
+ statusManager?: ScoutStatusManager;
164
171
  } = {}
165
172
  ) => {
166
173
  const advisorSessionState =
167
174
  dependencies.sessionState ?? defaultAdvisorSessionState;
175
+ const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
168
176
  const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
169
177
  const requestAdvisor =
170
178
  dependencies.consult ??
171
- ((ctx, question, signal) =>
172
- consultAdvisor(ctx, question, signal, undefined, "manual"));
173
- const manualConsultations = new Set<AbortController>();
179
+ ((ctx, question, signal, onChunk, onScout) =>
180
+ consultAdvisor(
181
+ ctx,
182
+ question,
183
+ signal,
184
+ onChunk,
185
+ "manual",
186
+ undefined,
187
+ undefined,
188
+ undefined,
189
+ undefined,
190
+ onScout
191
+ ));
192
+ const manualConsultations = new Map<AbortController, symbol>();
174
193
 
175
194
  const startManualConsultation = (
176
195
  ctx: ExtensionContext,
177
196
  question: string | undefined,
178
- controller: AbortController
197
+ controller: AbortController,
198
+ scoutStatusToken: symbol
179
199
  ) => {
180
200
  herdrAdvisorActivity.start();
181
- return requestAdvisor(ctx, question, controller.signal)
201
+ let scoutDetails: Parameters<typeof appendScoutLifecycleEntry>[2];
202
+ return requestAdvisor(
203
+ ctx,
204
+ question,
205
+ controller.signal,
206
+ undefined,
207
+ (event) => {
208
+ if (!controller.signal.aborted) {
209
+ scoutStatus.update(ctx, scoutStatusToken, event);
210
+ scoutDetails = appendScoutLifecycleEntry(pi, event, scoutDetails);
211
+ }
212
+ }
213
+ )
182
214
  .then(({ markdown }) => {
183
215
  if (controller.signal.aborted) {
184
216
  return;
@@ -232,6 +264,7 @@ export const registerCommands = (
232
264
  notifyHerdrAdvisorFailure("Advisor consultation failed", message);
233
265
  })
234
266
  .finally(() => {
267
+ scoutStatus.release(ctx, scoutStatusToken);
235
268
  manualConsultations.delete(controller);
236
269
  herdrAdvisorActivity.finish();
237
270
  });
@@ -398,10 +431,12 @@ export const registerCommands = (
398
431
  saveConfig(ctx);
399
432
  });
400
433
 
401
- pi.on("session_shutdown", () => {
402
- for (const controller of manualConsultations) {
434
+ pi.on("session_shutdown", (_event, ctx) => {
435
+ for (const [controller, token] of manualConsultations) {
403
436
  controller.abort();
437
+ scoutStatus.release(ctx, token);
404
438
  }
439
+ scoutStatus.clear(ctx);
405
440
  manualConsultations.clear();
406
441
  herdrAdvisorActivity.clear();
407
442
  });
@@ -430,14 +465,17 @@ export const registerCommands = (
430
465
  const question = resolveAdvisorRequest(args);
431
466
  // A single visible progress surface avoids competing consultations overwriting
432
467
  // each other's streamed state. A newer manual request replaces the previous one.
433
- for (const pending of manualConsultations) {
468
+ for (const [pending, token] of manualConsultations) {
434
469
  pending.abort();
470
+ scoutStatus.release(ctx, token);
435
471
  }
436
472
  manualConsultations.clear();
437
473
  const controller = new AbortController();
438
- manualConsultations.add(controller);
474
+ const scoutStatusToken = Symbol("manual-scout");
475
+ scoutStatus.register(scoutStatusToken);
476
+ manualConsultations.set(controller, scoutStatusToken);
439
477
  pi.appendEntry?.("advisor-manual-call", { question });
440
- startManualConsultation(ctx, question, controller);
478
+ startManualConsultation(ctx, question, controller, scoutStatusToken);
441
479
  return Promise.resolve();
442
480
  },
443
481
  });
@@ -563,6 +601,7 @@ export const registerCommands = (
563
601
  setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
564
602
  setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
565
603
  setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
604
+ setAdvisorScoutEnabledRef(settings.scoutEnabled ?? false);
566
605
  setSimpleModeRef(settings.simpleMode ?? false);
567
606
  setAlwaysOnRef(settings.alwaysOn ?? false);
568
607
  setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
package/src/config.ts CHANGED
@@ -65,6 +65,7 @@ export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
65
65
  export let advisorOutcomeLoggingRef = false;
66
66
  export let advisorUntrackedContentRef = false;
67
67
  export let advisorTrackedFileContentRef = false;
68
+ export let advisorScoutEnabledRef = false;
68
69
 
69
70
  export const setExecutorRef = (ref: string) => {
70
71
  executorRef = ref;
@@ -170,6 +171,9 @@ export const setAdvisorUntrackedContentRef = (enabled: boolean) => {
170
171
  export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
171
172
  advisorTrackedFileContentRef = enabled;
172
173
  };
174
+ export const setAdvisorScoutEnabledRef = (enabled: boolean) => {
175
+ advisorScoutEnabledRef = enabled;
176
+ };
173
177
 
174
178
  /**
175
179
  * Returns the current live settings state. Use this at UI boundaries instead of
@@ -194,6 +198,7 @@ export const getAdvisorSettings = () => ({
194
198
  outcomeLogging: advisorOutcomeLoggingRef,
195
199
  planGate: advisorPlanGateRef,
196
200
  redactSecrets: advisorRedactSecretsRef,
201
+ scoutEnabled: advisorScoutEnabledRef,
197
202
  sessionSummary: advisorSessionSummaryRef,
198
203
  simpleMode: simpleModeRef,
199
204
  toolPolicies: { ...advisorToolPoliciesRef },
@@ -232,6 +237,7 @@ export interface AdvisorConfig {
232
237
  advisorOutcomeLogging?: boolean;
233
238
  advisorPlanGate?: boolean;
234
239
  advisorRedactSecrets?: boolean;
240
+ advisorScoutEnabled?: boolean;
235
241
  advisorSessionSummary?: boolean;
236
242
  advisorToolPolicies?: AdvisorToolPolicies;
237
243
  advisorToolResultMaxBytes?: number;
@@ -262,6 +268,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
262
268
  "advisorMaxCallsPerSession",
263
269
  "advisorPlanGate",
264
270
  "advisorSessionSummary",
271
+ "advisorScoutEnabled",
265
272
  "simpleMode",
266
273
  "alwaysOn",
267
274
  "advisorToolResultMaxBytes",
@@ -284,6 +291,7 @@ const BOOLEAN_CONFIG_KEYS = [
284
291
  "advisorBlockOnBlocked",
285
292
  "advisorAutoLoopGate",
286
293
  "advisorSessionSummary",
294
+ "advisorScoutEnabled",
287
295
  "simpleMode",
288
296
  "alwaysOn",
289
297
  "advisorHerdrIntegration",
@@ -469,6 +477,7 @@ const resetDefaults = () => {
469
477
  advisorOutcomeLoggingRef = false;
470
478
  advisorUntrackedContentRef = false;
471
479
  advisorTrackedFileContentRef = false;
480
+ advisorScoutEnabledRef = false;
472
481
  };
473
482
 
474
483
  const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
@@ -535,6 +544,7 @@ const applyConfig = (config: AdvisorConfig) => {
535
544
  "advisorSessionSummary",
536
545
  setAdvisorSessionSummaryRef
537
546
  );
547
+ applyOptionalConfig(config, "advisorScoutEnabled", setAdvisorScoutEnabledRef);
538
548
  applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
539
549
  applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
540
550
  applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
@@ -693,6 +703,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
693
703
  advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
694
704
  advisorHerdrIntegration: advisorHerdrIntegrationRef,
695
705
  advisorRedactSecrets: advisorRedactSecretsRef,
706
+ advisorScoutEnabled: advisorScoutEnabledRef,
696
707
  advisorSessionSummary: advisorSessionSummaryRef,
697
708
  advisorToolPolicies: advisorToolPoliciesRef,
698
709
  advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
@@ -247,7 +247,7 @@ const toolResultEntry = (
247
247
  return `[Tool Result for ${toolName}] (${message.isError ? "Error " : ""}output):\n${capped.content}`;
248
248
  };
249
249
 
250
- const conversationEntry = (
250
+ export const conversationEntry = (
251
251
  entry: unknown,
252
252
  toolResultMaxLines: number,
253
253
  toolResultMaxBytes: number,
@@ -0,0 +1,106 @@
1
+ import {
2
+ type Api,
3
+ type AssistantMessage,
4
+ type Message,
5
+ type Model,
6
+ stream,
7
+ } from "@earendil-works/pi-ai/compat";
8
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
+ import { splitRef } from "./config.js";
10
+
11
+ export interface ResolvedConfiguredModel {
12
+ apiKey: string;
13
+ env?: Record<string, string>;
14
+ headers?: Record<string, string | null>;
15
+ model: Model<Api>;
16
+ ref: string;
17
+ }
18
+
19
+ export const resolveConfiguredModel = async (
20
+ ctx: ExtensionContext,
21
+ ref: string,
22
+ label: string
23
+ ): Promise<ResolvedConfiguredModel> => {
24
+ const [provider, modelId] = splitRef(ref);
25
+ const model = ctx.modelRegistry.find(provider, modelId);
26
+ if (!model) {
27
+ throw new Error(`${label} model not found: ${ref}`);
28
+ }
29
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
30
+ if (!auth.ok) {
31
+ throw new Error((auth as { error: string }).error);
32
+ }
33
+ if (!auth.apiKey) {
34
+ throw new Error(`No API key for ${ref}`);
35
+ }
36
+ return {
37
+ apiKey: auth.apiKey,
38
+ env: auth.env,
39
+ headers: auth.headers,
40
+ model,
41
+ ref,
42
+ };
43
+ };
44
+
45
+ export interface CollectTextStreamOptions {
46
+ messages: Message[];
47
+ onChunk?: (thinking: string, text: string) => void;
48
+ reasoning?: string;
49
+ signal?: AbortSignal;
50
+ systemPrompt: string;
51
+ }
52
+
53
+ export interface CollectedTextStream {
54
+ text: string;
55
+ thinking: string;
56
+ usage?: unknown;
57
+ }
58
+
59
+ export const collectTextStream = async (
60
+ resolved: ResolvedConfiguredModel,
61
+ options: CollectTextStreamOptions,
62
+ streamModel: typeof stream = stream
63
+ ): Promise<CollectedTextStream> => {
64
+ let thinking = "";
65
+ let text = "";
66
+ const eventStream = streamModel(
67
+ resolved.model,
68
+ { messages: options.messages, systemPrompt: options.systemPrompt },
69
+ {
70
+ apiKey: resolved.apiKey,
71
+ env: resolved.env,
72
+ headers: resolved.headers,
73
+ reasoning: options.reasoning as never,
74
+ signal: options.signal,
75
+ }
76
+ );
77
+
78
+ for await (const event of eventStream) {
79
+ if (event.type === "thinking_delta") {
80
+ thinking += event.delta;
81
+ options.onChunk?.(thinking, text);
82
+ } else if (event.type === "text_delta") {
83
+ text += event.delta;
84
+ options.onChunk?.(thinking, text);
85
+ }
86
+ }
87
+
88
+ const response = await eventStream.result();
89
+ const lastAssistant = [response].find(
90
+ (message): message is AssistantMessage => message.role === "assistant"
91
+ );
92
+ const finalText =
93
+ lastAssistant?.content
94
+ .filter(
95
+ (part): part is { type: "text"; text: string } => part.type === "text"
96
+ )
97
+ .map((part) => part.text)
98
+ .join("\n") || text;
99
+ return {
100
+ text: finalText,
101
+ thinking,
102
+ usage: (
103
+ lastAssistant as (AssistantMessage & { usage?: unknown }) | undefined
104
+ )?.usage,
105
+ };
106
+ };
package/src/outcomes.ts CHANGED
@@ -126,16 +126,16 @@ export const appendOutcome = async (
126
126
  ) => {
127
127
  const path = outcomeLogPath();
128
128
  await mkdir(getAgentDir(), { mode: 0o700, recursive: true });
129
- const next: OutcomeRecord = {
130
- adoption: record.adoption,
131
- adviceHash: adviceDigest(record.advice, await salt()),
132
- timestamp: new Date().toISOString(),
133
- trigger: record.trigger,
134
- v: 1,
135
- validationStatus: record.validationStatus,
136
- };
137
- const line = `${JSON.stringify(next)}\n`;
138
- await withOutcomeLock(async () => {
129
+ return withOutcomeLock(async () => {
130
+ const next: OutcomeRecord = {
131
+ adoption: record.adoption,
132
+ adviceHash: adviceDigest(record.advice, await salt()),
133
+ timestamp: new Date().toISOString(),
134
+ trigger: record.trigger,
135
+ v: 1,
136
+ validationStatus: record.validationStatus,
137
+ };
138
+ const line = `${JSON.stringify(next)}\n`;
139
139
  const currentBytes = await stat(path)
140
140
  .then((value) => value.size)
141
141
  .catch((error: NodeJS.ErrnoException) => {
@@ -150,6 +150,6 @@ export const appendOutcome = async (
150
150
  await appendFile(path, line, { encoding: "utf8", mode: 0o600 });
151
151
  }
152
152
  await chmod(path, 0o600);
153
+ return next;
153
154
  });
154
- return next;
155
155
  };