@aeondave/pi-persona 1.17.4 → 1.17.5
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/AGENTS.md +13 -0
- package/CHANGELOG.md +30 -0
- package/README.md +11 -5
- package/docs/ARCHITECTURE.md +109 -18
- package/docs/STRATEGIES.md +6 -0
- package/package.json +2 -2
- package/src/bridge.ts +8 -15
- package/src/bus/broker/client.ts +32 -9
- package/src/bus/broker/host.ts +7 -3
- package/src/bus/broker/messages.ts +4 -1
- package/src/bus/contact-transport.ts +8 -0
- package/src/bus/contact.ts +10 -7
- package/src/bus/peers.ts +8 -6
- package/src/core/companion.ts +64 -0
- package/src/engine/adapter.ts +18 -2
- package/src/engine/async.ts +4 -1
- package/src/engine/child.ts +1 -0
- package/src/engine/inproc.ts +11 -1
- package/src/extension/companion.ts +38 -0
- package/src/extension/engine.ts +20 -8
- package/src/extension/hooks.ts +18 -9
- package/src/extension/memory-owner.ts +54 -0
- package/src/extension/mind-worker.ts +129 -0
- package/src/extension/results.ts +14 -12
- package/src/extension.ts +117 -134
- package/src/orchestration/agent-attempt.ts +15 -0
- package/src/orchestration/flow-run.ts +4 -1
- package/src/orchestration/sdk.ts +19 -15
- package/src/persona/orchestrate.ts +4 -7
- package/src/telemetry/contract.ts +1 -1
- package/src/ui/activity-rows.ts +34 -0
- package/src/ui/agent-overlay.ts +13 -11
- package/src/ui/agent-tree.ts +30 -11
- package/src/ui/strategy-leg.ts +40 -0
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
import type { RunLimits } from "../core/capabilities.ts";
|
|
8
8
|
import { makeRoster, type RosterMember } from "../orchestration/roster.ts";
|
|
9
9
|
import {
|
|
10
|
-
type AgentProgress,
|
|
11
|
-
type AgentStatus,
|
|
12
10
|
makeSDK,
|
|
13
11
|
type SDKDeps,
|
|
14
|
-
type SteerFn,
|
|
15
12
|
type StrategyEngine,
|
|
16
13
|
type StrategyInput,
|
|
17
14
|
} from "../orchestration/sdk.ts";
|
|
@@ -72,14 +69,14 @@ export interface RunStrategyDeps {
|
|
|
72
69
|
log?: (message: string) => void;
|
|
73
70
|
/** Per-agent lifecycle, for live UI (which roster agent is running/done + its result).
|
|
74
71
|
* `key` is a run-unique display id (disambiguates same-agent roster-role members). */
|
|
75
|
-
onAgentStatus?:
|
|
72
|
+
onAgentStatus?: SDKDeps["onAgentStatus"];
|
|
76
73
|
onAgentResult?: SDKDeps["onAgentResult"];
|
|
77
74
|
/** Per-agent streaming progress (rolling output), for live UI. */
|
|
78
|
-
onAgentProgress?:
|
|
75
|
+
onAgentProgress?: SDKDeps["onAgentProgress"];
|
|
79
76
|
/** Called as each agent starts with a handle to abort just that agent (UI stop). */
|
|
80
|
-
onAgentStart?:
|
|
77
|
+
onAgentStart?: SDKDeps["onAgentStart"];
|
|
81
78
|
/** Called once an agent is live with a handle to steer it (in-process engine only). */
|
|
82
|
-
onAgentSteerable?:
|
|
79
|
+
onAgentSteerable?: SDKDeps["onAgentSteerable"];
|
|
83
80
|
/** See `SDKDeps.canSpawn` — every strategy spawn, not only `delegate`. */
|
|
84
81
|
canSpawn?: (agent: string) => boolean;
|
|
85
82
|
}
|
|
@@ -4,7 +4,7 @@ export const TELEMETRY_EVENT_NAME = "pi:telemetry" as const;
|
|
|
4
4
|
export const LEGACY_TELEMETRY_EVENT_NAME = "pi-persona:telemetry" as const;
|
|
5
5
|
export const LEGACY_TELEMETRY_VERSION = 1 as const;
|
|
6
6
|
export const TELEMETRY_PRODUCER_ID = "pi-persona" as const;
|
|
7
|
-
export const TELEMETRY_PRODUCER_VERSION = "1.17.
|
|
7
|
+
export const TELEMETRY_PRODUCER_VERSION = "1.17.5" as const;
|
|
8
8
|
|
|
9
9
|
export type InstanceStatus = "active" | "idle" | "stopped" | "stale" | (string & {});
|
|
10
10
|
export type AgentKind = "subagent" | "delegate" | "council" | "flow" | "phase" | (string & {});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Bounded local-work rows. A staged plan is waiting data, never a running agent. */
|
|
2
|
+
import type { CompanionActivity } from "../core/companion.ts";
|
|
3
|
+
import type { AddNodeInput } from "./agent-tree.ts";
|
|
4
|
+
|
|
5
|
+
export const MAX_COMPANION_ACTIVITIES = 8;
|
|
6
|
+
interface ActivityRecord { event: CompanionActivity; startedAt: number }
|
|
7
|
+
|
|
8
|
+
function description(event: CompanionActivity): string {
|
|
9
|
+
const scope = `${event.target} memory`;
|
|
10
|
+
if (event.state === "staged") return `${scope} · proposal prepared; explicit apply required`;
|
|
11
|
+
if (event.state === "failed") return `${scope} · ${event.phase} failed; see the memory result`;
|
|
12
|
+
if (event.state === "done") return `${scope} · ${event.phase === "inspect" ? "inspection complete; ready for review" : event.phase === "apply" ? "staged plan applied" : "plan validation complete"}`;
|
|
13
|
+
return `${scope} · ${event.phase === "inspect" ? "inspecting" : event.phase === "stage" ? "validating consolidation" : "applying staged plan"}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ActivityRows {
|
|
17
|
+
private readonly records = new Map<string, ActivityRecord>();
|
|
18
|
+
accept(event: CompanionActivity, now: number): { node: AddNodeInput; removed: string[] } | undefined {
|
|
19
|
+
const id = `activity:pi-persona-mind:${event.activityId}`;
|
|
20
|
+
const existing = this.records.get(id);
|
|
21
|
+
if (existing && (existing.event.phase !== event.phase || existing.event.target !== event.target || existing.event.state !== "running")) return undefined;
|
|
22
|
+
const removed: string[] = [];
|
|
23
|
+
if (!existing && this.records.size >= MAX_COMPANION_ACTIVITIES) {
|
|
24
|
+
const victim = [...this.records].find(([, record]) => record.event.state !== "running");
|
|
25
|
+
if (!victim) return undefined; // never silently evict active local work
|
|
26
|
+
this.records.delete(victim[0]); removed.push(victim[0]);
|
|
27
|
+
}
|
|
28
|
+
const record = { event, startedAt: existing?.startedAt ?? now };
|
|
29
|
+
this.records.set(id, record);
|
|
30
|
+
const detail = description(event);
|
|
31
|
+
return { removed, node: { id, label: "Mind", kind: "activity", status: event.state === "running" ? "running" : event.state === "failed" ? "failed" : "done", detail, output: `${detail}.\nLocal memory operation; no sub-agent or model execution is implied.\nInspect the memory tool or command result for the full receipt.`, startedAt: record.startedAt, lastAdvanceAt: now } };
|
|
32
|
+
}
|
|
33
|
+
clear(): string[] { const ids = [...this.records.keys()]; this.records.clear(); return ids; }
|
|
34
|
+
}
|
package/src/ui/agent-overlay.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
wrapTextWithAnsi,
|
|
24
24
|
} from "@earendil-works/pi-tui";
|
|
25
25
|
|
|
26
|
-
import { type AgentNode, type AgentTree, type FlatRow, deriveWaitingNodeIds, effortTag, flattenTree, isWakeSource, nodeGlyph, runningAnnotation, thinkingDetailLine } from "./agent-tree.ts";
|
|
26
|
+
import { type AgentNode, type AgentTree, type FlatRow, deriveWaitingNodeIds, effortTag, flattenTree, isWakeSource, isNonAgentNode, nodeGlyph, runningAnnotation, thinkingDetailLine } from "./agent-tree.ts";
|
|
27
27
|
import { LiveClock } from "./live-clock.ts";
|
|
28
28
|
import { visibleWindow } from "./model-picker.ts";
|
|
29
29
|
import { compactInlineText, OPEN_SEQUENCE_TAIL, sanitizeTerminalText } from "./presentation.ts";
|
|
@@ -234,7 +234,7 @@ export class AgentOverlay extends Container {
|
|
|
234
234
|
*/
|
|
235
235
|
private selectedLeaf(leaves: FlatRow[]): FlatRow | undefined {
|
|
236
236
|
const anchored = this.selectedId !== undefined ? leaves.find((r) => r.node.id === this.selectedId) : undefined;
|
|
237
|
-
const leaf = anchored ?? leaves.find((r) => !
|
|
237
|
+
const leaf = anchored ?? leaves.find((r) => !isNonAgentNode(r.node)) ?? leaves[0];
|
|
238
238
|
if (this.selectedId !== undefined && anchored === undefined && leaf !== undefined) this.aimLost = true;
|
|
239
239
|
this.selectedId = leaf?.node.id;
|
|
240
240
|
return leaf;
|
|
@@ -305,7 +305,7 @@ export class AgentOverlay extends Container {
|
|
|
305
305
|
}
|
|
306
306
|
this.addChild(new Spacer(1));
|
|
307
307
|
const stoppable = selected !== undefined && this.canStopNode(selected);
|
|
308
|
-
const steerable = selected !== undefined && selected.status === "running" && (this.actions.canSteer?.(selected.id) ?? false);
|
|
308
|
+
const steerable = selected !== undefined && !isNonAgentNode(selected) && selected.status === "running" && (this.actions.canSteer?.(selected.id) ?? false);
|
|
309
309
|
this.addChild(new Text(t.fg("dim", `↑↓ navigate ⏎ open${this.hintLine("list", stoppable, steerable, this.stoppableIds(nodes).length)} esc close`), 1, 0));
|
|
310
310
|
this.renderNotice();
|
|
311
311
|
}
|
|
@@ -383,8 +383,9 @@ export class AgentOverlay extends Container {
|
|
|
383
383
|
// A wake source is armed, not a live agent: it has no steer handle, so the agent-only "● live"
|
|
384
384
|
// badge and the steer-unavailable hint below would both be wrong for it.
|
|
385
385
|
const wake = isWakeSource(node);
|
|
386
|
+
const localActivity = node.kind === "activity";
|
|
386
387
|
this.addChild(
|
|
387
|
-
new Text(`${nodeGlyph(node)} ${t.fg("accent", t.bold(safeInline(node.label) || "agent"))}${live ? t.fg("success", wake ? " ● armed" : " ● live") : ""}`, 1, 0),
|
|
388
|
+
new Text(`${nodeGlyph(node)} ${t.fg("accent", t.bold(safeInline(node.label) || "agent"))}${live ? t.fg("success", localActivity ? " ● local work" : wake ? " ● armed" : " ● live") : ""}`, 1, 0),
|
|
388
389
|
);
|
|
389
390
|
if (node.detail) this.addChild(new Text(t.fg("dim", safeInline(node.detail)), 1, 0));
|
|
390
391
|
// The drill-in has room for FULL effort names (the rows stay compact): an observed level
|
|
@@ -409,7 +410,7 @@ export class AgentOverlay extends Container {
|
|
|
409
410
|
for (const line of all.slice(start, end)) this.addChild(new Text(t.fg("toolOutput", line), 1, 0));
|
|
410
411
|
if (this.detailScroll > 0) this.addChild(new Text(t.fg("dim", `▼ ${this.detailScroll} newer`), 1, 0));
|
|
411
412
|
|
|
412
|
-
const steerable = live && (this.actions.canSteer?.(node.id) ?? false);
|
|
413
|
+
const steerable = live && !isNonAgentNode(node) && (this.actions.canSteer?.(node.id) ?? false);
|
|
413
414
|
const stoppable = this.canStopNode(node);
|
|
414
415
|
if (this.steering && !steerable) this.steering = false; // agent finished mid-compose
|
|
415
416
|
this.addChild(new Spacer(1));
|
|
@@ -421,7 +422,7 @@ export class AgentOverlay extends Container {
|
|
|
421
422
|
} else {
|
|
422
423
|
this.addChild(new Text(t.fg("dim", `esc back · ↑↓ scroll${this.hintLine("detail", stoppable, steerable)}`), 1, 0));
|
|
423
424
|
this.renderNotice();
|
|
424
|
-
if (live && !steerable && !
|
|
425
|
+
if (live && !steerable && !isNonAgentNode(node)) {
|
|
425
426
|
this.addChild(new Text(t.fg("dim", "(steer unavailable: no live handle yet, or this engine/broker does not expose one)"), 1, 0));
|
|
426
427
|
}
|
|
427
428
|
}
|
|
@@ -456,7 +457,8 @@ export class AgentOverlay extends Container {
|
|
|
456
457
|
} else if (hadNotice) this.refresh();
|
|
457
458
|
return;
|
|
458
459
|
}
|
|
459
|
-
|
|
460
|
+
const detailNode = this.tree.snapshot().find((node) => node.id === this.detailId);
|
|
461
|
+
if (keyData === "s" && detailNode && !isNonAgentNode(detailNode) && (this.actions.canSteer?.(this.detailId) ?? false)) {
|
|
460
462
|
this.steering = true;
|
|
461
463
|
this.steerBuffer = "";
|
|
462
464
|
this.refresh();
|
|
@@ -492,7 +494,7 @@ export class AgentOverlay extends Container {
|
|
|
492
494
|
// Steer straight from the list: drill into the selected agent with the
|
|
493
495
|
// compose line already open (same gate as the detail view's `s`).
|
|
494
496
|
const leaf = this.aimedLeaf(leaves);
|
|
495
|
-
if (leaf && leaf.node.status === "running" && (this.actions.canSteer?.(leaf.node.id) ?? false)) {
|
|
497
|
+
if (leaf && !isNonAgentNode(leaf.node) && leaf.node.status === "running" && (this.actions.canSteer?.(leaf.node.id) ?? false)) {
|
|
496
498
|
this.detailId = leaf.node.id;
|
|
497
499
|
this.detailScroll = 0;
|
|
498
500
|
this.steering = true;
|
|
@@ -512,8 +514,8 @@ export class AgentOverlay extends Container {
|
|
|
512
514
|
/** Whether an agent can be stopped right now: it must be running, and the caller's `canStop`
|
|
513
515
|
* gate (defaulting to "yes" when omitted) must agree. Shared by both hints and `tryStop`, so
|
|
514
516
|
* the hint that advertises "x stop" and the handler that actually gates it never diverge. */
|
|
515
|
-
private canStopNode(node: Pick<AgentNode, "id" | "status">): boolean {
|
|
516
|
-
return node.status === "running" && (this.actions.canStop?.(node.id) ?? true);
|
|
517
|
+
private canStopNode(node: Pick<AgentNode, "id" | "status" | "kind">): boolean {
|
|
518
|
+
return node.kind !== "activity" && node.status === "running" && (this.actions.canStop?.(node.id) ?? true);
|
|
517
519
|
}
|
|
518
520
|
|
|
519
521
|
/** The agents `X` would stop: every leaf that `x` could stop, wake sources excluded. `X` means
|
|
@@ -521,7 +523,7 @@ export class AgentOverlay extends Container {
|
|
|
521
523
|
* only by an explicit `x` on its own row. One list feeds the hint count, the confirmation prompt
|
|
522
524
|
* and the bulk stop, so the three always agree. */
|
|
523
525
|
private stoppableIds(snap: AgentNode[] = this.tree.snapshot()): string[] {
|
|
524
|
-
return this.leafRows(snap).filter((r) => !
|
|
526
|
+
return this.leafRows(snap).filter((r) => !isNonAgentNode(r.node) && this.canStopNode(r.node)).map((r) => r.node.id);
|
|
525
527
|
}
|
|
526
528
|
|
|
527
529
|
/** The trailing "x stop"/"X stop all"/"s steer" hint fragment for an action line — the list's
|
package/src/ui/agent-tree.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { formatUsage } from "./usage.ts";
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
export type AgentNodeStatus = "running" | "done" | "failed" | "stopped";
|
|
18
|
-
export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer";
|
|
18
|
+
export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer" | "activity";
|
|
19
19
|
|
|
20
20
|
export interface AgentNode {
|
|
21
21
|
id: string;
|
|
@@ -78,10 +78,16 @@ export function isWakeSource(node: Pick<AgentNode, "kind">): boolean {
|
|
|
78
78
|
return node.kind === "monitor" || node.kind === "timer";
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Local companion work shares the view but is neither an agent nor a wake source. */
|
|
82
|
+
export function isNonAgentNode(node: Pick<AgentNode, "kind">): boolean {
|
|
83
|
+
return isWakeSource(node) || node.kind === "activity";
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
/** The kind-aware glyph shared by every surface (tree, overlay): a running timer is ⏰, a running
|
|
82
87
|
* monitor ◉; everything else, settled wake rows included, uses the ordinary status glyph. */
|
|
83
88
|
export function nodeGlyph(node: Pick<AgentNode, "status" | "kind">): string {
|
|
84
89
|
if (node.status === "running") {
|
|
90
|
+
if (node.kind === "activity") return "◈";
|
|
85
91
|
if (node.kind === "timer") return "⏰";
|
|
86
92
|
if (node.kind === "monitor") return "◉";
|
|
87
93
|
}
|
|
@@ -136,6 +142,7 @@ export function isQueuedMarker(detail: string | undefined): boolean {
|
|
|
136
142
|
export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail" | "kind" | "dueAt">, now: number, stallMs: number, waitingForSupervisor = false): string | undefined {
|
|
137
143
|
if (node.status !== "running" || node.startedAt === undefined) return undefined;
|
|
138
144
|
if (isQueuedMarker(node.detail)) return undefined;
|
|
145
|
+
if (node.kind === "activity") return formatDuration(now - node.startedAt);
|
|
139
146
|
if (waitingForSupervisor) return "waiting for supervisor";
|
|
140
147
|
if (node.dueAt !== undefined) {
|
|
141
148
|
const left = node.dueAt - now;
|
|
@@ -236,6 +243,8 @@ function wakeDigestLine(hidden: readonly AgentNode[], now: number | undefined):
|
|
|
236
243
|
const monitors = hidden.filter((node) => node.kind === "monitor").length;
|
|
237
244
|
const timers = hidden.filter((node) => node.kind === "timer").length;
|
|
238
245
|
const parts: string[] = [];
|
|
246
|
+
const activities = hidden.filter((node) => node.kind === "activity");
|
|
247
|
+
if (activities.length > 0) parts.push(`◈ Mind · ${safeInline(activities[activities.length - 1]!.detail ?? "local memory work")}`);
|
|
239
248
|
if (monitors > 0) parts.push(`${nodeGlyph({ status: "running", kind: "monitor" })} ${monitors} monitor${monitors === 1 ? "" : "s"}`);
|
|
240
249
|
if (timers > 0) parts.push(`${nodeGlyph({ status: "running", kind: "timer" })} ${timers} timer${timers === 1 ? "" : "s"}`);
|
|
241
250
|
if (now !== undefined) {
|
|
@@ -277,7 +286,7 @@ export function renderAgentTreeSummary(nodes: AgentNode[], maxRows = 8, opts: Re
|
|
|
277
286
|
// same parent→children order, so a row's flat index doubles as its position in the rendered list.
|
|
278
287
|
const flat = flattenTree(nodes);
|
|
279
288
|
const hiddenWake = (from: number): AgentNode[] =>
|
|
280
|
-
flat.slice(from).map((row) => row.node).filter((node) => isWakeSource(node) && node.status === "running");
|
|
289
|
+
flat.slice(from).map((row) => row.node).filter((node) => node.kind === "activity" || (isWakeSource(node) && node.status === "running"));
|
|
281
290
|
let visibleCount = Math.max(0, limit - (failureLine !== undefined ? 2 : 1));
|
|
282
291
|
let hidden = hiddenWake(visibleCount);
|
|
283
292
|
if (hidden.length > 0) {
|
|
@@ -300,9 +309,9 @@ export interface AddNodeInput {
|
|
|
300
309
|
status?: AgentNodeStatus;
|
|
301
310
|
kind?: AgentNodeKind;
|
|
302
311
|
agent?: string;
|
|
303
|
-
model?: string;
|
|
304
|
-
thinking?: ThinkingLevel;
|
|
305
|
-
thinkingEffective?: ThinkingLevel;
|
|
312
|
+
model?: string | undefined;
|
|
313
|
+
thinking?: ThinkingLevel | undefined;
|
|
314
|
+
thinkingEffective?: ThinkingLevel | undefined;
|
|
306
315
|
detail?: string;
|
|
307
316
|
output?: string;
|
|
308
317
|
startedAt?: number;
|
|
@@ -314,9 +323,9 @@ export interface AgentNodePatch {
|
|
|
314
323
|
status?: AgentNodeStatus;
|
|
315
324
|
kind?: AgentNodeKind;
|
|
316
325
|
agent?: string;
|
|
317
|
-
model?: string;
|
|
318
|
-
thinking?: ThinkingLevel;
|
|
319
|
-
thinkingEffective?: ThinkingLevel;
|
|
326
|
+
model?: string | undefined;
|
|
327
|
+
thinking?: ThinkingLevel | undefined;
|
|
328
|
+
thinkingEffective?: ThinkingLevel | undefined;
|
|
320
329
|
detail?: string;
|
|
321
330
|
output?: string;
|
|
322
331
|
startedAt?: number;
|
|
@@ -330,9 +339,19 @@ function applyPatch(node: AgentNode, patch: AgentNodePatch): void {
|
|
|
330
339
|
if (patch.status) node.status = patch.status;
|
|
331
340
|
if (patch.kind !== undefined) node.kind = patch.kind;
|
|
332
341
|
if (patch.agent !== undefined) node.agent = patch.agent;
|
|
333
|
-
|
|
334
|
-
if (patch
|
|
335
|
-
|
|
342
|
+
// Omission preserves metadata; explicit undefined clears a previous attempt's intent/observation.
|
|
343
|
+
if (Object.hasOwn(patch, "model")) {
|
|
344
|
+
if (patch.model === undefined) delete node.model;
|
|
345
|
+
else node.model = patch.model;
|
|
346
|
+
}
|
|
347
|
+
if (Object.hasOwn(patch, "thinking")) {
|
|
348
|
+
if (patch.thinking === undefined) delete node.thinking;
|
|
349
|
+
else node.thinking = patch.thinking;
|
|
350
|
+
}
|
|
351
|
+
if (Object.hasOwn(patch, "thinkingEffective")) {
|
|
352
|
+
if (patch.thinkingEffective === undefined) delete node.thinkingEffective;
|
|
353
|
+
else node.thinkingEffective = patch.thinkingEffective;
|
|
354
|
+
}
|
|
336
355
|
if (patch.detail !== undefined) node.detail = patch.detail;
|
|
337
356
|
if (patch.output !== undefined) node.output = patch.output;
|
|
338
357
|
if (patch.startedAt !== undefined) node.startedAt = patch.startedAt;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Model intent for live strategy rows, resolved from exact attempt identity and frozen run settings. */
|
|
2
|
+
import { isThinkingLevel, type ThinkingLevel } from "../core/types.ts";
|
|
3
|
+
import type { AgentAttemptIdentity } from "../orchestration/agent-attempt.ts";
|
|
4
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
5
|
+
import { participantKey } from "../persona/model-participants.ts";
|
|
6
|
+
|
|
7
|
+
export interface StrategyModels {
|
|
8
|
+
readonly assigned: Readonly<Record<string, string>>;
|
|
9
|
+
readonly agents: Readonly<Record<string, string>>;
|
|
10
|
+
readonly sessionModel?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function snapshotStrategyModels(input: { assigned?: Record<string, string>; agents: readonly { name: string; model?: string }[]; sessionModel?: string }): StrategyModels {
|
|
14
|
+
return Object.freeze({ assigned: Object.freeze({ ...input.assigned }),
|
|
15
|
+
agents: Object.freeze(Object.fromEntries(input.agents.flatMap((a) => a.model ? [[a.name, a.model]] : []))),
|
|
16
|
+
...(input.sessionModel ? { sessionModel: input.sessionModel } : {}) });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StrategyLegMetadata {
|
|
20
|
+
agent: string;
|
|
21
|
+
model: string | undefined;
|
|
22
|
+
thinking: ThinkingLevel | undefined;
|
|
23
|
+
thinkingEffective: ThinkingLevel | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A bare observed id preserves a known provider only when it exactly names the same requested id. */
|
|
27
|
+
function observedModel(observed: string | undefined, requested: string | undefined): string | undefined {
|
|
28
|
+
const value = observed?.trim();
|
|
29
|
+
if (!value) return requested;
|
|
30
|
+
if (!value.includes("/") && requested?.includes("/") && requested.slice(requested.indexOf("/") + 1) === value) return requested;
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function strategyLegMetadata(identity: AgentAttemptIdentity, models: StrategyModels, result?: AgentResult): StrategyLegMetadata {
|
|
35
|
+
const requested = identity.model ?? models.assigned[participantKey(identity.agent, identity.role)]
|
|
36
|
+
?? models.assigned[identity.agent] ?? models.agents[identity.agent] ?? models.sessionModel;
|
|
37
|
+
return { agent: identity.agent, model: observedModel(result?.modelUsed, requested),
|
|
38
|
+
thinking: isThinkingLevel(result?.thinkingRequested) ? result.thinkingRequested : identity.thinking,
|
|
39
|
+
thinkingEffective: isThinkingLevel(result?.thinkingEffective) ? result.thinkingEffective : undefined };
|
|
40
|
+
}
|