pi-advisor-flow 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -17
- package/README.md +10 -8
- package/dist/index.js +2852 -868
- package/extensions/index.ts +2 -1
- package/package.json +55 -40
- package/src/attachments.ts +11 -10
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +2 -0
- package/src/commands/lifecycle.ts +1 -0
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +103 -79
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +64 -2
- package/src/commands/types.ts +2 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +83 -1
- package/src/config/schema.ts +152 -15
- package/src/config/state.ts +80 -2
- package/src/config/storage.ts +38 -22
- package/src/config/types.ts +45 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +338 -0
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +267 -0
- package/src/jev/ledger.ts +228 -0
- package/src/jev/questions.ts +122 -0
- package/src/jev/state.ts +50 -0
- package/src/jev/transport.ts +63 -0
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +20 -12
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +115 -73
- package/src/session-state.ts +148 -39
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +204 -0
- package/src/tools/jev-turn-gate.ts +203 -0
- package/src/tools/loop-gate.ts +32 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/outage-notifier.ts +30 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +73 -17
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +149 -73
- package/src/tools/registration.ts +15 -0
- package/src/tools/render-advisor-result.ts +77 -35
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +11 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +350 -0
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +62 -0
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +200 -2
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +169 -47
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +25 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +29 -20
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { advisorJevTransportRef } from "../config/state.ts";
|
|
4
|
+
import { resolveTypeSafeKey } from "./key-store.ts";
|
|
5
|
+
import type { JevKeySource } from "./key-store.ts";
|
|
6
|
+
|
|
7
|
+
export type JevTransportKind = "typesafe" | "openrouter";
|
|
8
|
+
|
|
9
|
+
export interface JevCredentials {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
/** Where the TypeSafe key came from; present on the typesafe transport. */
|
|
12
|
+
source?: JevKeySource;
|
|
13
|
+
transport: JevTransportKind;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface JevTransportDeps {
|
|
17
|
+
/** Reads a pi-stored provider login; injectable for tests. */
|
|
18
|
+
getProviderKey?: (provider: string) => Promise<string | undefined>;
|
|
19
|
+
/** Resolves the TypeSafe key chain; injectable for tests. */
|
|
20
|
+
resolveTypesafe?: () => ReturnType<typeof resolveTypeSafeKey>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OPENROUTER_PROVIDER = "openrouter";
|
|
24
|
+
|
|
25
|
+
const openRouterKey = async (
|
|
26
|
+
ctx: ExtensionContext | undefined,
|
|
27
|
+
deps: JevTransportDeps
|
|
28
|
+
): Promise<string | undefined> => {
|
|
29
|
+
const key = deps.getProviderKey
|
|
30
|
+
? await deps.getProviderKey(OPENROUTER_PROVIDER)
|
|
31
|
+
: await ctx?.modelRegistry?.getApiKeyForProvider(OPENROUTER_PROVIDER);
|
|
32
|
+
return key?.trim() || undefined;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Resolves how Jev calls authenticate: a dedicated TypeSafe key first, then
|
|
36
|
+
* the existing pi OpenRouter login (reuse per advisorJevTransport). */
|
|
37
|
+
export const resolveJevTransport = async (
|
|
38
|
+
ctx?: ExtensionContext,
|
|
39
|
+
deps: JevTransportDeps = {}
|
|
40
|
+
): Promise<JevCredentials | undefined> => {
|
|
41
|
+
const preference = advisorJevTransportRef;
|
|
42
|
+
if (preference !== "openrouter") {
|
|
43
|
+
const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
|
|
44
|
+
const resolution = await resolveTypesafe();
|
|
45
|
+
if (resolution.key) {
|
|
46
|
+
const credentials: JevCredentials = {
|
|
47
|
+
apiKey: resolution.key,
|
|
48
|
+
transport: "typesafe",
|
|
49
|
+
};
|
|
50
|
+
if (resolution.source) {
|
|
51
|
+
credentials.source = resolution.source;
|
|
52
|
+
}
|
|
53
|
+
return credentials;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (preference === "typesafe") {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const openrouter = await openRouterKey(ctx, deps);
|
|
60
|
+
return openrouter
|
|
61
|
+
? { apiKey: openrouter, transport: "openrouter" }
|
|
62
|
+
: undefined;
|
|
63
|
+
};
|
package/src/model-stream.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { stream } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import type {
|
|
3
|
+
Api,
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
Message,
|
|
6
|
+
Model,
|
|
7
7
|
} from "@earendil-works/pi-ai/compat";
|
|
8
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
9
10
|
import { splitRef } from "./config/state.ts";
|
|
10
11
|
|
|
11
12
|
export interface ResolvedConfiguredModel {
|
|
@@ -25,13 +26,14 @@ export const resolveConfiguredModel = async (
|
|
|
25
26
|
throw new Error(`${label} model not configured`);
|
|
26
27
|
}
|
|
27
28
|
const [provider, modelId] = splitRef(ref);
|
|
28
|
-
const
|
|
29
|
+
const lookup: [string, string] = [provider, modelId];
|
|
30
|
+
const model = ctx.modelRegistry.find(...lookup);
|
|
29
31
|
if (!model) {
|
|
30
32
|
throw new Error(`${label} model not found: ${ref}`);
|
|
31
33
|
}
|
|
32
34
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
33
35
|
if (!auth.ok) {
|
|
34
|
-
throw new Error(
|
|
36
|
+
throw new Error(auth.error);
|
|
35
37
|
}
|
|
36
38
|
if (!auth.apiKey) {
|
|
37
39
|
throw new Error(`No API key for ${ref}`);
|
|
@@ -61,6 +63,12 @@ export interface CollectedTextStream {
|
|
|
61
63
|
|
|
62
64
|
export const ADVISOR_STREAM_UPDATE_INTERVAL_MS = 90;
|
|
63
65
|
|
|
66
|
+
const defaultScheduler: CoalescedUpdateScheduler = {
|
|
67
|
+
clearTimeout,
|
|
68
|
+
now: Date.now,
|
|
69
|
+
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
70
|
+
};
|
|
71
|
+
|
|
64
72
|
interface CoalescedUpdateResult {
|
|
65
73
|
error?: unknown;
|
|
66
74
|
failed: boolean;
|
|
@@ -85,19 +93,14 @@ export interface CoalescedUpdateScheduler {
|
|
|
85
93
|
export const createCoalescedUpdate = <T>(
|
|
86
94
|
publish: (value: T) => void,
|
|
87
95
|
intervalMs = ADVISOR_STREAM_UPDATE_INTERVAL_MS,
|
|
88
|
-
scheduler: CoalescedUpdateScheduler =
|
|
89
|
-
clearTimeout,
|
|
90
|
-
now: Date.now,
|
|
91
|
-
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
92
|
-
}
|
|
96
|
+
scheduler: CoalescedUpdateScheduler | undefined = defaultScheduler
|
|
93
97
|
): CoalescedUpdate<T> => {
|
|
94
98
|
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
95
99
|
throw new Error("Coalesced update interval must be positive and finite.");
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
let closed = false;
|
|
99
|
-
let
|
|
100
|
-
let pending: T | undefined;
|
|
103
|
+
let pending: { value: T } | undefined;
|
|
101
104
|
let lastPublishedAt: number | undefined;
|
|
102
105
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
103
106
|
let publishError: unknown;
|
|
@@ -112,12 +115,11 @@ export const createCoalescedUpdate = <T>(
|
|
|
112
115
|
|
|
113
116
|
const publishPending = () => {
|
|
114
117
|
timer = undefined;
|
|
115
|
-
if (!
|
|
118
|
+
if (!pending) {
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
|
-
const value = pending
|
|
121
|
+
const { value } = pending;
|
|
119
122
|
pending = undefined;
|
|
120
|
-
hasPending = false;
|
|
121
123
|
lastPublishedAt = scheduler.now();
|
|
122
124
|
try {
|
|
123
125
|
publish(value);
|
|
@@ -150,7 +152,6 @@ export const createCoalescedUpdate = <T>(
|
|
|
150
152
|
closed = true;
|
|
151
153
|
clearTimer();
|
|
152
154
|
pending = undefined;
|
|
153
|
-
hasPending = false;
|
|
154
155
|
},
|
|
155
156
|
flush: () => {
|
|
156
157
|
if (!closed) {
|
|
@@ -167,8 +168,7 @@ export const createCoalescedUpdate = <T>(
|
|
|
167
168
|
if (publishFailed) {
|
|
168
169
|
throw publishError;
|
|
169
170
|
}
|
|
170
|
-
pending = value;
|
|
171
|
-
hasPending = true;
|
|
171
|
+
pending = { value };
|
|
172
172
|
if (timer === undefined) {
|
|
173
173
|
schedule();
|
|
174
174
|
}
|
|
@@ -186,22 +186,22 @@ export const collectTextStream = async (
|
|
|
186
186
|
): Promise<CollectedTextStream> => {
|
|
187
187
|
let thinking = "";
|
|
188
188
|
let text = "";
|
|
189
|
+
const streamOptions: Parameters<typeof streamModel>[2] = {
|
|
190
|
+
apiKey: resolved.apiKey,
|
|
191
|
+
env: resolved.env,
|
|
192
|
+
headers: resolved.headers,
|
|
193
|
+
// SAFETY: stream() models the provider-facing reasoning field as never; options.reasoning is the Pi-facing effort string.
|
|
194
|
+
reasoning: options.reasoning as never,
|
|
195
|
+
signal: options.signal,
|
|
196
|
+
};
|
|
197
|
+
if (options.reasoning !== undefined) {
|
|
198
|
+
// SAFETY: reasoningEffort takes the same effort string; kept absent when reasoning is unset.
|
|
199
|
+
streamOptions.reasoningEffort = options.reasoning as never;
|
|
200
|
+
}
|
|
189
201
|
const eventStream = streamModel(
|
|
190
202
|
resolved.model,
|
|
191
203
|
{ messages: options.messages, systemPrompt: options.systemPrompt },
|
|
192
|
-
|
|
193
|
-
apiKey: resolved.apiKey,
|
|
194
|
-
env: resolved.env,
|
|
195
|
-
headers: resolved.headers,
|
|
196
|
-
// `stream()` uses the provider-facing name while the extension's public
|
|
197
|
-
// option keeps the Pi-facing `reasoning` name. Preserve both so the
|
|
198
|
-
// configured effort reaches providers that serialize reasoning_effort.
|
|
199
|
-
reasoning: options.reasoning as never,
|
|
200
|
-
...(options.reasoning === undefined
|
|
201
|
-
? {}
|
|
202
|
-
: { reasoningEffort: options.reasoning as never }),
|
|
203
|
-
signal: options.signal,
|
|
204
|
-
}
|
|
204
|
+
streamOptions
|
|
205
205
|
);
|
|
206
206
|
|
|
207
207
|
for await (const event of eventStream) {
|
|
@@ -242,8 +242,6 @@ export const collectTextStream = async (
|
|
|
242
242
|
return {
|
|
243
243
|
text: finalText,
|
|
244
244
|
thinking,
|
|
245
|
-
usage:
|
|
246
|
-
lastAssistant as (AssistantMessage & { usage?: unknown }) | undefined
|
|
247
|
-
)?.usage,
|
|
245
|
+
usage: lastAssistant?.usage,
|
|
248
246
|
};
|
|
249
247
|
};
|
package/src/outcomes.ts
CHANGED
|
@@ -11,11 +11,17 @@ import {
|
|
|
11
11
|
writeFile,
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
15
|
+
|
|
14
16
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
17
|
|
|
16
18
|
export type OutcomeAdoption = "followed" | "not-followed" | "unknown";
|
|
17
19
|
export type OutcomeValidation = "passed" | "failed" | "not-run" | "unknown";
|
|
18
|
-
type OutcomeTrigger =
|
|
20
|
+
type OutcomeTrigger =
|
|
21
|
+
| "manual"
|
|
22
|
+
| "executor-requested"
|
|
23
|
+
| "turn-gate"
|
|
24
|
+
| "repeated-tool-call";
|
|
19
25
|
export const ADOPTIONS: OutcomeAdoption[] = [
|
|
20
26
|
"followed",
|
|
21
27
|
"not-followed",
|
|
@@ -40,12 +46,15 @@ const statePath = () => join(getAgentDir(), "advisor-outcomes-salt");
|
|
|
40
46
|
export const outcomeLogPath = () =>
|
|
41
47
|
join(getAgentDir(), "advisor-outcomes.jsonl");
|
|
42
48
|
|
|
49
|
+
const isErrnoException = (error: unknown): error is NodeJS.ErrnoException =>
|
|
50
|
+
error instanceof Error && "code" in error;
|
|
51
|
+
|
|
43
52
|
const salt = async () => {
|
|
44
53
|
const path = statePath();
|
|
45
54
|
await mkdir(getAgentDir(), { mode: 0o700, recursive: true });
|
|
46
55
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
47
56
|
try {
|
|
48
|
-
//
|
|
57
|
+
// Contenders retry sequentially until one salt is atomically published.
|
|
49
58
|
const existing = await readFile(path);
|
|
50
59
|
if (existing.length === 32) {
|
|
51
60
|
return existing;
|
|
@@ -53,7 +62,7 @@ const salt = async () => {
|
|
|
53
62
|
// Recover a salt file left incomplete by an interrupted older writer.
|
|
54
63
|
await unlink(path);
|
|
55
64
|
} catch (error) {
|
|
56
|
-
if ((error
|
|
65
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") {
|
|
57
66
|
throw error;
|
|
58
67
|
}
|
|
59
68
|
}
|
|
@@ -64,7 +73,7 @@ const salt = async () => {
|
|
|
64
73
|
await link(temporary, path);
|
|
65
74
|
return value;
|
|
66
75
|
} catch (error) {
|
|
67
|
-
if ((error
|
|
76
|
+
if (!isErrnoException(error) || error.code !== "EEXIST") {
|
|
68
77
|
throw error;
|
|
69
78
|
}
|
|
70
79
|
} finally {
|
|
@@ -82,7 +91,7 @@ const withOutcomeLock = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
|
82
91
|
const lockPath = `${outcomeLogPath()}.lock`;
|
|
83
92
|
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
84
93
|
try {
|
|
85
|
-
//
|
|
94
|
+
// Lock acquisition must retry sequentially across processes.
|
|
86
95
|
const lock = await open(lockPath, "wx", 0o600);
|
|
87
96
|
const identity = await lock.stat();
|
|
88
97
|
try {
|
|
@@ -95,7 +104,7 @@ const withOutcomeLock = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
} catch (error) {
|
|
98
|
-
if ((error
|
|
107
|
+
if (!isErrnoException(error) || error.code !== "EEXIST") {
|
|
99
108
|
throw error;
|
|
100
109
|
}
|
|
101
110
|
const observed = await stat(lockPath).catch(() => undefined);
|
|
@@ -106,7 +115,7 @@ const withOutcomeLock = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
|
106
115
|
}
|
|
107
116
|
continue;
|
|
108
117
|
}
|
|
109
|
-
await
|
|
118
|
+
await sleep(5);
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
121
|
throw new Error("Timed out waiting to append an Advisor outcome.");
|
|
@@ -141,11 +150,10 @@ export const appendOutcome = async (
|
|
|
141
150
|
}
|
|
142
151
|
throw error;
|
|
143
152
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
153
|
+
const overflow = currentBytes + Buffer.byteLength(line) > MAX_LOG_BYTES;
|
|
154
|
+
await (overflow
|
|
155
|
+
? writeFile(path, line, { encoding: "utf-8", mode: 0o600 })
|
|
156
|
+
: appendFile(path, line, { encoding: "utf-8", mode: 0o600 }));
|
|
149
157
|
await chmod(path, 0o600);
|
|
150
158
|
return next;
|
|
151
159
|
});
|
package/src/pi-settings.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
|
|
3
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
hideThinkingBlock?: boolean;
|
|
7
|
-
}
|
|
6
|
+
import { isRecord } from "./content-utils.ts";
|
|
8
7
|
|
|
9
8
|
interface CachedSetting {
|
|
10
9
|
hidden: boolean;
|
|
@@ -27,12 +26,8 @@ const settingsIdentity = (path: string): string => {
|
|
|
27
26
|
|
|
28
27
|
const readHideThinking = (path: string): boolean => {
|
|
29
28
|
try {
|
|
30
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "
|
|
31
|
-
return (
|
|
32
|
-
typeof parsed === "object" &&
|
|
33
|
-
parsed !== null &&
|
|
34
|
-
(parsed as PiGlobalSettings).hideThinkingBlock === true
|
|
35
|
-
);
|
|
29
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
|
30
|
+
return isRecord(parsed) && parsed.hideThinkingBlock === true;
|
|
36
31
|
} catch {
|
|
37
32
|
return false;
|
|
38
33
|
}
|
package/src/preferences.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { lstat, open, realpath } from "node:fs/promises";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
+
|
|
3
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
4
6
|
import { redactAndCapText } from "./redaction.ts";
|
|
5
7
|
|
|
6
8
|
const PREFERENCES_MAX_BYTES = 8 * 1024;
|
|
@@ -40,9 +42,9 @@ export const readProjectPreferences = async (
|
|
|
40
42
|
try {
|
|
41
43
|
const buffer = Buffer.alloc(maxBytes + 1);
|
|
42
44
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
43
|
-
const source = buffer.subarray(0, bytesRead).toString("
|
|
45
|
+
const source = buffer.subarray(0, bytesRead).toString("utf-8");
|
|
44
46
|
const capped = redactAndCapText(source, maxBytes, redact);
|
|
45
|
-
return { bytes: Buffer.byteLength(capped, "
|
|
47
|
+
return { bytes: Buffer.byteLength(capped, "utf-8"), text: capped };
|
|
46
48
|
} finally {
|
|
47
49
|
await file.close();
|
|
48
50
|
}
|
package/src/redaction.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { capUtf8Bytes } from "./content-utils.ts";
|
|
1
|
+
import { capUtf8Bytes, isString } from "./content-utils.ts";
|
|
2
2
|
|
|
3
3
|
const REDACTION_MARKER = "[REDACTED SECRET]";
|
|
4
|
-
const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/
|
|
5
|
-
const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/
|
|
4
|
+
const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
|
|
5
|
+
const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
|
|
6
6
|
const SECRET_PATTERNS = [
|
|
7
|
-
/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/
|
|
8
|
-
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/
|
|
9
|
-
/\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/
|
|
10
|
-
/([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/
|
|
11
|
-
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/
|
|
12
|
-
/\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/
|
|
7
|
+
/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/giu,
|
|
8
|
+
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/giu,
|
|
9
|
+
/\b(?:api[_-]?key|token|secret|password|passwd)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s"'&,;)}\]]+)/giu,
|
|
10
|
+
/(?<scheme>[a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/giu,
|
|
11
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu,
|
|
12
|
+
/\b(?:aws_secret_access_key|aws_session_token)\s*[:=]\s*[^\s"'&,;)}\]]+/giu,
|
|
13
13
|
] as const;
|
|
14
14
|
|
|
15
15
|
const redactUnterminatedPem = (value: string): string => {
|
|
@@ -31,9 +31,7 @@ export const redactSecrets = (value: string): string => {
|
|
|
31
31
|
let redacted = redactUnterminatedPem(value);
|
|
32
32
|
for (const pattern of SECRET_PATTERNS) {
|
|
33
33
|
redacted = redacted.replace(pattern, (_match, scheme) =>
|
|
34
|
-
|
|
35
|
-
? `${scheme}${REDACTION_MARKER}@`
|
|
36
|
-
: REDACTION_MARKER
|
|
34
|
+
isString(scheme) ? `${scheme}${REDACTION_MARKER}@` : REDACTION_MARKER
|
|
37
35
|
);
|
|
38
36
|
}
|
|
39
37
|
return redacted;
|
package/src/scout-context.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
advisorRedactSecretsRef,
|
|
4
5
|
advisorToolPoliciesRef,
|
|
5
6
|
advisorToolResultMaxBytesRef,
|
|
6
7
|
advisorToolResultMaxLinesRef,
|
|
7
8
|
} from "./config/state.ts";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
type DisclosureCaps,
|
|
11
|
-
type GroupPass,
|
|
12
|
-
groupWireBytes,
|
|
13
|
-
} from "./scout-groups.ts";
|
|
9
|
+
import { buildGroups, groupWireBytes } from "./scout-groups.ts";
|
|
10
|
+
import type { DisclosureCaps, GroupPass } from "./scout-groups.ts";
|
|
14
11
|
import { indexToolCalls } from "./scout-protocol.ts";
|
|
15
12
|
import {
|
|
16
|
-
type BuildScoutManifestOptions,
|
|
17
13
|
SCOUT_GROUP_MAX_BYTES,
|
|
18
14
|
SCOUT_MANIFEST_MAX_BYTES,
|
|
19
15
|
SCOUT_MANIFEST_MAX_GROUPS,
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
} from "./scout-types.ts";
|
|
17
|
+
import type {
|
|
18
|
+
BuildScoutManifestOptions,
|
|
19
|
+
ScoutContextGroup,
|
|
20
|
+
ScoutManifestResult,
|
|
22
21
|
} from "./scout-types.ts";
|
|
23
22
|
|
|
24
|
-
//
|
|
23
|
+
// Re-exports preserve scout-context's historical public import surface.
|
|
25
24
|
export { reconstructScoutConversation } from "./scout-reconstruct.ts";
|
|
26
25
|
export type {
|
|
27
26
|
BuildScoutManifestOptions,
|
|
@@ -54,21 +53,44 @@ const resolveCaps = (
|
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
/** Builds disclosed, indivisible history groups from Pi's compaction-aware branch. */
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
const contentChars = (items: ScoutContextGroup[]) =>
|
|
57
|
+
items.reduce((sum, group) => sum + group.content.length, 0) +
|
|
58
|
+
Math.max(0, items.length - 1) * 2;
|
|
59
|
+
|
|
60
|
+
const requiredOverflow = (
|
|
61
|
+
required: ScoutContextGroup[],
|
|
62
|
+
caps: {
|
|
63
|
+
maxConversationChars?: number;
|
|
64
|
+
maxGroupBytes: number;
|
|
65
|
+
maxGroups: number;
|
|
66
|
+
maxManifestBytes: number;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
68
|
+
): ScoutManifestResult | undefined => {
|
|
69
|
+
if (
|
|
70
|
+
required.some((group) => group.bytes > caps.maxGroupBytes) ||
|
|
71
|
+
required.length > caps.maxGroups ||
|
|
72
|
+
required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
|
|
73
|
+
caps.maxManifestBytes
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
message:
|
|
77
|
+
"Required Scout context exceeds the Scout manifest transport limit.",
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: "required-group-overflow",
|
|
80
|
+
};
|
|
70
81
|
}
|
|
71
|
-
|
|
82
|
+
if (
|
|
83
|
+
caps.maxConversationChars !== undefined &&
|
|
84
|
+
contentChars(required) > caps.maxConversationChars
|
|
85
|
+
) {
|
|
86
|
+
return {
|
|
87
|
+
message:
|
|
88
|
+
"Required Scout context exceeds the Advisor conversation budget.",
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: "required-group-overflow",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
72
94
|
};
|
|
73
95
|
|
|
74
96
|
const fits = (
|
|
@@ -86,10 +108,6 @@ const fits = (
|
|
|
86
108
|
(caps.maxConversationChars === undefined ||
|
|
87
109
|
contentChars(selected) <= caps.maxConversationChars);
|
|
88
110
|
|
|
89
|
-
const contentChars = (items: ScoutContextGroup[]) =>
|
|
90
|
-
items.reduce((sum, group) => sum + group.content.length, 0) +
|
|
91
|
-
Math.max(0, items.length - 1) * 2;
|
|
92
|
-
|
|
93
111
|
const fitToBudget = (
|
|
94
112
|
built: GroupPass,
|
|
95
113
|
caps: {
|
|
@@ -126,7 +144,7 @@ const fitToBudget = (
|
|
|
126
144
|
);
|
|
127
145
|
while (!fits(selected, caps)) {
|
|
128
146
|
const optionalIndex = selected.findIndex((group) => !group.required);
|
|
129
|
-
if (optionalIndex
|
|
147
|
+
if (optionalIndex === -1) {
|
|
130
148
|
return {
|
|
131
149
|
message: "Required Scout context exceeds fixed manifest limits.",
|
|
132
150
|
ok: false,
|
|
@@ -151,38 +169,19 @@ const fitToBudget = (
|
|
|
151
169
|
};
|
|
152
170
|
};
|
|
153
171
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
required.some((group) => group.bytes > caps.maxGroupBytes) ||
|
|
165
|
-
required.length > caps.maxGroups ||
|
|
166
|
-
required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
|
|
167
|
-
caps.maxManifestBytes
|
|
168
|
-
) {
|
|
169
|
-
return {
|
|
170
|
-
message:
|
|
171
|
-
"Required Scout context exceeds the Scout manifest transport limit.",
|
|
172
|
-
ok: false,
|
|
173
|
-
reason: "required-group-overflow",
|
|
174
|
-
};
|
|
172
|
+
export const buildScoutManifest = (
|
|
173
|
+
ctx: ExtensionContext,
|
|
174
|
+
options: BuildScoutManifestOptions = {}
|
|
175
|
+
): ScoutManifestResult => {
|
|
176
|
+
const entries = ctx.sessionManager.buildContextEntries();
|
|
177
|
+
const caps = resolveCaps(options);
|
|
178
|
+
const indexed = indexToolCalls(entries);
|
|
179
|
+
if (!indexed.ok) {
|
|
180
|
+
return indexed;
|
|
175
181
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
) {
|
|
180
|
-
return {
|
|
181
|
-
message:
|
|
182
|
-
"Required Scout context exceeds the Advisor conversation budget.",
|
|
183
|
-
ok: false,
|
|
184
|
-
reason: "required-group-overflow",
|
|
185
|
-
};
|
|
182
|
+
const built = buildGroups(entries, indexed.index, caps);
|
|
183
|
+
if (!built.ok) {
|
|
184
|
+
return built;
|
|
186
185
|
}
|
|
187
|
-
return
|
|
186
|
+
return fitToBudget(built, caps);
|
|
188
187
|
};
|
package/src/scout-curation.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import { advisorScoutEnabledRef, executorRef } from "./config/state.ts";
|
|
3
|
-
import {
|
|
4
|
-
runAdvisorScout,
|
|
5
|
-
type ScoutLifecycleEvent,
|
|
6
|
-
type ScoutOutcome,
|
|
7
|
-
} from "./scout.ts";
|
|
8
4
|
import {
|
|
9
5
|
buildScoutManifest,
|
|
10
6
|
reconstructScoutConversation,
|
|
11
7
|
SCOUT_MANIFEST_MAX_BYTES,
|
|
12
8
|
} from "./scout-context.ts";
|
|
9
|
+
import { runAdvisorScout } from "./scout.ts";
|
|
10
|
+
import type { ScoutLifecycleEvent, ScoutOutcome } from "./scout.ts";
|
|
13
11
|
|
|
14
12
|
/** Runs Scout over the manifested branch and reconstructs the curated
|
|
15
13
|
* conversation, falling back to the exact legacy conversation on any
|
|
@@ -55,14 +53,7 @@ export const curateAdvisorConversation = async (
|
|
|
55
53
|
onScout?.({ outcome: scout, type: "fallback" });
|
|
56
54
|
return { conversation: legacyConversation, scout };
|
|
57
55
|
}
|
|
58
|
-
const outcome = await runScout(
|
|
59
|
-
ctx,
|
|
60
|
-
built.manifest,
|
|
61
|
-
signal,
|
|
62
|
-
onScout,
|
|
63
|
-
undefined,
|
|
64
|
-
undefined
|
|
65
|
-
);
|
|
56
|
+
const outcome = await runScout(ctx, built.manifest, signal, onScout);
|
|
66
57
|
if (!outcome.ok && outcome.cancelled) {
|
|
67
58
|
throw signal?.reason instanceof Error
|
|
68
59
|
? signal.reason
|