pi-advisor-flow 0.3.1 → 0.3.3
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 +11 -0
- package/README.md +19 -3
- package/extensions/index.ts +4 -2
- package/package.json +1 -1
- package/src/commands.ts +50 -11
- package/src/config.ts +11 -0
- package/src/conversation.ts +1 -1
- package/src/model-stream.ts +106 -0
- package/src/scout-context.ts +526 -0
- package/src/scout.ts +360 -0
- package/src/tools.ts +431 -73
- package/src/ui.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ 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.3
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- Included the current repeated tool invocation in automatic-gate Scout context.
|
|
17
|
+
|
|
7
18
|
## 0.3.1
|
|
8
19
|
|
|
9
20
|
### 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.
|
|
27
|
+
Current release: **0.3.2**. 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.
|
|
60
|
-
4. The
|
|
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
|
package/extensions/index.ts
CHANGED
|
@@ -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
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(
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/conversation.ts
CHANGED
|
@@ -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
|
+
};
|