@mgiles/perk 1.1.0 → 2.0.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/README.md +68 -44
- package/extension/adapters/planAdapterPlannotator.ts +27 -41
- package/extension/adapters/planAdapterTombell.ts +15 -28
- package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
- package/extension/checkpoints/checkpoints.ts +19 -12
- package/extension/doors/address.ts +4 -4
- package/extension/doors/askUser.ts +12 -8
- package/extension/doors/ciExecutor.ts +21 -14
- package/extension/doors/hunkHandoff.ts +202 -0
- package/extension/doors/land.ts +31 -9
- package/extension/doors/learn.ts +2 -2
- package/extension/doors/learnFactory.ts +144 -0
- package/extension/doors/plannotatorHandoff.ts +509 -0
- package/extension/doors/prReview.ts +4 -4
- package/extension/doors/prReviewBrowser.ts +341 -0
- package/extension/doors/prReviewTerminal.ts +267 -0
- package/extension/doors/selfcheck.ts +238 -5
- package/extension/doors/submit.ts +20 -0
- package/extension/doors/submitPrReview.ts +408 -0
- package/extension/factories/objective.ts +15 -5
- package/extension/factories/objectiveAuthor.ts +15 -32
- package/extension/factories/objectiveDraft.ts +1 -1
- package/extension/factories/objectivePlan.ts +12 -10
- package/extension/factories/objectiveSave.ts +2 -2
- package/extension/factories/planMode.ts +22 -40
- package/extension/factories/planReview.ts +213 -191
- package/extension/factories/planSave.ts +7 -7
- package/extension/index.ts +83 -25
- package/extension/substrate/bindingDelivery.ts +32 -10
- package/extension/substrate/bindings.ts +4 -2
- package/extension/substrate/cache.ts +34 -7
- package/extension/substrate/clipboard.ts +81 -0
- package/extension/substrate/config.ts +88 -65
- package/extension/substrate/git.ts +43 -0
- package/extension/substrate/paths.ts +1 -1
- package/extension/substrate/prompts.ts +2 -2
- package/extension/substrate/providers.ts +62 -8
- package/extension/substrate/sessionPointers.ts +35 -6
- package/extension/substrate/structuredOutput.ts +3 -1
- package/extension/substrate/terminalLaunch.ts +178 -0
- package/extension/substrate/toolGating.ts +330 -79
- package/extension/substrate/toolParams.ts +7 -0
- package/extension/substrate/workflowState.ts +54 -2
- package/extension/surfaces/footerProvider.ts +8 -4
- package/extension/surfaces/surfaces.ts +330 -12
- package/extension/vendor/btw/btw.ts +10 -0
- package/extension/worker/readOnlySession.ts +19 -6
- package/extension/worker/worker.ts +77 -7
- package/extension/workerMain.ts +12 -13
- package/package.json +3 -3
- package/prompts/_fixtures/live.yaml +117 -2
- package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -0
- package/prompts/contexts/adapters/plannotator-plan.md +6 -0
- package/prompts/contexts/adapters/tombell-plan.md +17 -0
- package/prompts/contexts/objective-authoring.md +20 -0
- package/prompts/contexts/plan-authoring.md +24 -0
- package/prompts/contexts/read-only.md +10 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-plan/guidance.md +1 -1
- package/prompts/stages/objective-plan/seed.md +1 -1
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-replan.md +1 -1
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/pr-review-browser/active.md +11 -0
- package/prompts/stages/pr-review-browser/foreign.md +11 -0
- package/prompts/stages/pr-review-terminal/active.md +12 -0
- package/prompts/stages/pr-review-terminal/foreign.md +13 -0
- package/prompts/stages/pr-review-terminal/local.md +4 -0
- package/prompts/stages/pr-review.md +1 -1
- package/prompts/stages/replan.md +2 -2
- package/prompts/stages/skills/create-from.md +1 -1
- package/prompts/stages/skills/create.md +1 -1
- package/prompts/stages/skills/refine.md +1 -1
- package/shared/README.md +22 -18
- package/shared/bindings.yaml +10 -2
- package/shared/contracts-history.md +24 -0
- package/shared/contracts.md +1442 -1787
- package/shared/providers.yaml +8 -1
- package/shared/registry.yaml +7 -8
- package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
- package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
- package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
- package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
- package/extension/doors/learnCode.ts +0 -100
- package/extension/doors/learnDocs.ts +0 -100
- package/extension/doors/prReviewLocal.ts +0 -229
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
18
18
|
|
|
19
|
+
// `Key` is keybinding vocabulary (`pi.registerShortcut(Key.ctrlAlt("p"), …)`), not rich UI —
|
|
20
|
+
// re-exported so pi-tui imports stay structurally confined to the surfaces module (the
|
|
21
|
+
// surfacesGuard pi-tui import rule) without allowlisting the shortcut-registering modules.
|
|
22
|
+
export { Key } from "@earendil-works/pi-tui";
|
|
19
23
|
// Re-exports: the notify seam stays in report.ts; surfaces.ts is the one import for UI vocabulary.
|
|
20
24
|
export { type ReportTarget, report, type Severity } from "./report.ts";
|
|
21
25
|
|
|
@@ -30,7 +34,7 @@ export const WIDGET_SLOT_CHECKPOINTS = "perk-checkpoints";
|
|
|
30
34
|
export const MARK_CHECKPOINTS = "📋";
|
|
31
35
|
export const MARK_OBJECTIVE = "🎯";
|
|
32
36
|
|
|
33
|
-
// --- glyph vocabulary (charter §5 / D3) — data only; themed rendering
|
|
37
|
+
// --- glyph vocabulary (charter §5 / D3) — data only; themed rendering lives in the widget/footer builders below ---
|
|
34
38
|
export type GlyphKind = "done" | "current" | "pending" | "warning" | "failure";
|
|
35
39
|
export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> = {
|
|
36
40
|
done: { glyph: "✓", themeColor: "success" },
|
|
@@ -40,7 +44,7 @@ export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> =
|
|
|
40
44
|
failure: { glyph: "✗", themeColor: "error" },
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
// --- height bounds (charter §4 / D1/D8) —
|
|
47
|
+
// --- height bounds (charter §4 / D1/D8) — enforced by the report() notify budget and the footer/widget builders ---
|
|
44
48
|
export const NOTIFY_MAX_LINES = 1;
|
|
45
49
|
export const FOOTER_MAX_LINES = 1;
|
|
46
50
|
export const CHECKPOINTS_WIDGET_MAX_LINES = 4;
|
|
@@ -192,8 +196,8 @@ export function createPerkStatus(): PerkStatusHandle {
|
|
|
192
196
|
/**
|
|
193
197
|
* The raw material for one composed footer line. Left group (charter order 1–3): `identity`,
|
|
194
198
|
* `objective`, `checkpoints` — the segments render verbatim (they carry their own 🎯/📋 marks).
|
|
195
|
-
* Right group (charter order 4, 5, +context, 6): `branch`, `model`, `thinking`, `
|
|
196
|
-
* right-aligned, non-segment system text dim-themed.
|
|
199
|
+
* Right group (charter order 4, 5, +context, 6): `branch`, `model`, `thinking`, `cache`,
|
|
200
|
+
* `context`, `guests` — right-aligned, non-segment system text dim-themed.
|
|
197
201
|
*/
|
|
198
202
|
export interface FooterParts {
|
|
199
203
|
/** e.g. `perk v0.0.1` — standing identity (D7), dim. */
|
|
@@ -208,12 +212,52 @@ export interface FooterParts {
|
|
|
208
212
|
model?: string;
|
|
209
213
|
/** The session thinking level (dim; e.g. `high`/`off`); omitted when there is no model. */
|
|
210
214
|
thinking?: string;
|
|
215
|
+
/** The prompt-cache-hit segment, e.g. `CH42.3%` (dim; right group); omitted until cache activity. */
|
|
216
|
+
cache?: string;
|
|
211
217
|
/** Context usage — rendered `<pct>%/<window>` (dim; warning >70, error >90; `?` when null). */
|
|
212
218
|
context?: { percent: number | null; contextWindow: number };
|
|
213
219
|
/** Guest extension statuses (dim), pre-sorted by slot key; sanitized here. */
|
|
214
220
|
guests: string[];
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A structural slice of pi's `SessionEntry` — only what `latestCacheHitRate` reads. Keeps
|
|
225
|
+
* surfaces.ts dependency-light (no pi imports; `SessionEntry[]` is assignable) — the same
|
|
226
|
+
* structural-mirror pattern as `FooterDataLike`/`ThemeLike`.
|
|
227
|
+
*/
|
|
228
|
+
export interface UsageEntryLike {
|
|
229
|
+
type: string;
|
|
230
|
+
message?: {
|
|
231
|
+
role?: string;
|
|
232
|
+
usage?: { input: number; cacheRead: number; cacheWrite: number };
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The prompt-cache-hit rate of the latest usage-bearing assistant message, as a percentage —
|
|
238
|
+
* an exact local mirror of pi's default-footer `CH` computation (pi's cache-stats helpers are
|
|
239
|
+
* unexported; the `sanitizeGuestStatus` reimplementation precedent). Includes pi's display gate:
|
|
240
|
+
* returns `null` unless the session shows cache activity (total cacheRead or cacheWrite > 0) AND
|
|
241
|
+
* the latest usage-bearing assistant message has prompt tokens > 0 (a trailing zero-prompt-token
|
|
242
|
+
* assistant message resets the rate, exactly like pi's `undefined`).
|
|
243
|
+
*/
|
|
244
|
+
export function latestCacheHitRate(entries: readonly UsageEntryLike[]): number | null {
|
|
245
|
+
let totalCacheRead = 0;
|
|
246
|
+
let totalCacheWrite = 0;
|
|
247
|
+
let latest: number | null = null;
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
|
|
250
|
+
const usage = entry.message.usage;
|
|
251
|
+
if (usage === undefined) continue;
|
|
252
|
+
totalCacheRead += usage.cacheRead;
|
|
253
|
+
totalCacheWrite += usage.cacheWrite;
|
|
254
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
255
|
+
latest = promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : null;
|
|
256
|
+
}
|
|
257
|
+
if (totalCacheRead <= 0 && totalCacheWrite <= 0) return null;
|
|
258
|
+
return latest;
|
|
259
|
+
}
|
|
260
|
+
|
|
217
261
|
/** Pi's `sanitizeStatusText` behavior, reimplemented locally (pi does not export it). */
|
|
218
262
|
function sanitizeGuestStatus(text: string): string {
|
|
219
263
|
return text
|
|
@@ -239,8 +283,8 @@ function formatContextSegment(
|
|
|
239
283
|
* checkpoints (two-space-joined, charter order); right group = branch + model + context + guests
|
|
240
284
|
* (two-space-joined), right-aligned with ≥2 spaces of padding. When the line exceeds `width`,
|
|
241
285
|
* whole segments drop in the extended D9 order — guests (rightmost-first) → thinking → model →
|
|
242
|
-
* branch → context → checkpoints; `identity` and `objective` are NEVER dropped — then
|
|
243
|
-
* as the last resort (ANSI- and 2-cell-emoji-aware).
|
|
286
|
+
* branch → cache → context → checkpoints; `identity` and `objective` are NEVER dropped — then
|
|
287
|
+
* `truncateToWidth` as the last resort (ANSI- and 2-cell-emoji-aware).
|
|
244
288
|
*/
|
|
245
289
|
export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: number): string {
|
|
246
290
|
const keep = {
|
|
@@ -248,6 +292,7 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
248
292
|
model: true,
|
|
249
293
|
thinking: true,
|
|
250
294
|
branch: true,
|
|
295
|
+
cache: true,
|
|
251
296
|
context: true,
|
|
252
297
|
checkpoints: true,
|
|
253
298
|
};
|
|
@@ -259,6 +304,7 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
259
304
|
if (keep.branch && parts.branch !== undefined) right.push(theme.fg("dim", parts.branch));
|
|
260
305
|
if (keep.model && parts.model !== undefined) right.push(theme.fg("dim", parts.model));
|
|
261
306
|
if (keep.thinking && parts.thinking !== undefined) right.push(theme.fg("dim", parts.thinking));
|
|
307
|
+
if (keep.cache && parts.cache !== undefined) right.push(theme.fg("dim", parts.cache));
|
|
262
308
|
if (keep.context && parts.context !== undefined) {
|
|
263
309
|
right.push(formatContextSegment(parts.context, theme));
|
|
264
310
|
}
|
|
@@ -275,6 +321,7 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
275
321
|
else if (keep.thinking) keep.thinking = false;
|
|
276
322
|
else if (keep.model) keep.model = false;
|
|
277
323
|
else if (keep.branch) keep.branch = false;
|
|
324
|
+
else if (keep.cache) keep.cache = false;
|
|
278
325
|
else if (keep.context) keep.context = false;
|
|
279
326
|
else if (keep.checkpoints) keep.checkpoints = false;
|
|
280
327
|
else break; // identity + objective only — nothing left to drop
|
|
@@ -299,6 +346,7 @@ export interface PerkFooterDeps {
|
|
|
299
346
|
status: PerkStatusHandle;
|
|
300
347
|
getModelId(): string | null;
|
|
301
348
|
getThinkingLevel(): string | null;
|
|
349
|
+
getCacheHitRate(): number | null;
|
|
302
350
|
getContext(): { percent: number | null; contextWindow: number } | null;
|
|
303
351
|
}
|
|
304
352
|
|
|
@@ -318,7 +366,7 @@ export type PerkFooterFactory = (
|
|
|
318
366
|
* line in the intended split layout. `render` gathers everything live per call (D10 stateless
|
|
319
367
|
* render): segments via the handle, branch/guests via `footerData` (excluding perk's own
|
|
320
368
|
* `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the segments
|
|
321
|
-
* directly), model/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
369
|
+
* directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
322
370
|
* every handle recompose and on branch change; `dispose` detaches both.
|
|
323
371
|
*/
|
|
324
372
|
export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
@@ -331,6 +379,7 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
331
379
|
.filter(([key]) => key !== STATUS_SLOT_PERK)
|
|
332
380
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
333
381
|
.map(([, text]) => text);
|
|
382
|
+
const rate = deps.getCacheHitRate();
|
|
334
383
|
const parts: FooterParts = {
|
|
335
384
|
identity: deps.identity,
|
|
336
385
|
objective: deps.status.get("objective"),
|
|
@@ -338,6 +387,7 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
338
387
|
branch: footerData.getGitBranch() ?? undefined,
|
|
339
388
|
model: deps.getModelId() ?? undefined,
|
|
340
389
|
thinking: deps.getThinkingLevel() ?? undefined,
|
|
390
|
+
cache: rate === null ? undefined : `CH${rate.toFixed(1)}%`,
|
|
341
391
|
context: deps.getContext() ?? undefined,
|
|
342
392
|
guests,
|
|
343
393
|
};
|
|
@@ -438,14 +488,28 @@ export function renderProgressLines(
|
|
|
438
488
|
if (item.kind === "elision") {
|
|
439
489
|
return truncateToWidth(theme.fg("dim", `… +${item.hidden} ${item.side}`), width);
|
|
440
490
|
}
|
|
441
|
-
|
|
442
|
-
const glyph = theme.fg(GLYPHS[kind].themeColor, GLYPHS[kind].glyph);
|
|
443
|
-
const text = `${item.step.step}. ${item.step.text}`;
|
|
444
|
-
const line = `${glyph} ${kind === "done" ? theme.fg("muted", text) : text}`;
|
|
445
|
-
return truncateToWidth(line, width);
|
|
491
|
+
return renderStepLine(state, item.step, theme, width);
|
|
446
492
|
});
|
|
447
493
|
}
|
|
448
494
|
|
|
495
|
+
/**
|
|
496
|
+
* ONE themed per-step line (`✓/▸/○ <n>. <text>`, §5 colors, D9-truncated) — shared by the
|
|
497
|
+
* checkpoints widget (`renderProgressLines`, windowed) and the checkpoint transcript marker's
|
|
498
|
+
* expanded view (all steps, unwindowed), so the two surfaces render steps identically.
|
|
499
|
+
*/
|
|
500
|
+
function renderStepLine(
|
|
501
|
+
state: ProgressState,
|
|
502
|
+
step: ProgressStep,
|
|
503
|
+
theme: ThemeLike,
|
|
504
|
+
width: number,
|
|
505
|
+
): string {
|
|
506
|
+
const kind = stepGlyphKind(state, step);
|
|
507
|
+
const glyph = theme.fg(GLYPHS[kind].themeColor, GLYPHS[kind].glyph);
|
|
508
|
+
const text = `${step.step}. ${step.text}`;
|
|
509
|
+
const line = `${glyph} ${kind === "done" ? theme.fg("muted", text) : text}`;
|
|
510
|
+
return truncateToWidth(line, width);
|
|
511
|
+
}
|
|
512
|
+
|
|
449
513
|
function formatTokens(tokens: number): string {
|
|
450
514
|
if (tokens < 1000) return `${tokens}`;
|
|
451
515
|
// Whole-k values render bare (`200k`, matching pi's footer); fractional keep one decimal.
|
|
@@ -465,3 +529,257 @@ function formatElapsed(ms: number): string {
|
|
|
465
529
|
export function formatBudgetLine(args: { tokens: number; elapsedMs: number }): string {
|
|
466
530
|
return `${formatTokens(args.tokens)} tok · ${formatElapsed(args.elapsedMs)}`;
|
|
467
531
|
}
|
|
532
|
+
|
|
533
|
+
// --- the transcript markers — display-only entry renderers ---------------------------------------
|
|
534
|
+
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's four display-only custom-entry
|
|
535
|
+
// families render as durable one-line transcript markers. Renderer BODIES live here (a transcript
|
|
536
|
+
// renderer IS a rich-UI surface the surfaces module owns); registration is wiring at the feature
|
|
537
|
+
// modules via the `registerTranscriptRenderer` seam below. Renderers are an interactive-TUI-only
|
|
538
|
+
// concern (never invoked in json/RPC mode), so registration is inert-safe everywhere.
|
|
539
|
+
|
|
540
|
+
/** Structural slice of pi's `CustomEntry` — the only field the marker renderers read. */
|
|
541
|
+
export interface TranscriptEntryLike {
|
|
542
|
+
data?: unknown;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Structural mirror of pi's `EntryRenderOptions`. */
|
|
546
|
+
export interface EntryRenderOptionsLike {
|
|
547
|
+
expanded: boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* A transcript entry renderer, assignable to pi's `EntryRenderer<unknown>`: the params are
|
|
552
|
+
* structural supertypes of pi's (`CustomEntry`/`EntryRenderOptions`/`Theme`), and the returned
|
|
553
|
+
* object satisfies pi-tui's structural `Component` (`render(width): string[]`; `handleInput` is
|
|
554
|
+
* optional). `undefined` = render nothing (malformed/missing `data` stays invisible — exactly the
|
|
555
|
+
* pre-renderer behavior).
|
|
556
|
+
*/
|
|
557
|
+
export type TranscriptRenderer = (
|
|
558
|
+
entry: TranscriptEntryLike,
|
|
559
|
+
options: EntryRenderOptionsLike,
|
|
560
|
+
theme: ThemeLike,
|
|
561
|
+
) => { render(width: number): string[] } | undefined;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The minimal host surface the registration seam needs. The member is OPTIONAL and
|
|
565
|
+
* method-syntax (bivariant — the `PerkFooterFactory` recipe): pi ≥ 0.80.4's `ExtensionAPI`
|
|
566
|
+
* satisfies it; pre-0.80.4 hosts simply don't have the method.
|
|
567
|
+
*/
|
|
568
|
+
export interface TranscriptRendererHost {
|
|
569
|
+
registerEntryRenderer?(customType: string, renderer: TranscriptRenderer): void;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* The one sanctioned `registerEntryRenderer` call site (guard-confined) carrying the one typeof
|
|
574
|
+
* feature-detect: on a pre-0.80.4 host the method is absent (calling it would `TypeError`), so
|
|
575
|
+
* registration is a silent no-op and the entries stay invisible — exactly today's behavior.
|
|
576
|
+
*/
|
|
577
|
+
export function registerTranscriptRenderer(
|
|
578
|
+
host: TranscriptRendererHost,
|
|
579
|
+
customType: string,
|
|
580
|
+
renderer: TranscriptRenderer,
|
|
581
|
+
): void {
|
|
582
|
+
if (typeof host.registerEntryRenderer !== "function") return; // pre-0.80.4 host: inert
|
|
583
|
+
host.registerEntryRenderer(customType, renderer);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Charter budget: a COLLAPSED transcript marker is exactly one line. The expanded view is
|
|
588
|
+
* human-requested scrollback and renders its full detail unbounded (all checkpoint steps, the
|
|
589
|
+
* whole btw answer).
|
|
590
|
+
*/
|
|
591
|
+
export const TRANSCRIPT_MARKER_MAX_LINES = 1;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The collapsed-marker grammar: the `report()` transition grammar `perk: <scope> — <message>`,
|
|
595
|
+
* dim, D9-truncated. Emoji stay footer-only (D3); themed §5 glyphs appear only in expanded
|
|
596
|
+
* checkpoint step lines.
|
|
597
|
+
*/
|
|
598
|
+
function markerLine(scope: string, message: string, theme: ThemeLike, width: number): string {
|
|
599
|
+
return truncateToWidth(theme.fg("dim", `perk: ${scope} — ${message}`), width);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** The three-clause object-shape guard: a plain (non-null, non-array) object or null. */
|
|
603
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
604
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
605
|
+
return value as Record<string, unknown>;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Decode `{ steps }` from a `perk:checkpoint` entry: a non-empty array of valid steps, or null. */
|
|
609
|
+
function decodeCheckpointSteps(data: unknown): ProgressStep[] | null {
|
|
610
|
+
const record = asRecord(data);
|
|
611
|
+
if (record === null) return null;
|
|
612
|
+
const steps = record.steps;
|
|
613
|
+
if (!Array.isArray(steps) || steps.length === 0) return null;
|
|
614
|
+
const decoded: ProgressStep[] = [];
|
|
615
|
+
for (const raw of steps) {
|
|
616
|
+
const step = asRecord(raw);
|
|
617
|
+
if (step === null) return null;
|
|
618
|
+
if (
|
|
619
|
+
typeof step.step !== "number" ||
|
|
620
|
+
typeof step.text !== "string" ||
|
|
621
|
+
typeof step.completed !== "boolean"
|
|
622
|
+
) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
decoded.push({ step: step.step, text: step.text, completed: step.completed });
|
|
626
|
+
}
|
|
627
|
+
return decoded;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* `perk:checkpoint` marker. Collapsed: `perk: checkpoints — <done/total>`. Expanded: that line +
|
|
632
|
+
* one §5 glyph line per step — ALL steps, unwindowed (scrollback is human-requested, so the
|
|
633
|
+
* `CHECKPOINTS_WIDGET_MAX_LINES` standing budget does not apply). `current` is derived state that
|
|
634
|
+
* lives in checkpoints.ts; a historical marker renders with `current: null` (bare `done/total`,
|
|
635
|
+
* no `▸` step) — which keeps surfaces.ts free of checkpoint imports.
|
|
636
|
+
*/
|
|
637
|
+
export const checkpointEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
638
|
+
const steps = decodeCheckpointSteps(entry.data);
|
|
639
|
+
if (steps === null) return undefined;
|
|
640
|
+
const state: ProgressState = { steps, current: null };
|
|
641
|
+
return {
|
|
642
|
+
render(width) {
|
|
643
|
+
const collapsed = markerLine("checkpoints", progressLine(state), theme, width);
|
|
644
|
+
if (!options.expanded) return [collapsed];
|
|
645
|
+
return [collapsed, ...steps.map((step) => renderStepLine(state, step, theme, width))];
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* The first matching workflow-state field's marker message — a deliberately BOUNDED vocabulary
|
|
652
|
+
* (the four headline fields + a SET `objective_node_claim`), extensible later. Bookkeeping deltas
|
|
653
|
+
* (`session_artifacts`, `last_review*`, `conflict_resolution_attempts`, cleared node claims) stay
|
|
654
|
+
* invisible by returning null.
|
|
655
|
+
*/
|
|
656
|
+
function workflowStateMessage(data: Record<string, unknown>): string | null {
|
|
657
|
+
if (typeof data.run_id === "string") {
|
|
658
|
+
if (typeof data.predecessor === "string") {
|
|
659
|
+
return `run ${data.run_id} · child of ${data.predecessor}`;
|
|
660
|
+
}
|
|
661
|
+
let message = `run ${data.run_id} claimed`;
|
|
662
|
+
if (typeof data.stage === "string") message += ` · stage ${data.stage}`;
|
|
663
|
+
if (typeof data.mode === "string") message += ` · ${data.mode}`;
|
|
664
|
+
return message;
|
|
665
|
+
}
|
|
666
|
+
if (typeof data.mode === "string") return `${data.mode} mode`;
|
|
667
|
+
// Key-presence check (not value truthiness): an explicit null means "cleared" here.
|
|
668
|
+
if (Object.hasOwn(data, "active_objective")) {
|
|
669
|
+
if (typeof data.active_objective === "string") {
|
|
670
|
+
return `objective ${data.active_objective} activated`;
|
|
671
|
+
}
|
|
672
|
+
if (data.active_objective === null) return "objective cleared";
|
|
673
|
+
}
|
|
674
|
+
const planRef = asRecord(data.active_plan_ref);
|
|
675
|
+
if (planRef !== null && typeof planRef.pr_id === "string") {
|
|
676
|
+
return `plan ${planRef.pr_id} linked`;
|
|
677
|
+
}
|
|
678
|
+
const claim = asRecord(data.objective_node_claim);
|
|
679
|
+
if (claim !== null && typeof claim.objective === "string" && typeof claim.node === "string") {
|
|
680
|
+
// A cleared claim (null) stays invisible — only the SET claim is a marker-worthy moment.
|
|
681
|
+
return `node ${claim.node} claimed for objective ${claim.objective}`;
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `perk:workflow-state` marker: the first matching headline field renders (precedence:
|
|
688
|
+
* run claim/fork → mode flip → objective set/clear → plan link → node claim); bookkeeping-only
|
|
689
|
+
* deltas stay invisible. Expanded: the collapsed line + the raw delta as one dim JSON line
|
|
690
|
+
* (/learn-grade debuggability).
|
|
691
|
+
*/
|
|
692
|
+
export const workflowStateEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
693
|
+
const data = asRecord(entry.data);
|
|
694
|
+
if (data === null) return undefined;
|
|
695
|
+
const message = workflowStateMessage(data);
|
|
696
|
+
if (message === null) return undefined;
|
|
697
|
+
return {
|
|
698
|
+
render(width) {
|
|
699
|
+
const collapsed = markerLine("workflow", message, theme, width);
|
|
700
|
+
if (!options.expanded) return [collapsed];
|
|
701
|
+
return [collapsed, truncateToWidth(theme.fg("dim", JSON.stringify(data)), width)];
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* `perk:objective-budget` marker (`{ objective_id, activated_at }` activation entries).
|
|
708
|
+
* Collapsed: `perk: objective — <id> budget tracking started`. Expanded: + a dim activation
|
|
709
|
+
* timestamp line.
|
|
710
|
+
*/
|
|
711
|
+
export const objectiveBudgetEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
712
|
+
const data = asRecord(entry.data);
|
|
713
|
+
if (data === null) return undefined;
|
|
714
|
+
const objectiveId = data.objective_id;
|
|
715
|
+
const activatedAt = data.activated_at;
|
|
716
|
+
if (typeof objectiveId !== "string" || typeof activatedAt !== "string") return undefined;
|
|
717
|
+
return {
|
|
718
|
+
render(width) {
|
|
719
|
+
const collapsed = markerLine(
|
|
720
|
+
"objective",
|
|
721
|
+
`${objectiveId} budget tracking started`,
|
|
722
|
+
theme,
|
|
723
|
+
width,
|
|
724
|
+
);
|
|
725
|
+
if (!options.expanded) return [collapsed];
|
|
726
|
+
return [collapsed, truncateToWidth(theme.fg("dim", `activated at ${activatedAt}`), width)];
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* `btw-thread-entry` marker. Collapsed: `perk: btw — <first line of question>` (dim). Expanded:
|
|
733
|
+
* the question line accented + the answer split on newlines, each line dim + width-truncated —
|
|
734
|
+
* no wrapping (a bounded choice: the marker is a durable pointer; the `/btw` overlay remains the
|
|
735
|
+
* full reader).
|
|
736
|
+
*/
|
|
737
|
+
export const btwThreadEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
738
|
+
const data = asRecord(entry.data);
|
|
739
|
+
if (data === null) return undefined;
|
|
740
|
+
const question = data.question;
|
|
741
|
+
const answer = data.answer;
|
|
742
|
+
if (typeof question !== "string" || typeof answer !== "string") return undefined;
|
|
743
|
+
const headline = question.split("\n", 1)[0] ?? "";
|
|
744
|
+
return {
|
|
745
|
+
render(width) {
|
|
746
|
+
if (!options.expanded) return [markerLine("btw", headline, theme, width)];
|
|
747
|
+
return [
|
|
748
|
+
truncateToWidth(theme.fg("accent", `perk: btw — ${headline}`), width),
|
|
749
|
+
...answer.split("\n").map((line) => truncateToWidth(theme.fg("dim", line), width)),
|
|
750
|
+
];
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
/** `btw-thread-reset` marker: `perk: btw — thread reset` (+ a dim ISO timestamp line expanded). */
|
|
756
|
+
export const btwThreadResetEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
757
|
+
const data = asRecord(entry.data);
|
|
758
|
+
if (data === null) return undefined;
|
|
759
|
+
const timestamp = data.timestamp;
|
|
760
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) return undefined;
|
|
761
|
+
return {
|
|
762
|
+
render(width) {
|
|
763
|
+
const collapsed = markerLine("btw", "thread reset", theme, width);
|
|
764
|
+
if (!options.expanded) return [collapsed];
|
|
765
|
+
return [
|
|
766
|
+
collapsed,
|
|
767
|
+
truncateToWidth(theme.fg("dim", new Date(timestamp).toISOString()), width),
|
|
768
|
+
];
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* The coarse prose-plan widget line (relocated verbatim from checkpoints.ts's inline factory —
|
|
775
|
+
* its only pi-tui usage): one dim, D9-truncated line naming the active plan with no `## Steps`
|
|
776
|
+
* checklist.
|
|
777
|
+
*/
|
|
778
|
+
export function renderCoarsePlanLines(planId: string, theme: ThemeLike, width: number): string[] {
|
|
779
|
+
return [
|
|
780
|
+
truncateToWidth(
|
|
781
|
+
theme.fg("dim", `Plan #${planId}: prose plan — no \`## Steps\` checklist`),
|
|
782
|
+
width,
|
|
783
|
+
),
|
|
784
|
+
];
|
|
785
|
+
}
|
|
@@ -57,6 +57,11 @@ type OverlayHandleLike = {
|
|
|
57
57
|
|
|
58
58
|
import type { ToolGating } from "../../substrate/toolGating.ts";
|
|
59
59
|
import { report } from "../../surfaces/report.ts";
|
|
60
|
+
import {
|
|
61
|
+
btwThreadEntryRenderer,
|
|
62
|
+
btwThreadResetEntryRenderer,
|
|
63
|
+
registerTranscriptRenderer,
|
|
64
|
+
} from "../../surfaces/surfaces.ts";
|
|
60
65
|
import {
|
|
61
66
|
extractEventAssistantText,
|
|
62
67
|
extractText,
|
|
@@ -304,6 +309,11 @@ class BtwOverlay extends Container implements Focusable {
|
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
export function registerBtw(pi: ExtensionAPI, gating: ToolGating): void {
|
|
312
|
+
// Transcript markers for the btw thread entries (audit §2.3): renderer bodies in surfaces.ts,
|
|
313
|
+
// registration = wiring, feature-detect inside the seam (pre-0.80.4 hosts stay inert).
|
|
314
|
+
registerTranscriptRenderer(pi, BTW_ENTRY_TYPE, btwThreadEntryRenderer);
|
|
315
|
+
registerTranscriptRenderer(pi, BTW_RESET_TYPE, btwThreadResetEntryRenderer);
|
|
316
|
+
|
|
307
317
|
let thread: BtwDetails[] = [];
|
|
308
318
|
let pendingQuestion: string | null = null;
|
|
309
319
|
let pendingAnswer = "";
|
|
@@ -80,25 +80,38 @@ export interface CapResult {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* UTF-8-byte-safe truncation (subagent's byte-trim loop). Under cap ⇒ unchanged, truncated:false.
|
|
83
|
-
* When truncated,
|
|
84
|
-
*
|
|
83
|
+
* When truncated, a notice points at the scratch file holding the full result; the notice sits at
|
|
84
|
+
* the cut edge (appended in head mode, prepended in tail mode) so a top-down reader immediately
|
|
85
|
+
* knows which side is missing.
|
|
86
|
+
*
|
|
87
|
+
* `keep` mirrors the SDK's truncateHead/truncateTail guidance: "head" (default) for
|
|
88
|
+
* model-authored summaries/handoffs where the beginning matters; "tail" for command/CI logs where
|
|
89
|
+
* failure summaries live at the end. Deliberately perk's own byte-only util (not the SDK's
|
|
90
|
+
* line-count-aware `truncateTail`): `CapResult`'s byte fields and the scratch-pointing notice are
|
|
91
|
+
* load-bearing in `ChildStructured`/`CiCheckResult`. Pure.
|
|
85
92
|
*/
|
|
86
93
|
export function capForModel(
|
|
87
94
|
text: string,
|
|
88
95
|
cap: number = DEFAULT_MODEL_VISIBLE_CAP,
|
|
89
96
|
scratchPath: string | null = null,
|
|
97
|
+
keep: "head" | "tail" = "head",
|
|
90
98
|
): CapResult {
|
|
91
99
|
const bytesTotal = Buffer.byteLength(text, "utf8");
|
|
92
100
|
if (bytesTotal <= cap) {
|
|
93
101
|
return { shown: text, bytesTotal, bytesShown: bytesTotal, truncated: false };
|
|
94
102
|
}
|
|
95
|
-
let trimmed = text.slice(0, cap);
|
|
96
|
-
while (Buffer.byteLength(trimmed, "utf8") > cap)
|
|
103
|
+
let trimmed = keep === "head" ? text.slice(0, cap) : text.slice(-cap);
|
|
104
|
+
while (Buffer.byteLength(trimmed, "utf8") > cap) {
|
|
105
|
+
trimmed = keep === "head" ? trimmed.slice(0, -1) : trimmed.slice(1);
|
|
106
|
+
}
|
|
97
107
|
const bytesShown = Buffer.byteLength(trimmed, "utf8");
|
|
98
108
|
const omitted = bytesTotal - bytesShown;
|
|
99
109
|
const where = scratchPath ? ` Full output preserved at ${scratchPath}.` : "";
|
|
100
|
-
const
|
|
101
|
-
|
|
110
|
+
const shown =
|
|
111
|
+
keep === "head"
|
|
112
|
+
? `${trimmed}\n\n[Output truncated: ${omitted} bytes omitted.${where}]`
|
|
113
|
+
: `[Output truncated: ${omitted} bytes omitted.${where}]\n\n${trimmed}`;
|
|
114
|
+
return { shown, bytesTotal, bytesShown, truncated: true };
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
/**
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// /plan-body materialization, `run_id` mint) is the cold-door/runner's job and is a PREPARED-
|
|
13
13
|
// WORKTREE input (audit Gap 7): the worker inherits `PERK_RUN_ID` from the env and never re-mints.
|
|
14
14
|
//
|
|
15
|
+
// Budget semantics: `budget.tokens` counts FRESH WORK only — assistant `input + output` per
|
|
16
|
+
// `turn_end`. Cache reads/writes and the provider `reasoning` breakdown (a subset of `output` in
|
|
17
|
+
// pi-ai's normalization) are excluded by design; see `applyEvent`.
|
|
18
|
+
//
|
|
15
19
|
// Inverse of `extension/worker/readOnlySession.ts`: that builds a fully-isolated READ-ONLY child (loads
|
|
16
20
|
// nothing, `["read","grep","find","ls"]`); the worker is the OPPOSITE — read-write defaults + the
|
|
17
21
|
// real perk extension loaded from the worktree's `.pi/settings.json` (disk-layered settings:
|
|
@@ -23,7 +27,10 @@ import { appendFileSync, mkdtempSync } from "node:fs";
|
|
|
23
27
|
import { tmpdir } from "node:os";
|
|
24
28
|
import { join } from "node:path";
|
|
25
29
|
import { env } from "node:process";
|
|
26
|
-
|
|
30
|
+
// pi-ai's `ModelThinkingLevel` (`"off" | minimal | … | xhigh`) is the union `resolveCliModel`
|
|
31
|
+
// returns and `createAgentSessionFromServices` accepts; the pi-coding-agent root does not
|
|
32
|
+
// re-export a thinking-level type (only `ThinkingLevelChangeEntry`).
|
|
33
|
+
import type { Api, Model, ModelThinkingLevel as ThinkingLevel } from "@earendil-works/pi-ai";
|
|
27
34
|
import {
|
|
28
35
|
AuthStorage,
|
|
29
36
|
type CreateAgentSessionRuntimeFactory,
|
|
@@ -31,6 +38,7 @@ import {
|
|
|
31
38
|
createAgentSessionRuntime,
|
|
32
39
|
createAgentSessionServices,
|
|
33
40
|
ModelRegistry,
|
|
41
|
+
resolveCliModel,
|
|
34
42
|
SessionManager,
|
|
35
43
|
SettingsManager,
|
|
36
44
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -124,6 +132,11 @@ export interface DriveStageOptions {
|
|
|
124
132
|
* the first provider (a since-removed `claude-3-5-haiku` date-pin 404'd a whole remote drive).
|
|
125
133
|
*/
|
|
126
134
|
model?: Model<Api>;
|
|
135
|
+
/**
|
|
136
|
+
* Thinking level parsed from the `--model <pattern>:<level>` suffix (`resolveWorkerModel`).
|
|
137
|
+
* `undefined` ⇒ the SDK's settings-default resolution — unchanged behavior.
|
|
138
|
+
*/
|
|
139
|
+
thinkingLevel?: ThinkingLevel;
|
|
127
140
|
authStorage?: AuthStorage;
|
|
128
141
|
modelRegistry?: ModelRegistry;
|
|
129
142
|
budget: DriveBudget;
|
|
@@ -155,7 +168,14 @@ export interface DriveEvent {
|
|
|
155
168
|
role?: string;
|
|
156
169
|
stopReason?: string;
|
|
157
170
|
errorMessage?: string;
|
|
158
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Assistant token usage. `reasoning` is a provider-reported breakdown that is a **subset of
|
|
173
|
+
* `output`** on every pi-ai provider that populates it (anthropic `thinking_tokens`, google
|
|
174
|
+
* `thoughtsTokenCount` folded into `output`, openai `reasoning_tokens` inside completion/
|
|
175
|
+
* output tokens — verified @ pi-ai 0.80.5), so it is deliberately EXCLUDED from the budget
|
|
176
|
+
* sum: adding it would double-count.
|
|
177
|
+
*/
|
|
178
|
+
usage?: { input?: number; output?: number; reasoning?: number };
|
|
159
179
|
/** Assistant text/content blocks (where `[WIP:n]`/`[DONE:n]` markers live). */
|
|
160
180
|
content?: unknown;
|
|
161
181
|
};
|
|
@@ -221,6 +241,10 @@ function detailsOf(result: unknown): Record<string, unknown> | null {
|
|
|
221
241
|
* assistant token usage (the `sumAssistantTokens` pattern in objective.ts), captures the `submit`
|
|
222
242
|
* /`resolve_review_threads` terminal tool details, and records a post-acceptance model error
|
|
223
243
|
* (assistant `message_end` with `stopReason:"error"`, surfaced with retry off — audit §B #4).
|
|
244
|
+
*
|
|
245
|
+
* The token sum is `input + output` ONLY: `usage.reasoning` is a subset of `output` on every
|
|
246
|
+
* pi-ai provider that reports it (see the `DriveEvent.usage` doc), so summing it would
|
|
247
|
+
* double-count.
|
|
224
248
|
*/
|
|
225
249
|
export function applyEvent(counters: DriveCounters, event: DriveEvent): void {
|
|
226
250
|
if (event.type === "turn_end") {
|
|
@@ -488,9 +512,11 @@ export function defaultEventSink(worktree: string, runId: string): RunEventSink
|
|
|
488
512
|
/**
|
|
489
513
|
* Re-derive the stage's initial prompt from the plan-ref — the TS twin of
|
|
490
514
|
* `perk/run/launch.py._implement_prompt`/`_address_prompt`. INVARIANT: textual parity with the Python
|
|
491
|
-
* plane (asserted reciprocally in `worker.test.ts` + `tests/test_worker_prompt_parity.py`)
|
|
492
|
-
*
|
|
493
|
-
*
|
|
515
|
+
* plane (asserted reciprocally in `worker.test.ts` + `tests/test_worker_prompt_parity.py`). No
|
|
516
|
+
* skill-binding suffix is appended here: in the driven session the bindings arrive via Mechanism A
|
|
517
|
+
* (bindingDelivery.ts injects the handoff stage's render because this prompt carries no
|
|
518
|
+
* `BINDING_HEADER`) — content byte-identical to the cold door's suffix (contracts.md §8.38).
|
|
519
|
+
* Returns `null` when there is no plan-ref (nothing to prime).
|
|
494
520
|
*
|
|
495
521
|
* The implement primer's wording lives in the canonical template `prompts/stages/implement.md`,
|
|
496
522
|
* rendered by the shared seam (contracts.md §8.31); branching stays in code — only the `read_cmd`
|
|
@@ -516,7 +542,7 @@ export function initialPromptFor(
|
|
|
516
542
|
}
|
|
517
543
|
// address
|
|
518
544
|
const modelClause = classifierModel
|
|
519
|
-
? `, passing \`model: "${classifierModel}"\` on that call (the configured [subagents] review-classifier model)`
|
|
545
|
+
? `, passing \`model: "${classifierModel}"\` on that call (the configured [models.subagents] review-classifier model)`
|
|
520
546
|
: "";
|
|
521
547
|
return render("stages/address/action.md", {
|
|
522
548
|
provider,
|
|
@@ -601,8 +627,10 @@ async function defaultCreateRuntime(
|
|
|
601
627
|
services,
|
|
602
628
|
sessionManager: factoryOpts.sessionManager,
|
|
603
629
|
sessionStartEvent: factoryOpts.sessionStartEvent,
|
|
604
|
-
// `undefined` ⇒ the SDK's initial-model resolution picks the model (see `resolveAuth`)
|
|
630
|
+
// `undefined` ⇒ the SDK's initial-model resolution picks the model (see `resolveAuth`);
|
|
631
|
+
// an `undefined` thinkingLevel likewise defers to the settings default.
|
|
605
632
|
model: resolved.model,
|
|
633
|
+
thinkingLevel: opts.thinkingLevel,
|
|
606
634
|
});
|
|
607
635
|
// Name the model that will actually drive (the SDK may have picked it) — the remote step
|
|
608
636
|
// log is otherwise silent about it until a provider error.
|
|
@@ -652,6 +680,48 @@ export function resolveAuth(opts: DriveStageOptions): ResolvedAuth | null {
|
|
|
652
680
|
return { authStorage, modelRegistry, model: opts.model };
|
|
653
681
|
}
|
|
654
682
|
|
|
683
|
+
/** What an explicit `--model` flag resolves to (a thin projection of `ResolveCliModelResult`). */
|
|
684
|
+
export interface ResolvedWorkerModel {
|
|
685
|
+
model: Model<Api> | undefined;
|
|
686
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
687
|
+
/** Non-fatal resolution diagnostic (e.g. an invalid `:thinking` suffix) — surface, continue. */
|
|
688
|
+
warning: string | undefined;
|
|
689
|
+
/** Fatal: the pattern resolved to no model — fail fast, never guess. */
|
|
690
|
+
error: string | undefined;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Resolve an explicit `--model` flag with pi's OWN CLI semantics (`resolveCliModel`): fuzzy
|
|
695
|
+
* matching, bare-id resolution, `provider/pattern`, and a `:thinking` suffix — the same chain the
|
|
696
|
+
* flag's string hits in an interactive pi launch, closing the warm/cold parity gap (cf.
|
|
697
|
+
* docs/learned/workflow/execution-path-parity.md). `raw` falsy ⇒ all-undefined (the SDK's own
|
|
698
|
+
* default resolution picks the model at session creation — see `resolveAuth`). A resolution that
|
|
699
|
+
* yields neither a model nor an error is normalized to the worker's not-found error.
|
|
700
|
+
*/
|
|
701
|
+
export function resolveWorkerModel(
|
|
702
|
+
raw: string | undefined,
|
|
703
|
+
modelRegistry: ModelRegistry,
|
|
704
|
+
): ResolvedWorkerModel {
|
|
705
|
+
if (!raw) {
|
|
706
|
+
return { model: undefined, thinkingLevel: undefined, warning: undefined, error: undefined };
|
|
707
|
+
}
|
|
708
|
+
const result = resolveCliModel({ cliModel: raw, modelRegistry });
|
|
709
|
+
if (result.model === undefined && result.error === undefined) {
|
|
710
|
+
return {
|
|
711
|
+
model: undefined,
|
|
712
|
+
thinkingLevel: undefined,
|
|
713
|
+
warning: result.warning,
|
|
714
|
+
error: `model '${raw}' not found in the registry.`,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
model: result.model,
|
|
719
|
+
thinkingLevel: result.thinkingLevel,
|
|
720
|
+
warning: result.warning,
|
|
721
|
+
error: result.error,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
655
725
|
// --- the drive primitive ------------------------------------------------------------------------
|
|
656
726
|
|
|
657
727
|
/**
|