@aeondave/pi-persona 1.17.3 → 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 +57 -36
- package/CHANGELOG.md +106 -1
- package/README.md +32 -16
- package/docs/ARCHITECTURE.md +199 -48
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +33 -8
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +65 -14
- package/docs/STRATEGIES.md +6 -0
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +33 -23
- package/src/bus/broker/client.ts +151 -30
- package/src/bus/broker/host.ts +130 -11
- package/src/bus/broker/messages.ts +35 -5
- package/src/bus/contact-transport.ts +8 -0
- package/src/bus/contact.ts +118 -40
- package/src/bus/peers.ts +23 -6
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +14 -4
- package/src/core/capabilities.ts +1 -1
- package/src/core/companion.ts +64 -0
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/session-runs.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +100 -20
- package/src/engine/async.ts +53 -11
- package/src/engine/child.ts +11 -2
- package/src/engine/inproc.ts +98 -47
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +6 -0
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +45 -17
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/companion.ts +38 -0
- package/src/extension/engine.ts +40 -29
- package/src/extension/hooks.ts +954 -931
- package/src/extension/identity.ts +41 -75
- package/src/extension/memory-owner.ts +54 -0
- package/src/extension/mind-worker.ts +129 -0
- package/src/extension/results.ts +138 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2922 -2849
- package/src/loader.ts +11 -3
- package/src/orchestration/agent-attempt.ts +15 -0
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +125 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -19
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +6 -7
- package/src/persona/persona.ts +1 -1
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/council.ts +40 -23
- package/src/tools/delegate-tool.ts +50 -14
- package/src/tools/delegate.ts +13 -6
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom.ts +20 -3
- package/src/tools/flow.ts +24 -8
- package/src/tools/intercom-tool.ts +98 -14
- package/src/tools/models.ts +15 -0
- package/src/tools/monitor.ts +20 -1
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +23 -2
- package/src/ui/activity-rows.ts +34 -0
- package/src/ui/agent-overlay.ts +23 -13
- package/src/ui/agent-tree.ts +67 -6
- package/src/ui/literal-tool-result.ts +1 -1
- package/src/ui/model-picker.ts +1 -1
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/session-cards.ts +191 -14
- package/src/ui/strategy-leg.ts +40 -0
package/src/ui/agent-tree.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { compactTokens } from "../core/format.ts";
|
|
2
|
+
import { effortFullName, effortSigla } from "../core/leg-thinking.ts";
|
|
2
3
|
import { formatDuration } from "../core/time.ts";
|
|
4
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
3
5
|
import type { ChildUsage } from "../engine/stream.ts";
|
|
4
6
|
import { compactInlineText } from "./presentation.ts";
|
|
5
7
|
import { formatUsage } from "./usage.ts";
|
|
@@ -13,7 +15,7 @@ import { formatUsage } from "./usage.ts";
|
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export type AgentNodeStatus = "running" | "done" | "failed" | "stopped";
|
|
16
|
-
export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer";
|
|
18
|
+
export type AgentNodeKind = "subagent" | "delegate" | "council" | "flow" | "phase" | "monitor" | "timer" | "activity";
|
|
17
19
|
|
|
18
20
|
export interface AgentNode {
|
|
19
21
|
id: string;
|
|
@@ -25,6 +27,13 @@ export interface AgentNode {
|
|
|
25
27
|
/** Configured agent definition (operator/reviewer/…), if known. */
|
|
26
28
|
agent?: string;
|
|
27
29
|
model?: string;
|
|
30
|
+
/** Requested per-leg thinking effort, known at launch (the spec-level value the launch row
|
|
31
|
+
* carried). Rendered as the compact sigla right after the model; absent/off render nothing. */
|
|
32
|
+
thinking?: ThinkingLevel;
|
|
33
|
+
/** Observed effective Pi effort from the terminal result. Set ONLY from a real
|
|
34
|
+
* AgentResult.thinkingEffective (never invented); when it diverges from `thinking`, the row
|
|
35
|
+
* renders the honest `xh→h` form. */
|
|
36
|
+
thinkingEffective?: ThinkingLevel;
|
|
28
37
|
/** A short trailing annotation (usage, current activity), shown after the label. */
|
|
29
38
|
detail: string | undefined;
|
|
30
39
|
/** The agent's output buffer, shown when the user drills into the node. */
|
|
@@ -69,10 +78,16 @@ export function isWakeSource(node: Pick<AgentNode, "kind">): boolean {
|
|
|
69
78
|
return node.kind === "monitor" || node.kind === "timer";
|
|
70
79
|
}
|
|
71
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
|
+
|
|
72
86
|
/** The kind-aware glyph shared by every surface (tree, overlay): a running timer is ⏰, a running
|
|
73
87
|
* monitor ◉; everything else, settled wake rows included, uses the ordinary status glyph. */
|
|
74
88
|
export function nodeGlyph(node: Pick<AgentNode, "status" | "kind">): string {
|
|
75
89
|
if (node.status === "running") {
|
|
90
|
+
if (node.kind === "activity") return "◈";
|
|
76
91
|
if (node.kind === "timer") return "⏰";
|
|
77
92
|
if (node.kind === "monitor") return "◉";
|
|
78
93
|
}
|
|
@@ -88,6 +103,28 @@ export interface RenderOptions {
|
|
|
88
103
|
waitingForSupervisor?: ReadonlySet<string>;
|
|
89
104
|
}
|
|
90
105
|
|
|
106
|
+
/** The frozen compact effort tag for a tree/overlay row: ` · xh` after the model, or ` · xh→h`
|
|
107
|
+
* when the terminal result reported a DIFFERENT observed effective level. `off`/absent render
|
|
108
|
+
* nothing — absence means off, and an observation is never invented: the divergence form shows
|
|
109
|
+
* only when both sides are known and genuinely differ (an observed `off` never leaves a dangling
|
|
110
|
+
* `→`). Pure. */
|
|
111
|
+
export function effortTag(requested: ThinkingLevel | undefined, effective?: ThinkingLevel): string {
|
|
112
|
+
const req = requested !== undefined ? effortSigla(requested) : "";
|
|
113
|
+
if (!req) return "";
|
|
114
|
+
const obs = effective !== undefined ? effortSigla(effective) : "";
|
|
115
|
+
return obs && obs !== req ? ` · ${req}→${obs}` : ` · ${req}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The F9 drill-in line for a leg's effort — FULL names, the drill-in has the room (the compact
|
|
119
|
+
* siglas live only on the rows): `thinking: requested xhigh → observed high`. Same honesty rules
|
|
120
|
+
* as {@link effortTag}; the line is omitted entirely when nothing was requested. Pure. */
|
|
121
|
+
export function thinkingDetailLine(requested: ThinkingLevel | undefined, effective?: ThinkingLevel): string {
|
|
122
|
+
const req = requested !== undefined ? effortFullName(requested) : "";
|
|
123
|
+
if (!req) return "";
|
|
124
|
+
const obs = effective !== undefined && effective !== requested ? effortFullName(effective) : "";
|
|
125
|
+
return obs ? `thinking: requested ${req} → observed ${obs}` : `thinking: requested ${req}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
91
128
|
/** Whether `detail` is the literal "queued" marker a seeded-but-not-yet-live leg carries — shared
|
|
92
129
|
* by `runningAnnotation` (suppresses the clock) and `telemetryStatus` (`extension.ts`, reports the
|
|
93
130
|
* `queued` state). One predicate so the two readings of the marker cannot drift apart. */
|
|
@@ -105,6 +142,7 @@ export function isQueuedMarker(detail: string | undefined): boolean {
|
|
|
105
142
|
export function runningAnnotation(node: Pick<AgentNode, "status" | "startedAt" | "lastAdvanceAt" | "detail" | "kind" | "dueAt">, now: number, stallMs: number, waitingForSupervisor = false): string | undefined {
|
|
106
143
|
if (node.status !== "running" || node.startedAt === undefined) return undefined;
|
|
107
144
|
if (isQueuedMarker(node.detail)) return undefined;
|
|
145
|
+
if (node.kind === "activity") return formatDuration(now - node.startedAt);
|
|
108
146
|
if (waitingForSupervisor) return "waiting for supervisor";
|
|
109
147
|
if (node.dueAt !== undefined) {
|
|
110
148
|
const left = node.dueAt - now;
|
|
@@ -179,7 +217,10 @@ export function renderAgentTree(nodes: AgentNode[], opts: RenderOptions = {}): s
|
|
|
179
217
|
const isRoot = parentId === undefined;
|
|
180
218
|
const isLast = i === kids.length - 1;
|
|
181
219
|
const branch = isRoot ? "" : isLast ? "└─ " : "├─ ";
|
|
182
|
-
const
|
|
220
|
+
const base = safeInline(node.label) || safeInline(node.id) || "agent";
|
|
221
|
+
// The compact effort sigla rides AFTER the label clamp — the existing per-part clipping is
|
|
222
|
+
// untouched; the appended field is at most ` · xh→h` wide.
|
|
223
|
+
const label = `${base}${effortTag(node.thinking, node.thinkingEffective)}`;
|
|
183
224
|
const detailText = node.detail ? safeInline(node.detail) : "";
|
|
184
225
|
const clock = opts.now !== undefined ? runningAnnotation(node, opts.now, opts.stallMs ?? 0, opts.waitingForSupervisor?.has(node.id) ?? false) : undefined;
|
|
185
226
|
const tail = [detailText, clock].filter((s): s is string => Boolean(s)).join(" · ");
|
|
@@ -202,6 +243,8 @@ function wakeDigestLine(hidden: readonly AgentNode[], now: number | undefined):
|
|
|
202
243
|
const monitors = hidden.filter((node) => node.kind === "monitor").length;
|
|
203
244
|
const timers = hidden.filter((node) => node.kind === "timer").length;
|
|
204
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")}`);
|
|
205
248
|
if (monitors > 0) parts.push(`${nodeGlyph({ status: "running", kind: "monitor" })} ${monitors} monitor${monitors === 1 ? "" : "s"}`);
|
|
206
249
|
if (timers > 0) parts.push(`${nodeGlyph({ status: "running", kind: "timer" })} ${timers} timer${timers === 1 ? "" : "s"}`);
|
|
207
250
|
if (now !== undefined) {
|
|
@@ -243,7 +286,7 @@ export function renderAgentTreeSummary(nodes: AgentNode[], maxRows = 8, opts: Re
|
|
|
243
286
|
// same parent→children order, so a row's flat index doubles as its position in the rendered list.
|
|
244
287
|
const flat = flattenTree(nodes);
|
|
245
288
|
const hiddenWake = (from: number): AgentNode[] =>
|
|
246
|
-
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"));
|
|
247
290
|
let visibleCount = Math.max(0, limit - (failureLine !== undefined ? 2 : 1));
|
|
248
291
|
let hidden = hiddenWake(visibleCount);
|
|
249
292
|
if (hidden.length > 0) {
|
|
@@ -266,7 +309,9 @@ export interface AddNodeInput {
|
|
|
266
309
|
status?: AgentNodeStatus;
|
|
267
310
|
kind?: AgentNodeKind;
|
|
268
311
|
agent?: string;
|
|
269
|
-
model?: string;
|
|
312
|
+
model?: string | undefined;
|
|
313
|
+
thinking?: ThinkingLevel | undefined;
|
|
314
|
+
thinkingEffective?: ThinkingLevel | undefined;
|
|
270
315
|
detail?: string;
|
|
271
316
|
output?: string;
|
|
272
317
|
startedAt?: number;
|
|
@@ -278,7 +323,9 @@ export interface AgentNodePatch {
|
|
|
278
323
|
status?: AgentNodeStatus;
|
|
279
324
|
kind?: AgentNodeKind;
|
|
280
325
|
agent?: string;
|
|
281
|
-
model?: string;
|
|
326
|
+
model?: string | undefined;
|
|
327
|
+
thinking?: ThinkingLevel | undefined;
|
|
328
|
+
thinkingEffective?: ThinkingLevel | undefined;
|
|
282
329
|
detail?: string;
|
|
283
330
|
output?: string;
|
|
284
331
|
startedAt?: number;
|
|
@@ -292,7 +339,19 @@ function applyPatch(node: AgentNode, patch: AgentNodePatch): void {
|
|
|
292
339
|
if (patch.status) node.status = patch.status;
|
|
293
340
|
if (patch.kind !== undefined) node.kind = patch.kind;
|
|
294
341
|
if (patch.agent !== undefined) node.agent = patch.agent;
|
|
295
|
-
|
|
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
|
+
}
|
|
296
355
|
if (patch.detail !== undefined) node.detail = patch.detail;
|
|
297
356
|
if (patch.output !== undefined) node.output = patch.output;
|
|
298
357
|
if (patch.startedAt !== undefined) node.startedAt = patch.startedAt;
|
|
@@ -333,6 +392,8 @@ export class AgentTree {
|
|
|
333
392
|
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
334
393
|
...(input.agent !== undefined ? { agent: input.agent } : {}),
|
|
335
394
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
395
|
+
...(input.thinking !== undefined ? { thinking: input.thinking } : {}),
|
|
396
|
+
...(input.thinkingEffective !== undefined ? { thinkingEffective: input.thinkingEffective } : {}),
|
|
336
397
|
...(input.output !== undefined ? { output: input.output } : {}),
|
|
337
398
|
...(input.dueAt !== undefined ? { dueAt: input.dueAt } : {}),
|
|
338
399
|
};
|
|
@@ -5,7 +5,7 @@ import { compactVisibleText, sanitizeTerminalText } from "./presentation.ts";
|
|
|
5
5
|
|
|
6
6
|
interface LiteralResult {
|
|
7
7
|
content: readonly { type: string; text?: string }[];
|
|
8
|
-
/**
|
|
8
|
+
/** Direct internal renderer calls carry this; Pi passes it in the render context. */
|
|
9
9
|
isError?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
package/src/ui/model-picker.ts
CHANGED
|
@@ -217,7 +217,7 @@ export class ModelPicker extends Container implements Focusable {
|
|
|
217
217
|
return;
|
|
218
218
|
} else {
|
|
219
219
|
const before = this.input.getValue();
|
|
220
|
-
// Input has no per-instance keybindings API (Pi 1.
|
|
220
|
+
// Input has no per-instance keybindings API (Pi 1.4). Scope its global
|
|
221
221
|
// lookup to this synchronous call and restore it even if handling throws.
|
|
222
222
|
const global = getKeybindings();
|
|
223
223
|
try {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Persisted orchestration launch requests. Rendering never consults live persona or model settings. */
|
|
2
|
+
import { resolveLegThinking } from "../core/leg-thinking.ts";
|
|
3
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
4
|
+
import type { ModelParticipant, ModelSource } from "../persona/model-participants.ts";
|
|
5
|
+
import { compactInlineText, sanitizeTerminalText } from "./presentation.ts";
|
|
6
|
+
|
|
7
|
+
export interface OrchestrationLaunchParticipant {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
agent: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
modelSource: ModelSource;
|
|
13
|
+
thinkingRequested?: ThinkingLevel;
|
|
14
|
+
setupPending: boolean;
|
|
15
|
+
param?: string;
|
|
16
|
+
phase?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OrchestrationLaunchSnapshot {
|
|
20
|
+
participants: OrchestrationLaunchParticipant[];
|
|
21
|
+
unavailable?: true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function snapshotOrchestrationParticipants(
|
|
25
|
+
participants: readonly ModelParticipant[],
|
|
26
|
+
opts: { agentThinking(agent: string): ThinkingLevel | undefined; childThinking: unknown; pendingKeys: ReadonlySet<string> },
|
|
27
|
+
): OrchestrationLaunchSnapshot {
|
|
28
|
+
return { participants: participants.map((p) => {
|
|
29
|
+
const agentThinking = opts.agentThinking(p.agent);
|
|
30
|
+
const thinkingRequested = resolveLegThinking(p, agentThinking ? { thinking: agentThinking } : {}, opts.childThinking);
|
|
31
|
+
return { key: p.key, label: p.label, agent: p.agent, modelSource: p.source, setupPending: !p.assigned && opts.pendingKeys.has(p.key),
|
|
32
|
+
...(p.model ? { model: p.model } : {}), ...(thinkingRequested ? { thinkingRequested } : {}), ...(p.param ? { param: p.param } : {}) };
|
|
33
|
+
}) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Three individually bounded rows collapsed; expansion retains every participant and full model ref. */
|
|
37
|
+
export function orchestrationLaunchLines(snapshot: OrchestrationLaunchSnapshot | undefined, expanded: boolean): string[] {
|
|
38
|
+
if (!snapshot || snapshot.unavailable) return ["Launch metadata unavailable"];
|
|
39
|
+
const rows = expanded ? snapshot.participants : snapshot.participants.slice(0, 3);
|
|
40
|
+
const lines = rows.map((p) => {
|
|
41
|
+
const clean = (value: string, maxChars: number): string => expanded ? sanitizeTerminalText(value) : compactInlineText(value, { maxChars });
|
|
42
|
+
const label = clean(`${p.phase ? `${p.phase} / ` : ""}${p.label}${p.param ? ` [${p.param}]` : ""}`, 48);
|
|
43
|
+
const model = p.model ? clean(p.model, 64) : "unknown";
|
|
44
|
+
const modelText = p.setupPending ? `model pending (current default ${model}; setup may change)` : `requested model ${model}`;
|
|
45
|
+
return ` ${label} · ${modelText} · requested effort ${p.thinkingRequested ?? "unknown"}`;
|
|
46
|
+
});
|
|
47
|
+
if (!expanded && snapshot.participants.length > rows.length) lines.push(` +${snapshot.participants.length - rows.length} more · expand for launch details`);
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
package/src/ui/session-cards.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Operator-only presentation of existing session payloads. No delivery, storage or tool shells. */
|
|
2
2
|
import type { ExtensionAPI, Theme, ThemeBg, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
4
4
|
import { formatDuration } from "../core/time.ts";
|
|
5
5
|
import { neutralizeMonitorUnicode } from "../core/monitor-output.ts";
|
|
6
6
|
import { createReportBody } from "./report-body.ts";
|
|
@@ -25,6 +25,14 @@ interface Card {
|
|
|
25
25
|
markdown?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The delegate-card-family frame every surface in this file shares: a bold semantic
|
|
29
|
+
* `<Event> · <datum>` title, two-space-indented compact content lines, and the dim
|
|
30
|
+
* expand hint only when compact actually hides content. Payloads are never modified —
|
|
31
|
+
* this is projection-only (expanded still shows the complete sanitized payload). */
|
|
32
|
+
const INDENT = " ";
|
|
33
|
+
/** Compact budget: title + 5 body rows + optional hint keeps every collapsed card ≤ 7 rows. */
|
|
34
|
+
const COMPACT_BODY_ROWS = 4;
|
|
35
|
+
|
|
28
36
|
/** All seven surfaces share semantic chrome. Literal paths style at paint time; report bodies
|
|
29
37
|
* use native Markdown's invalidatable layout cache. Pi rebuilds callbacks on transcript invalidation. */
|
|
30
38
|
function createSessionCard(card: Card, expanded: boolean, theme: Theme, detailHint: () => string, outputPad = 0): Component {
|
|
@@ -45,12 +53,19 @@ function createSessionCard(card: Card, expanded: boolean, theme: Theme, detailHi
|
|
|
45
53
|
const padding = Math.min(Math.max(0, Math.floor(outputPad)), Math.max(0, Math.floor((width - 1) / 2)));
|
|
46
54
|
const innerWidth = Math.max(1, width - padding * 2);
|
|
47
55
|
const colors = COLORS[card.state];
|
|
48
|
-
|
|
56
|
+
// The indent only applies where a clipped content column still fits; every compact line
|
|
57
|
+
// is clipped to the remaining columns so indent + text never wrap or spill mid-word.
|
|
58
|
+
const bodyPad = innerWidth > INDENT.length ? INDENT : "";
|
|
59
|
+
const contentWidth = Math.max(1, innerWidth - bodyPad.length);
|
|
60
|
+
const preview = expanded ? undefined : card.preview(contentWidth);
|
|
49
61
|
const body = expanded ? card.full : preview?.text ?? "(no output)";
|
|
62
|
+
const bodyLines = sanitizeTerminalText(body || "(no output)").split("\n").map((line) => expanded
|
|
63
|
+
? theme.fg("toolOutput", line)
|
|
64
|
+
: theme.fg("toolOutput", `${bodyPad}${truncateToWidth(line, contentWidth)}`));
|
|
50
65
|
const lines = [
|
|
51
66
|
theme.fg(colors.fg, theme.bold(truncateToWidth(sanitizeTerminalText(card.title), innerWidth))),
|
|
52
|
-
...(card.metadata ? [theme.fg("dim", truncateToWidth(card.metadata
|
|
53
|
-
...
|
|
67
|
+
...(card.metadata ? [theme.fg("dim", truncateToWidth(`${bodyPad}${sanitizeTerminalText(card.metadata)}`, innerWidth))] : []),
|
|
68
|
+
...bodyLines,
|
|
54
69
|
...(preview?.truncated ? [theme.fg("dim", truncateToWidth(detailHint(), innerWidth))] : []),
|
|
55
70
|
];
|
|
56
71
|
// Text owns wrapping and full-row padding/background, including embedded foreground resets.
|
|
@@ -65,11 +80,15 @@ function preview(full: string, maxLines: number, maxColumns: number): Card["prev
|
|
|
65
80
|
return (width) => compactVisibleText(full, { maxLines, maxLineChars: Math.min(width, maxColumns) });
|
|
66
81
|
}
|
|
67
82
|
|
|
83
|
+
/** Single source of truth for the settlement head grammar, shared by state selection and the
|
|
84
|
+
* compact projection below. The generator is `buildCompletionReport` in src/engine/async.ts. */
|
|
85
|
+
const SETTLEMENT_HEAD = /^(?:\[pi-persona\] )?(\d+) async runs? settled — (\d+) done, (\d+) failed(?:, (\d+) stopped)?$/;
|
|
86
|
+
|
|
68
87
|
/** Only this exact runtime-owned aggregate is evidence of settlement. No keyword scan of output,
|
|
69
88
|
* asks, timers, monitor event bodies, or peers. Legacy/unknown follow-ups remain informational. */
|
|
70
89
|
function followUpState(full: string): { state: State; status: string } {
|
|
71
90
|
const first = full.split("\n", 1)[0] ?? "";
|
|
72
|
-
const summary =
|
|
91
|
+
const summary = SETTLEMENT_HEAD.exec(first);
|
|
73
92
|
if (summary) {
|
|
74
93
|
const total = Number(summary[1]);
|
|
75
94
|
const done = Number(summary[2]);
|
|
@@ -101,6 +120,149 @@ interface ResultEntry { label?: string; content?: string; ok?: boolean; stoppedA
|
|
|
101
120
|
interface ContentEntry { content?: string; kind?: "routine" | "stall" }
|
|
102
121
|
interface RunFailureEntry { id: string; label: string; agent: string; model?: string; durationMs?: number; error: string }
|
|
103
122
|
|
|
123
|
+
/** One run a settlement report names, projected to `<name> · <status>[ · <duration>]`. The
|
|
124
|
+
* display name already carries the model (`runDisplayName`: "label · model"). */
|
|
125
|
+
interface SettledRunLine { name: string; status: "done" | "failed" | "stopped"; duration?: string }
|
|
126
|
+
|
|
127
|
+
/** Boilerplate the settlement generator interleaves between the per-run lines. Every entry is
|
|
128
|
+
* drift-guarded in tests against live `buildCompletionReport` output (src/engine/async.ts):
|
|
129
|
+
* receipts header + `run-x: …` reference lines, the effort header, salvage `↩` block headers,
|
|
130
|
+
* the shared untrusted-fence header (core/fence.ts quoteUntrusted) and its `> `-quoted payload
|
|
131
|
+
* lines, the failed-leg guidance sentence, the truncation marker and retention/tail notes. */
|
|
132
|
+
const SETTLEMENT_SKIP: readonly RegExp[] = [
|
|
133
|
+
/^Terminal reports \(/,
|
|
134
|
+
/^run-\S+: /,
|
|
135
|
+
/^Thinking \(not provider cognition\):$/,
|
|
136
|
+
/^↩ /,
|
|
137
|
+
/^Sub-agent output \(untrusted data\):$/,
|
|
138
|
+
/^>/,
|
|
139
|
+
/^Handle each failure deliberately: /,
|
|
140
|
+
/^\[\.\.\. .*?\]$/,
|
|
141
|
+
/^⚠ /,
|
|
142
|
+
/^Run ids in this report: /,
|
|
143
|
+
];
|
|
144
|
+
const SETTLEMENT_DONE = /^✅ (\S+) \((.+?)\) done(?: in (.+?))?:$/;
|
|
145
|
+
const SETTLEMENT_STOPPED_SECTION = /^⏹ \d+ stopped:$/;
|
|
146
|
+
const SETTLEMENT_FAILED_SECTION = /^❌ \d+ failed:$/;
|
|
147
|
+
/** Quoted (fence) bullets carry the stopped/failed runs; the section header above them names the status. */
|
|
148
|
+
const SETTLEMENT_BULLET = /^> ?• (\S+) \((.+?)\)(?: after (.+?))?: /;
|
|
149
|
+
|
|
150
|
+
/** Project the compact per-run lines of a settlement report, or undefined when the body is not
|
|
151
|
+
* exactly the known generator shape (head line + recognized/skipped lines only). A duration is
|
|
152
|
+
* kept only when it is short (≤ 8 columns, e.g. "12s"/"2m 5s"); width clipping handles the rest. */
|
|
153
|
+
function parseSettlementRuns(full: string): { counts: string; runs: SettledRunLine[] } | undefined {
|
|
154
|
+
const lines = full.split("\n");
|
|
155
|
+
const head = SETTLEMENT_HEAD.exec(lines[0] ?? "");
|
|
156
|
+
if (!head) return undefined;
|
|
157
|
+
const done = Number(head[2]);
|
|
158
|
+
const failed = Number(head[3]);
|
|
159
|
+
const stopped = Number(head[4] ?? 0);
|
|
160
|
+
const counts = `${done} done, ${failed} failed${stopped > 0 ? `, ${stopped} stopped` : ""}`;
|
|
161
|
+
let section: "failed" | "stopped" | undefined;
|
|
162
|
+
const runs: SettledRunLine[] = [];
|
|
163
|
+
const seen = new Set<string>();
|
|
164
|
+
const add = (name: string | undefined, status: SettledRunLine["status"], duration: string | undefined) => {
|
|
165
|
+
if (!name) return;
|
|
166
|
+
const key = `${status}:${name}`;
|
|
167
|
+
if (seen.has(key)) return;
|
|
168
|
+
seen.add(key);
|
|
169
|
+
runs.push({ name, status, ...(duration !== undefined && visibleWidth(duration) <= 8 ? { duration } : {}) });
|
|
170
|
+
};
|
|
171
|
+
for (const line of lines.slice(1)) {
|
|
172
|
+
const bullet = SETTLEMENT_BULLET.exec(line);
|
|
173
|
+
if (bullet) {
|
|
174
|
+
if (section === undefined) return undefined;
|
|
175
|
+
add(bullet[2], section, bullet[3]);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.trim().length === 0 || SETTLEMENT_SKIP.some((skip) => skip.test(line))) continue;
|
|
179
|
+
const doneLine = SETTLEMENT_DONE.exec(line);
|
|
180
|
+
if (doneLine) {
|
|
181
|
+
section = undefined;
|
|
182
|
+
add(doneLine[2], "done", doneLine[3]);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (SETTLEMENT_STOPPED_SECTION.test(line)) { section = "stopped"; continue; }
|
|
186
|
+
if (SETTLEMENT_FAILED_SECTION.test(line)) { section = "failed"; continue; }
|
|
187
|
+
return undefined; // unknown shape → the caller falls back to the generic bounded preview
|
|
188
|
+
}
|
|
189
|
+
return { counts, runs };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Compact projection of a settlement report: counts line + one line per named run, capped at
|
|
193
|
+
* COMPACT_BODY_ROWS (a `… +N more` tail row replaces the overflow). Undefined run list = fallback. */
|
|
194
|
+
function settlementPreview(full: string): Card["preview"] | undefined {
|
|
195
|
+
const parsed = parseSettlementRuns(full);
|
|
196
|
+
if (!parsed || parsed.runs.length === 0) return undefined;
|
|
197
|
+
return () => {
|
|
198
|
+
const lines = [parsed.counts, ...parsed.runs.map((run) => `${run.name} · ${run.status}${run.duration ? ` · ${run.duration}` : ""}`)];
|
|
199
|
+
const visible = lines.length <= COMPACT_BODY_ROWS ? lines : [...lines.slice(0, COMPACT_BODY_ROWS - 1), `… +${lines.length - (COMPACT_BODY_ROWS - 1)} more`];
|
|
200
|
+
return { text: visible.join("\n"), truncated: true };
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface StatusRunLine { name: string; detail: string; queued: boolean }
|
|
205
|
+
|
|
206
|
+
/** Skip-list for the peek-digest boilerplate the status card's title/frame replaces. Each entry
|
|
207
|
+
* is drift-guarded in tests against live `buildPeekDigest`/`buildCheckIn` output (src/engine/async.ts):
|
|
208
|
+
* the check-in header, the cumulative-token footnote and the omitted-runs tail. The
|
|
209
|
+
* `Async runs: N (M running)` header is recognized below (the title carries its counts). */
|
|
210
|
+
const STATUS_SKIP: readonly RegExp[] = [
|
|
211
|
+
/^Async activity · (?:routine check-in|possible stall)$/,
|
|
212
|
+
/^Cumulative tokens show /,
|
|
213
|
+
/^… \d+ additional async runs omitted/,
|
|
214
|
+
];
|
|
215
|
+
const STATUS_RUNNING = /^\[([^\]]+)\] (.+?) — running \((?:(\d+) turns, )?(.+?) cumulative input \+ output tokens\)(.*)$/;
|
|
216
|
+
const STATUS_SETTLED = /^\[([^\]]+)\] (.+?) — (done|failed|stopped)(?: (?:in|after) (.+?))?(?::.*)?$/;
|
|
217
|
+
const STATUS_HEADER_COUNTS = /^Async runs: \d+ \((\d+) running\)$/;
|
|
218
|
+
|
|
219
|
+
/** Parse one peek digest into per-run compact lines (`<name> · 53t · 216k` for running legs,
|
|
220
|
+
* `<name> · done · 12s` for settled ones), or undefined when the shape is not the generator's. */
|
|
221
|
+
function parseStatusRuns(full: string): { running: number; queued: number; runs: StatusRunLine[] } | undefined {
|
|
222
|
+
let running: number | undefined;
|
|
223
|
+
let queued = 0;
|
|
224
|
+
const runs: StatusRunLine[] = [];
|
|
225
|
+
for (const line of full.split("\n")) {
|
|
226
|
+
if (line.trim().length === 0 || STATUS_SKIP.some((skip) => skip.test(line))) continue;
|
|
227
|
+
const head = STATUS_HEADER_COUNTS.exec(line);
|
|
228
|
+
if (head) {
|
|
229
|
+
if (running !== undefined) return undefined;
|
|
230
|
+
running = Number(head[1]);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const runLine = STATUS_RUNNING.exec(line);
|
|
234
|
+
if (runLine) {
|
|
235
|
+
const [, , name, turns, tokens, suffix] = runLine;
|
|
236
|
+
const isQueued = /· queued \(/.test(suffix ?? "");
|
|
237
|
+
if (isQueued) queued++;
|
|
238
|
+
const usage = `${tokens ?? "0"} cumulative input + output tokens`;
|
|
239
|
+
const detail = isQueued ? `queued · ${usage}` : `${turns !== undefined ? `${turns} turns · ` : ""}${usage}`;
|
|
240
|
+
runs.push({ name: name ?? "", detail, queued: isQueued });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const settledLine = STATUS_SETTLED.exec(line);
|
|
244
|
+
if (settledLine) {
|
|
245
|
+
const [, , name, status, duration] = settledLine;
|
|
246
|
+
runs.push({ name: name ?? "", detail: duration !== undefined ? `${status} · ${duration}` : status ?? "", queued: false });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
return undefined; // unknown shape → the caller falls back to the generic bounded preview
|
|
250
|
+
}
|
|
251
|
+
if (running === undefined || runs.length === 0) return undefined;
|
|
252
|
+
return { running, queued, runs };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Compact projection of the periodic status digest, capped like the settlement preview. */
|
|
256
|
+
function statusPreview(full: string): Card["preview"] | undefined {
|
|
257
|
+
const parsed = parseStatusRuns(full);
|
|
258
|
+
if (!parsed) return undefined;
|
|
259
|
+
return () => {
|
|
260
|
+
const lines = parsed.runs.map((run) => `${run.name} · ${run.detail}`);
|
|
261
|
+
const visible = lines.length <= COMPACT_BODY_ROWS ? lines : [...lines.slice(0, COMPACT_BODY_ROWS - 1), `… +${lines.length - (COMPACT_BODY_ROWS - 1)} more`];
|
|
262
|
+
return { text: visible.join("\n"), truncated: true };
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
104
266
|
export interface SessionCardOptions {
|
|
105
267
|
detailHint: () => string;
|
|
106
268
|
/** Existing bounded/fenced runtime formatter, injected to keep UI independent of tool wiring. */
|
|
@@ -119,7 +281,12 @@ export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRend
|
|
|
119
281
|
// Only the validated settlement envelope is a pi-persona report. Unknown is NOT
|
|
120
282
|
// Markdown: monitor stdout/stderr, timers and operational notices share this type.
|
|
121
283
|
const markdown = customType === "exocom_received" || status !== "info";
|
|
122
|
-
|
|
284
|
+
const projected = customType === "pi-persona" ? settlementPreview(full) : undefined;
|
|
285
|
+
return sessionCard({
|
|
286
|
+
title: customType === "pi-persona" ? `Update · ${status}` : "Exocom · inbound",
|
|
287
|
+
state, full, markdown,
|
|
288
|
+
preview: projected ?? preview(full, 4, 100),
|
|
289
|
+
}, expanded, theme, outputPad);
|
|
123
290
|
});
|
|
124
291
|
}
|
|
125
292
|
pi.registerEntryRenderer(MONITOR_LOG_ENTRY, (entry, { expanded }, theme) => {
|
|
@@ -127,8 +294,9 @@ export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRend
|
|
|
127
294
|
if (!record) return undefined;
|
|
128
295
|
const { state, status } = monitorState(record);
|
|
129
296
|
return sessionCard({
|
|
130
|
-
title: `Monitor
|
|
131
|
-
state,
|
|
297
|
+
title: `Monitor · ${compactInlineText(record.entry.label)}`,
|
|
298
|
+
state, metadata: `${record.outcome} · ${status} · ${compactInlineText(record.entry.id)}`,
|
|
299
|
+
full: sanitizeTerminalText(options.monitorLog(record)),
|
|
132
300
|
preview: (width) => ({ text: `${compactInlineText(neutralizeMonitorUnicode(record.text), { maxChars: Math.min(width, 160) })}\n${compactInlineText(record.entry.id)} · monitor result`, truncated: true }),
|
|
133
301
|
}, expanded, theme);
|
|
134
302
|
});
|
|
@@ -137,7 +305,7 @@ export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRend
|
|
|
137
305
|
if (!data) return undefined;
|
|
138
306
|
const metadata = [compactInlineText(data.agent, { maxChars: 48 }), ...(data.model ? [compactInlineText(data.model, { maxChars: 96 })] : []), ...(data.durationMs !== undefined ? [formatDuration(data.durationMs)] : [])].join(" · ");
|
|
139
307
|
return sessionCard({
|
|
140
|
-
title:
|
|
308
|
+
title: `Failed · ${compactInlineText(data.label || data.agent, { maxChars: 80 })}`, state: "error", metadata,
|
|
141
309
|
full: `Run ${compactInlineText(data.id)}\n${sanitizeTerminalText(data.error)}`,
|
|
142
310
|
preview: (width) => ({ text: compactVisibleText(summarizeFailure(data.error), { maxLines: 2, maxLineChars: Math.min(width, 100) }).text, truncated: true }),
|
|
143
311
|
}, expanded, theme);
|
|
@@ -150,8 +318,9 @@ export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRend
|
|
|
150
318
|
const state: State = stopped ? "attention" : data.ok === true ? "success" : data.ok === false ? "error" : "neutral";
|
|
151
319
|
const status = stopped ? `stopped · checkpoint ${compactInlineText(data.stoppedAtGate ?? "", { maxChars: 32 })}` : data.ok === true ? "complete" : data.ok === false ? "failed" : "unknown";
|
|
152
320
|
const cause = compactVisibleText([data.failureKind, data.error].filter(Boolean).join(" · "), { maxLines: 1, maxLineChars: 80 }).text;
|
|
321
|
+
const label = compactInlineText(data.label ?? "", { maxChars: 96 });
|
|
153
322
|
return sessionCard({
|
|
154
|
-
title: `${
|
|
323
|
+
title: `${label || "Result"} · ${status}${state === "error" && cause !== "(no output)" ? ` · ${cause}` : ""}`,
|
|
155
324
|
state, full, markdown: true, preview: preview(full, 3, 100),
|
|
156
325
|
}, expanded, theme);
|
|
157
326
|
});
|
|
@@ -159,14 +328,17 @@ export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRend
|
|
|
159
328
|
if (!entry.data) return undefined;
|
|
160
329
|
const full = sanitizeTerminalText(entry.data.content || "⟢ pi-persona · checkpoint");
|
|
161
330
|
const lines = full.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
162
|
-
const header = compactInlineText(lines[0]?.replace(/^⟢\s*/, "") ?? "", { maxChars: 96 }) || "
|
|
331
|
+
const header = compactInlineText(lines[0]?.replace(/^⟢\s*/, "") ?? "", { maxChars: 96 }) || "checkpoint";
|
|
332
|
+
// Drop the generator's own "pi-persona · checkpoint" boilerplate from the datum; a bare
|
|
333
|
+
// checkpoint header leaves the title as just the event word.
|
|
334
|
+
const datum = header === "checkpoint" ? "" : header.replace(/^pi-persona · checkpoint\b\??/, "").replace(/^pi-persona · /, "").trim();
|
|
163
335
|
const details = lines.slice(1);
|
|
164
336
|
const trigger = details.find((line) => line.startsWith("Trigger:"));
|
|
165
337
|
const action = details.find((line) => /^(?:Action|Verify|Otherwise):/.test(line));
|
|
166
338
|
const selected = [trigger, action].filter((line): line is string => line !== undefined);
|
|
167
339
|
const chosen = selected.length > 0 ? selected : details.slice(0, 2);
|
|
168
340
|
return sessionCard({
|
|
169
|
-
title:
|
|
341
|
+
title: datum ? `Checkpoint · ${datum}` : "Checkpoint", state: "attention", full,
|
|
170
342
|
preview: (width) => {
|
|
171
343
|
const result = preview(chosen.join("\n"), 2, 100)(width);
|
|
172
344
|
return { text: result.text, truncated: result.truncated || details.some((line) => !chosen.includes(line)) };
|
|
@@ -177,10 +349,15 @@ export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRend
|
|
|
177
349
|
if (!entry.data) return undefined;
|
|
178
350
|
const full = sanitizeTerminalText(entry.data.content || "Async activity · routine check-in");
|
|
179
351
|
const stall = entry.data.kind === "stall";
|
|
352
|
+
const event = stall ? "Stall" : "Status";
|
|
180
353
|
const digest = full.split("\n").slice(1).filter((line) => line.trim().length > 0);
|
|
354
|
+
const parsed = parseStatusRuns(full);
|
|
181
355
|
return sessionCard({
|
|
182
|
-
title:
|
|
183
|
-
|
|
356
|
+
title: parsed
|
|
357
|
+
? `${event} · ${parsed.running} running${parsed.queued > 0 ? ` · ${parsed.queued} queued` : ""}`
|
|
358
|
+
: `${event} · check-in`,
|
|
359
|
+
state: stall ? "attention" : "neutral", full,
|
|
360
|
+
preview: parsed ? statusPreview(full)! : (width) => {
|
|
184
361
|
const result = preview(digest.slice(0, 2).join("\n") || full, 2, 140)(width);
|
|
185
362
|
return { text: result.text, truncated: result.truncated || digest.length > 2 };
|
|
186
363
|
},
|
|
@@ -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
|
+
}
|