@aeondave/pi-persona 1.17.1 → 1.17.4
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 +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- 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 +47 -18
- 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/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- 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/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- 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-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Session identity registration and Pi lifecycle adapter. */
|
|
2
2
|
|
|
3
3
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { type Static, Type } from "typebox";
|
|
7
7
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
sanitizeIdentityName,
|
|
16
16
|
} from "../core/session-identity.ts";
|
|
17
17
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
18
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
18
19
|
|
|
19
20
|
export interface IdentityHost {
|
|
20
21
|
capabilities(): EffectiveCapabilities | undefined;
|
|
@@ -34,50 +35,14 @@ const AgentNameParams = Type.Object({
|
|
|
34
35
|
name: Type.String({ minLength: 1, maxLength: 32, description: "A short personal handle, separate from persona and role." }),
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getHeader?: () => { id?: string } | null;
|
|
40
|
-
getBranch?: () => readonly unknown[];
|
|
41
|
-
getEntries?: () => readonly unknown[];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function sessionManager(ctx: ExtensionContext): SessionManagerCompat {
|
|
45
|
-
return (ctx as ExtensionContext & { sessionManager?: SessionManagerCompat }).sessionManager ?? {};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Read the real session id when available. Process fallback is intentionally start-only. */
|
|
49
|
-
function readSessionId(ctx: ExtensionContext, allowProcessFallback: boolean): string | undefined {
|
|
50
|
-
const manager = sessionManager(ctx);
|
|
38
|
+
/** Pi 1.4 supplies both methods. A failed host read is unavailable, never a different owner or branch. */
|
|
39
|
+
function readSession(ctx: ExtensionContext): { id: string; entries: readonly SessionEntry[] } | undefined {
|
|
51
40
|
try {
|
|
52
|
-
const id =
|
|
53
|
-
|
|
41
|
+
const id = ctx.sessionManager.getSessionId();
|
|
42
|
+
return id.trim() ? { id, entries: ctx.sessionManager.getBranch() } : undefined;
|
|
54
43
|
} catch {
|
|
55
|
-
|
|
44
|
+
return undefined;
|
|
56
45
|
}
|
|
57
|
-
try {
|
|
58
|
-
const id = manager.getHeader?.()?.id;
|
|
59
|
-
if (typeof id === "string" && id.trim()) return id;
|
|
60
|
-
} catch {
|
|
61
|
-
/* Compatibility with light-weight session-manager test doubles. */
|
|
62
|
-
}
|
|
63
|
-
return allowProcessFallback ? `process-${process.pid}` : undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readEntries(ctx: ExtensionContext): readonly unknown[] {
|
|
67
|
-
const manager = sessionManager(ctx);
|
|
68
|
-
try {
|
|
69
|
-
const branch = manager.getBranch?.();
|
|
70
|
-
if (Array.isArray(branch)) return branch;
|
|
71
|
-
} catch {
|
|
72
|
-
/* Fall through to getEntries when a mock or older host has no branch reader. */
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const entries = manager.getEntries?.();
|
|
76
|
-
if (Array.isArray(entries)) return entries;
|
|
77
|
-
} catch {
|
|
78
|
-
/* Missing persistence only means there is no stored chosen identity to restore. */
|
|
79
|
-
}
|
|
80
|
-
return [];
|
|
81
46
|
}
|
|
82
47
|
|
|
83
48
|
function setIdentityStatus(ctx: ExtensionContext, name: string): void {
|
|
@@ -90,9 +55,7 @@ function setIdentityStatus(ctx: ExtensionContext, name: string): void {
|
|
|
90
55
|
|
|
91
56
|
function activeTools(pi: ExtensionAPI): readonly string[] {
|
|
92
57
|
try {
|
|
93
|
-
|
|
94
|
-
const names = getActiveTools?.call(pi);
|
|
95
|
-
return Array.isArray(names) ? names : [];
|
|
58
|
+
return pi.getActiveTools();
|
|
96
59
|
} catch {
|
|
97
60
|
return [];
|
|
98
61
|
}
|
|
@@ -112,7 +75,7 @@ function messageCustomType(message: AgentMessage): string | undefined {
|
|
|
112
75
|
function hasInboundPeerRequest(messages: readonly AgentMessage[]): boolean {
|
|
113
76
|
return messages.some((message) => {
|
|
114
77
|
const customType = messageCustomType(message);
|
|
115
|
-
return customType === "exocom_received"
|
|
78
|
+
return customType === "exocom_received";
|
|
116
79
|
});
|
|
117
80
|
}
|
|
118
81
|
|
|
@@ -137,39 +100,41 @@ function identityReceipt(name: string, changed: boolean): string {
|
|
|
137
100
|
|
|
138
101
|
export function installIdentity(pi: ExtensionAPI, host: IdentityHost): SessionIdentity {
|
|
139
102
|
let currentSessionId: string | undefined;
|
|
140
|
-
let currentName =
|
|
103
|
+
let currentName = "unavailable";
|
|
141
104
|
let currentChosen = false;
|
|
142
|
-
|
|
143
|
-
|
|
105
|
+
const unavailable = (ctx: ExtensionContext): void => {
|
|
106
|
+
currentSessionId = undefined;
|
|
107
|
+
currentName = "unavailable";
|
|
108
|
+
currentChosen = false;
|
|
109
|
+
setIdentityStatus(ctx, currentName);
|
|
110
|
+
};
|
|
111
|
+
const selectSession = (session: NonNullable<ReturnType<typeof readSession>>): void => {
|
|
112
|
+
if (currentSessionId === session.id) return;
|
|
113
|
+
const stored = findStoredIdentity(session.entries, session.id, host.reservedNames());
|
|
114
|
+
currentSessionId = session.id;
|
|
115
|
+
currentName = stored ?? provisionalIdentityName(session.id);
|
|
116
|
+
currentChosen = stored !== undefined;
|
|
117
|
+
};
|
|
144
118
|
|
|
145
119
|
const identity: SessionIdentity = {
|
|
146
120
|
start(ctx) {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
setIdentityStatus(ctx, currentName);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
currentSessionId = sessionId;
|
|
154
|
-
currentSessionIsFallback = detectedSessionId === undefined;
|
|
155
|
-
const stored = findStoredIdentity(readEntries(ctx), sessionId, host.reservedNames());
|
|
156
|
-
currentName = stored ?? provisionalIdentityName(sessionId);
|
|
157
|
-
currentChosen = stored !== undefined;
|
|
158
|
-
started = true;
|
|
121
|
+
const session = readSession(ctx);
|
|
122
|
+
if (!session) { unavailable(ctx); return; }
|
|
123
|
+
selectSession(session);
|
|
159
124
|
setIdentityStatus(ctx, currentName);
|
|
160
125
|
},
|
|
161
126
|
|
|
162
127
|
rename(raw, ctx) {
|
|
163
128
|
const name = sanitizeIdentityName(raw, host.reservedNames());
|
|
164
129
|
if (!name) throw new Error("identity name is empty, unsafe, generic, or reserved by a persona/agent");
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
167
|
-
|
|
130
|
+
const session = readSession(ctx);
|
|
131
|
+
if (!session) { unavailable(ctx); throw new Error("identity session is unavailable"); }
|
|
132
|
+
selectSession(session);
|
|
133
|
+
if (currentChosen && currentName === name) return currentName;
|
|
168
134
|
|
|
169
135
|
// Persistence comes first. A failed append therefore cannot leave memory or telemetry
|
|
170
136
|
// claiming a name that is absent from the session history.
|
|
171
|
-
pi.appendEntry(IDENTITY_ENTRY_CUSTOM_TYPE, makeIdentityEntry(
|
|
172
|
-
currentSessionId = sessionId;
|
|
137
|
+
pi.appendEntry(IDENTITY_ENTRY_CUSTOM_TYPE, makeIdentityEntry(session.id, name));
|
|
173
138
|
currentName = name;
|
|
174
139
|
currentChosen = true;
|
|
175
140
|
setIdentityStatus(ctx, currentName);
|
|
@@ -202,17 +167,18 @@ export function installIdentity(pi: ExtensionAPI, host: IdentityHost): SessionId
|
|
|
202
167
|
return { content: [{ type: "text", text }], details: { name, changed } };
|
|
203
168
|
},
|
|
204
169
|
renderCall(args, theme) {
|
|
205
|
-
const name = compactInlineText(args.name, { maxChars: 32 }) || "?";
|
|
170
|
+
const name = compactInlineText(args.name ?? "", { maxChars: 32 }) || "?";
|
|
206
171
|
return new Text(`${theme.fg("toolTitle", theme.bold("Agent Name "))}${theme.fg("accent", name)}`, 0, 0);
|
|
207
172
|
},
|
|
208
|
-
renderResult(result, { expanded }, theme) {
|
|
173
|
+
renderResult(result, { expanded }, theme, context) {
|
|
174
|
+
const failed = context?.isError ?? result.isError ?? false;
|
|
209
175
|
const details = result.details as unknown as { name?: unknown; changed?: unknown } | undefined;
|
|
210
|
-
const name = typeof details?.name === "string" ?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return new Text(theme.fg(
|
|
176
|
+
const name = !failed && typeof details?.name === "string" ? details.name : "";
|
|
177
|
+
// Failures share the one literal renderer (error heading, bounded preview, full expanded
|
|
178
|
+
// text); a chosen handle is a one-line purpose receipt collapsed and the complete literal
|
|
179
|
+
// receipt expanded.
|
|
180
|
+
if (!name || expanded) return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
181
|
+
return new Text(theme.fg("accent", compactInlineText(identityReceipt(name, details?.changed !== false), { maxChars: 80 })), 0, 0);
|
|
216
182
|
},
|
|
217
183
|
});
|
|
218
184
|
|
|
@@ -228,7 +194,7 @@ export function installIdentity(pi: ExtensionAPI, host: IdentityHost): SessionId
|
|
|
228
194
|
inboundPeer: hasInboundPeerRequest(event.messages),
|
|
229
195
|
...(namingTool === undefined ? {} : { namingTool }),
|
|
230
196
|
};
|
|
231
|
-
const content = buildIdentityContext(currentChosen, contextOptions);
|
|
197
|
+
const content = currentSessionId === undefined ? undefined : buildIdentityContext(currentChosen, contextOptions);
|
|
232
198
|
if (content === undefined) return { messages };
|
|
233
199
|
const contextMessage = {
|
|
234
200
|
role: "custom" as const,
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ModelsRefreshResult } from "@earendil-works/pi-ai";
|
|
7
8
|
|
|
8
9
|
import type { ModelParticipant } from "../persona/model-participants.ts";
|
|
9
10
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
10
|
-
import { ModelPicker, orderModelRefs } from "../ui/model-picker.ts";
|
|
11
|
+
import { ModelPicker, orderModelRefs, type ModelPickerMetadata } from "../ui/model-picker.ts";
|
|
11
12
|
|
|
12
13
|
export interface PersonaModelOwner {
|
|
13
14
|
name: string;
|
|
@@ -34,6 +35,8 @@ export interface PersonaModelSettingsDeps {
|
|
|
34
35
|
write: (owner: string, patch: Record<string, string | null>) => void;
|
|
35
36
|
/** Session/persona liveness guard supplied by the extension. */
|
|
36
37
|
isCurrent?: (ctx: ExtensionContext) => boolean;
|
|
38
|
+
/** Refresh adapter supplied by the catalog owner to keep derived discovery caches consistent. */
|
|
39
|
+
refreshCatalog?: (signal: AbortSignal) => Promise<ModelsRefreshResult | undefined>;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
const DONE = "Done";
|
|
@@ -124,7 +127,7 @@ export async function openPersonaModelSettings(ctx: ExtensionContext, deps: Pers
|
|
|
124
127
|
return;
|
|
125
128
|
}
|
|
126
129
|
if (choice === REFRESH) {
|
|
127
|
-
if (!(await refreshCatalog(ctx, isCurrent))) return;
|
|
130
|
+
if (!(await refreshCatalog(ctx, isCurrent, deps.refreshCatalog))) return;
|
|
128
131
|
continue;
|
|
129
132
|
}
|
|
130
133
|
if (otherAssignmentsOption && choice === otherAssignmentsOption) {
|
|
@@ -167,20 +170,26 @@ export async function openPersonaModelSettings(ctx: ExtensionContext, deps: Pers
|
|
|
167
170
|
}
|
|
168
171
|
if (action !== actions[0]) continue;
|
|
169
172
|
|
|
170
|
-
const
|
|
173
|
+
const models = availableModels(ctx);
|
|
174
|
+
const modelRefs = orderModelRefs(models.map((model) => `${model.provider}/${model.id}`), ctx.model?.provider);
|
|
171
175
|
if (modelRefs.length === 0) {
|
|
172
176
|
ctx.ui.notify("No authenticated models are available. Use /login, then choose Set or change again; saved assignments were kept.", "warning");
|
|
173
177
|
continue;
|
|
174
178
|
}
|
|
179
|
+
const current = rows[rowIndex]!;
|
|
175
180
|
const modelTitle = compactInlineText(
|
|
176
|
-
`Model for ${participant.label} · current: ${
|
|
181
|
+
`Model for ${participant.label} · current: ${current.model} (${current.source})`,
|
|
177
182
|
{ maxChars: 180 },
|
|
178
183
|
);
|
|
179
184
|
let modelChoice: string | undefined;
|
|
180
185
|
try {
|
|
181
186
|
modelChoice = ctx.mode === "tui"
|
|
182
187
|
? await ctx.ui.custom<string | undefined>(
|
|
183
|
-
(tui, theme,
|
|
188
|
+
(tui, theme, keybindings, done) => new ModelPicker(tui, theme, modelTitle, modelRefs, ctx.model?.provider, done, {
|
|
189
|
+
models,
|
|
190
|
+
...(current.currentRef !== undefined ? { currentRef: current.currentRef } : {}),
|
|
191
|
+
...(keybindings ? { keybindings } : {}),
|
|
192
|
+
}),
|
|
184
193
|
{ overlay: false },
|
|
185
194
|
)
|
|
186
195
|
: await select(ctx, modelTitle, modelRefs);
|
|
@@ -199,7 +208,7 @@ function participantRow(
|
|
|
199
208
|
saved: Readonly<Record<string, string>>,
|
|
200
209
|
staged: Readonly<Record<string, string | null>>,
|
|
201
210
|
index: number,
|
|
202
|
-
): { option: string } {
|
|
211
|
+
): { option: string; model: string; source: string; currentRef?: string } {
|
|
203
212
|
const hasPendingDirect = Object.prototype.hasOwnProperty.call(staged, participant.key);
|
|
204
213
|
const pendingDirect = hasPendingDirect ? staged[participant.key] : undefined;
|
|
205
214
|
const hasPendingShared = participant.key !== participant.agent && Object.prototype.hasOwnProperty.call(staged, participant.agent);
|
|
@@ -208,22 +217,30 @@ function participantRow(
|
|
|
208
217
|
const sharedSaved = participant.key !== participant.agent ? effectiveSavedValue(participant.agent, saved, staged) : undefined;
|
|
209
218
|
let model: string;
|
|
210
219
|
let source: string;
|
|
220
|
+
let currentRef: string | undefined;
|
|
211
221
|
if (typeof pendingDirect === "string") {
|
|
212
222
|
model = pendingDirect;
|
|
223
|
+
currentRef = pendingDirect;
|
|
213
224
|
source = "assignment (pending)";
|
|
214
225
|
} else if (pendingDirect === null) {
|
|
215
226
|
if (sharedSaved) {
|
|
216
227
|
model = sharedSaved;
|
|
228
|
+
currentRef = sharedSaved;
|
|
217
229
|
source = "shared assignment (pending fallback)";
|
|
218
230
|
} else {
|
|
219
231
|
model = "resolve lower precedence next run";
|
|
220
232
|
source = "pending reset";
|
|
221
233
|
}
|
|
234
|
+
} else if (typeof pendingShared === "string" && !directSaved && participant.source !== "inline") {
|
|
235
|
+
model = pendingShared;
|
|
236
|
+
currentRef = pendingShared;
|
|
237
|
+
source = "shared assignment (pending)";
|
|
222
238
|
} else if (hasPendingShared && pendingShared === null && !directSaved) {
|
|
223
239
|
model = "resolve lower precedence next run";
|
|
224
240
|
source = "shared reset pending";
|
|
225
241
|
} else {
|
|
226
242
|
model = participant.model ?? "none";
|
|
243
|
+
currentRef = participant.model;
|
|
227
244
|
source = participant.source;
|
|
228
245
|
}
|
|
229
246
|
const sharedNote = !directSaved && sharedSaved
|
|
@@ -235,7 +252,8 @@ function participantRow(
|
|
|
235
252
|
: hasPendingShared && pendingShared === null ? " · shared reset pending"
|
|
236
253
|
: "";
|
|
237
254
|
const option = `${index + 1}. ${safeInline(participant.label, 80)} · ${safeInline(model, 90)} (${safeInline(source, 24)})${sharedNote}${pendingNote}`;
|
|
238
|
-
return { option: compactInlineText(option, { maxChars: 200 })
|
|
255
|
+
return { option: compactInlineText(option, { maxChars: 200 }), model, source,
|
|
256
|
+
...(currentRef !== undefined ? { currentRef } : {}) };
|
|
239
257
|
}
|
|
240
258
|
|
|
241
259
|
function effectiveSavedValue(
|
|
@@ -247,20 +265,20 @@ function effectiveSavedValue(
|
|
|
247
265
|
return saved[key];
|
|
248
266
|
}
|
|
249
267
|
|
|
250
|
-
function
|
|
268
|
+
function availableModels(ctx: ExtensionContext): ModelPickerMetadata[] {
|
|
251
269
|
try {
|
|
252
|
-
const unique = new
|
|
270
|
+
const unique = new Map<string, ModelPickerMetadata>();
|
|
253
271
|
for (const model of ctx.modelRegistry.getAvailable()) {
|
|
254
272
|
if (typeof model.provider !== "string" || !model.provider.trim() || typeof model.id !== "string" || !model.id.trim()) continue;
|
|
255
|
-
unique.
|
|
273
|
+
unique.set(`${model.provider}/${model.id}`, model);
|
|
256
274
|
}
|
|
257
|
-
return
|
|
275
|
+
return [...unique.values()];
|
|
258
276
|
} catch {
|
|
259
277
|
return [];
|
|
260
278
|
}
|
|
261
279
|
}
|
|
262
280
|
|
|
263
|
-
async function refreshCatalog(ctx: ExtensionContext, isCurrent: () => boolean): Promise<boolean> {
|
|
281
|
+
async function refreshCatalog(ctx: ExtensionContext, isCurrent: () => boolean, refresh?: PersonaModelSettingsDeps["refreshCatalog"]): Promise<boolean> {
|
|
264
282
|
if (!ctx.modelRegistry.refresh) {
|
|
265
283
|
ctx.ui.notify("This Pi host cannot explicitly refresh the model catalog; existing assignments were kept.", "warning");
|
|
266
284
|
return true;
|
|
@@ -268,7 +286,7 @@ async function refreshCatalog(ctx: ExtensionContext, isCurrent: () => boolean):
|
|
|
268
286
|
const deadline = AbortSignal.timeout(20_000);
|
|
269
287
|
const signal = ctx.signal ? AbortSignal.any([ctx.signal, deadline]) : deadline;
|
|
270
288
|
try {
|
|
271
|
-
const result = await ctx.modelRegistry.refresh({ signal });
|
|
289
|
+
const result = await (refresh ? refresh(signal) : ctx.modelRegistry.refresh({ signal }));
|
|
272
290
|
if (ctx.signal?.aborted) {
|
|
273
291
|
ctx.ui.notify("Model catalog refresh was cancelled; saved assignments were kept.", "info");
|
|
274
292
|
return false;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Session-native terminal archive ownership. Owners are frozen before asynchronous work starts. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { TerminalResultArchive } from "../results/archive.ts";
|
|
5
|
+
import { resultReceipt, type ResultReceipt } from "../results/retrieval.ts";
|
|
6
|
+
import type { TerminalResultInput, TerminalStatus } from "../results/types.ts";
|
|
7
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
8
|
+
import { emptyUsage } from "../engine/stream.ts";
|
|
9
|
+
import { type AsyncRun, type AsyncRunTracker } from "../engine/async.ts";
|
|
10
|
+
|
|
11
|
+
export interface CaptureMetadata {
|
|
12
|
+
originRunId?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
status?: TerminalStatus;
|
|
15
|
+
partial?: string;
|
|
16
|
+
}
|
|
17
|
+
export type CaptureResult = (origin: string, result: AgentResult, meta?: CaptureMetadata) => ResultReceipt;
|
|
18
|
+
interface Owner {
|
|
19
|
+
id: string;
|
|
20
|
+
file: string | undefined;
|
|
21
|
+
manager: ExtensionContext["sessionManager"];
|
|
22
|
+
generation: string;
|
|
23
|
+
archive: TerminalResultArchive;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SessionResults {
|
|
27
|
+
private owner: Owner | undefined;
|
|
28
|
+
private disposed = false;
|
|
29
|
+
private readonly captures = new Map<AsyncRun, CaptureResult>();
|
|
30
|
+
private readonly detach: Array<() => void> = [];
|
|
31
|
+
|
|
32
|
+
constructor(privateApi: Pick<ExtensionAPI, "appendEntry">) {
|
|
33
|
+
this.append = (type, data) => privateApi.appendEntry(type, data);
|
|
34
|
+
}
|
|
35
|
+
private readonly append: (type: string, data: unknown) => void;
|
|
36
|
+
|
|
37
|
+
activate(ctx: ExtensionContext): TerminalResultArchive {
|
|
38
|
+
// A session_shutdown revokes the previous owner; a later session_start in the same process
|
|
39
|
+
// (the documented multi-activation lifecycle) starts a FRESH owner, never a zombie.
|
|
40
|
+
if (this.disposed) {
|
|
41
|
+
this.disposed = false;
|
|
42
|
+
for (const off of this.detach.splice(0)) {
|
|
43
|
+
try { off(); } catch { /* teardown must not block a new activation */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const manager = ctx.sessionManager;
|
|
47
|
+
// A context without a session manager (e.g. --no-session / UI-less runs) still gets an
|
|
48
|
+
// honest MEMORY-ONLY owner: bounded runtime retention, no persistence claims.
|
|
49
|
+
const id = manager ? manager.getSessionId() : `memory:${randomUUID()}`;
|
|
50
|
+
const file = manager ? manager.getSessionFile() : undefined;
|
|
51
|
+
const previous = this.owner;
|
|
52
|
+
// File identity owns persisted history. A memory owner additionally needs manager identity.
|
|
53
|
+
if (previous && previous.id === id && previous.file === file && (file !== undefined || previous.manager === manager)) return previous.archive;
|
|
54
|
+
const generation = randomUUID();
|
|
55
|
+
// Freeze all-branch history now. A storage read failure belongs to archive accounting,
|
|
56
|
+
// not to the ordinary work being launched; never retry through a mutable future context.
|
|
57
|
+
let entries: readonly unknown[] = [];
|
|
58
|
+
let historyError: unknown;
|
|
59
|
+
let historyFailed = false;
|
|
60
|
+
try { entries = manager ? manager.getEntries() : []; } catch (error) { historyError = error; historyFailed = true; }
|
|
61
|
+
const owner: Owner = {
|
|
62
|
+
id, file, manager, generation,
|
|
63
|
+
archive: new TerminalResultArchive({
|
|
64
|
+
entries: () => { if (historyFailed) throw historyError; return entries; },
|
|
65
|
+
persistent: file !== undefined,
|
|
66
|
+
append: (type, data) => {
|
|
67
|
+
if (this.owner !== owner || (manager && (manager.getSessionId() !== id || manager.getSessionFile() !== file))) {
|
|
68
|
+
throw new Error("Terminal archive owner was revoked");
|
|
69
|
+
}
|
|
70
|
+
this.append(type, data);
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
this.owner = owner;
|
|
75
|
+
return owner.archive;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get archive(): TerminalResultArchive | undefined { return this.owner?.archive; }
|
|
79
|
+
|
|
80
|
+
/** Bind at logical creation, not at settlement through lastCtx. Each scope has its own origin namespace. */
|
|
81
|
+
bind(scope: string): CaptureResult {
|
|
82
|
+
const owner = this.owner;
|
|
83
|
+
const prefix = `${owner?.generation ?? randomUUID()}:${scope}`;
|
|
84
|
+
return (origin, result, meta = {}) => {
|
|
85
|
+
if (!owner || this.owner !== owner || (owner.manager && (owner.manager.getSessionId() !== owner.id || owner.manager.getSessionFile() !== owner.file))) {
|
|
86
|
+
return { warning: "Terminal report not archived: owning session activation is no longer current." };
|
|
87
|
+
}
|
|
88
|
+
const status = meta.status ?? (result.ok ? "done" : result.failureKind === "abort" ? "stopped" : "failed");
|
|
89
|
+
const output = result.output.trim() ? result.output : meta.partial?.trim() ? meta.partial : result.error ?? "(no output)";
|
|
90
|
+
const input: TerminalResultInput = {
|
|
91
|
+
originKey: `${prefix}:${origin}`, status, agent: result.agent.slice(0, 128), body: output,
|
|
92
|
+
provenance: result.output.trim() ? "result-output" : meta.partial?.trim() ? "partial-output" : result.error ? "error" : "result-output",
|
|
93
|
+
...(meta.originRunId ? { originRunId: meta.originRunId.slice(0, 128) } : {}),
|
|
94
|
+
...(meta.label ? { label: meta.label.slice(0, 256) } : {}),
|
|
95
|
+
...(result.modelUsed ? { model: result.modelUsed.slice(0, 512) } : {}),
|
|
96
|
+
...(result.thinkingRequested ? { thinkingRequested: result.thinkingRequested } : {}),
|
|
97
|
+
...(result.thinkingEffective ? { thinkingEffective: result.thinkingEffective } : {}),
|
|
98
|
+
...(result.error ? { error: result.error.slice(0, 2048) } : {}),
|
|
99
|
+
usage: { input: result.usage.input, output: result.usage.output,
|
|
100
|
+
...(result.usage.cacheRead !== undefined ? { cacheRead: result.usage.cacheRead } : {}),
|
|
101
|
+
...(result.usage.cacheWrite !== undefined ? { cacheWrite: result.usage.cacheWrite } : {}),
|
|
102
|
+
...(result.usage.cost !== undefined ? { cost: result.usage.cost } : {}),
|
|
103
|
+
...(result.usage.turns !== undefined ? { turns: result.usage.turns } : {}),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
try { return resultReceipt(owner.archive.save(input)); }
|
|
107
|
+
catch { return { warning: "Terminal report not archived: storage callback failed." }; }
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Register before the ordinary completion listener. onCreate precedes even a synchronous thunk failure. */
|
|
112
|
+
track(tracker: AsyncRunTracker): void {
|
|
113
|
+
this.detach.push(tracker.onCreate((run) => this.captures.set(run, this.bind("tracker"))));
|
|
114
|
+
this.detach.push(tracker.onComplete((run) => {
|
|
115
|
+
const capture = this.captures.get(run);
|
|
116
|
+
this.captures.delete(run);
|
|
117
|
+
if (!capture) return;
|
|
118
|
+
const result = run.result ?? { agent: run.agent, output: "", usage: emptyUsage(), ok: false, ...(run.error ? { error: run.error } : {}) };
|
|
119
|
+
run.resultReceipt = capture(run.id, result, {
|
|
120
|
+
originRunId: run.id, ...(run.label ? { label: run.label } : {}),
|
|
121
|
+
status: run.status === "running" ? "failed" : run.status,
|
|
122
|
+
partial: run.progress.output,
|
|
123
|
+
});
|
|
124
|
+
if (run.result) run.result.resultReceipt = { ...run.resultReceipt };
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
dispose(): void {
|
|
129
|
+
this.disposed = true;
|
|
130
|
+
this.owner = undefined;
|
|
131
|
+
this.captures.clear();
|
|
132
|
+
for (const off of this.detach.splice(0)) {
|
|
133
|
+
try { off(); } catch { /* best-effort teardown */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/extension/shared.ts
CHANGED
|
@@ -9,8 +9,6 @@ import { boundCompletionSurface, MAX_COMPLETION_REPORT_CHARS } from "../engine/a
|
|
|
9
9
|
import type { InboundDecision } from "../exocom/inbound.ts";
|
|
10
10
|
import { EXOCOM } from "../exocom/limits.ts";
|
|
11
11
|
import type { DisplayPeer, ExocomInboundResult } from "../exocom/plane.ts";
|
|
12
|
-
import type { DelegateView } from "../tools/delegate.ts";
|
|
13
|
-
import type { AgentNodeStatus } from "../ui/agent-tree.ts";
|
|
14
12
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
15
13
|
|
|
16
14
|
const PI_PERSONA_TOOL_ERROR = "__piPersonaToolError";
|
|
@@ -113,10 +111,20 @@ export function exocomInboundBatchSize(items: readonly string[]): number {
|
|
|
113
111
|
|
|
114
112
|
export const EXOCOM_INBOX_MAX = 3 * EXOCOM.SENDER_MAX_MSGS;
|
|
115
113
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return
|
|
114
|
+
/** A headless session (`pi -p`): a launching tool joins its run before returning, because nothing would
|
|
115
|
+
* deliver a follow-up once the process exits. Interactive (TUI/RPC) sessions launch and return a receipt. */
|
|
116
|
+
export function isInline(ctx: { hasUI?: boolean }): boolean {
|
|
117
|
+
return ctx.hasUI !== true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The closing advice of a `timer`/`monitor` arm result. Headless `pi -p` exits when the turn settles,
|
|
122
|
+
* so a wake lands only if it fires first: ending the turn to wait for one would drop it.
|
|
123
|
+
*/
|
|
124
|
+
export function armWakeAdvice(ctx: { hasUI?: boolean } | undefined, noun: string, pending: string, interactive: string): string {
|
|
125
|
+
return ctx?.hasUI === false
|
|
126
|
+
? `Headless pi -p: the process exits when this turn settles, so this ${noun} wakes you only if it fires before then. Do not end your turn expecting it; finish inline, or report the ${pending} as pending.`
|
|
127
|
+
: interactive;
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
export function sendPersonaFollowUp(
|
|
@@ -164,6 +172,23 @@ export function shouldReportHeartbeatFailure(consecutiveFailures: number): boole
|
|
|
164
172
|
return consecutiveFailures === 1 || consecutiveFailures % 10 === 0;
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
/** One bounded, terminal-safe cause line for an operator toast. Error `message`s interpolate raw
|
|
176
|
+
* provider/filesystem text (multi-line, ANSI-capable) — every toast that embeds one goes through
|
|
177
|
+
* this so the notification stays a single sanitized line. The untruncated text keeps its other
|
|
178
|
+
* surface (stderr, /doctor, the tool result). */
|
|
179
|
+
export function toastCause(message: string, maxChars = 160): string {
|
|
180
|
+
return compactInlineText(message, { maxChars });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A fair prefix of a name list, bounded for one toast line: `"a, b, c, +N more"`. Discovery
|
|
184
|
+
* reports (collisions, seeded files, installed personas) grow with the user's own directory, so
|
|
185
|
+
* an unbounded `join(", ")` would let the toast wrap over many terminal rows. */
|
|
186
|
+
export function compactNameList(names: readonly string[], max = 8): string {
|
|
187
|
+
const clean = names.map((name) => compactInlineText(name, { maxChars: 80 })).filter((name) => name.length > 0);
|
|
188
|
+
if (clean.length <= max) return clean.join(", ");
|
|
189
|
+
return `${clean.slice(0, max).join(", ")}, +${clean.length - max} more`;
|
|
190
|
+
}
|
|
191
|
+
|
|
167
192
|
export function fenceIntercomOutcome(out: { text: string; details: { action: string; messages?: unknown[] } }, fence: (t: string) => string): string {
|
|
168
193
|
const untrusted = out.details.action === "inbox" && (out.details.messages?.length ?? 0) > 0;
|
|
169
194
|
return untrusted ? fence(out.text) : out.text;
|
|
@@ -171,5 +196,5 @@ export function fenceIntercomOutcome(out: { text: string; details: { action: str
|
|
|
171
196
|
|
|
172
197
|
export function coachingDisabledHint(personaName: string | undefined): string {
|
|
173
198
|
const who = sanitizeDisplayLabel(personaName ?? "default", "default");
|
|
174
|
-
return `(coaching is OFF for persona "${who}" — sub-agents get no contact_supervisor tool, so the message bus is empty. To
|
|
199
|
+
return `(coaching is OFF for persona "${who}" — sub-agents get no contact_supervisor tool, so the message bus is empty. To redirect one use action "steer"; results return on their own. To exchange messages, add \`coaching: true\` or switch to a coaching persona.)`;
|
|
175
200
|
}
|