@pontia/pi-client-plugin 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -25
- package/package.json +30 -23
- package/src/context.ts +0 -104
- package/src/control-socket.ts +456 -0
- package/src/discovery.ts +2 -63
- package/src/events.ts +99 -20
- package/src/index.ts +479 -166
- package/src/live-output.ts +339 -0
- package/src/managed-runtime.ts +5 -1
- package/src/pi-message.ts +14 -4
- package/src/profile.ts +14 -22
- package/src/reporter.ts +62 -32
- package/src/runtime-binding.ts +22 -46
- package/src/session.ts +0 -1
- package/src/values.ts +9 -0
- package/src/workspace.ts +8 -13
- package/src/internal-api.ts +0 -29
package/src/index.ts
CHANGED
|
@@ -1,39 +1,88 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { connectPi, RpcError, type PiConnection, type ControlInput } from "./control-socket.js";
|
|
4
|
+
import { defaultHookLogFile, type EnvLike, type TurnContext } from "./context.js";
|
|
3
5
|
import { appendDiagnostic, type DiagnosticEntry } from "./diagnostics.js";
|
|
4
|
-
import { pontiaHomeFromEnv
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { pontiaHomeFromEnv } from "./discovery.js";
|
|
7
|
+
import {
|
|
8
|
+
buildSessionContextUsageUpdatedEvent,
|
|
9
|
+
buildSessionExitedEvent,
|
|
10
|
+
buildSessionMessageUpdatedEvent,
|
|
11
|
+
buildSessionReadyEvent,
|
|
12
|
+
buildTurnCompletedEvent,
|
|
13
|
+
buildTurnFailedEvent,
|
|
14
|
+
buildTurnInterruptedEvent,
|
|
15
|
+
buildTurnOutputEvent,
|
|
16
|
+
buildTurnStartedEvent,
|
|
17
|
+
contextUsageFromPiHook,
|
|
18
|
+
type InternalEvent,
|
|
19
|
+
type PiTopologyContext,
|
|
20
|
+
type PiTopologyEntryKind,
|
|
21
|
+
type SessionMessageUpdatedReason,
|
|
22
|
+
} from "./events.js";
|
|
23
|
+
import { asRecord, optionalString } from "./values.js";
|
|
24
|
+
import {
|
|
25
|
+
completeToolCallFromMessageUpdate,
|
|
26
|
+
LiveOutputPublisher,
|
|
27
|
+
type LiveOutputPublisherLike,
|
|
28
|
+
} from "./live-output.js";
|
|
29
|
+
import {
|
|
30
|
+
hasTmuxPaneEnvironment,
|
|
31
|
+
isPontiaManagedTmuxPane,
|
|
32
|
+
loadPontiaManagedRuntimeIdentity,
|
|
33
|
+
type ManagedRuntimeIdentity,
|
|
34
|
+
} from "./managed-runtime.js";
|
|
35
|
+
import {
|
|
36
|
+
agentEndWasInterrupted,
|
|
37
|
+
assistantDeltaFromEvent,
|
|
38
|
+
assistantTextFromMessage,
|
|
39
|
+
errorMessageFromAgentEnd,
|
|
40
|
+
isTranscriptBoundaryMessageUpdate,
|
|
41
|
+
lastAssistantTextFromMessages,
|
|
42
|
+
} from "./pi-message.js";
|
|
9
43
|
import { loadProfileSystemPrompt } from "./profile.js";
|
|
10
44
|
import { EventReporter, type EventReportResult } from "./reporter.js";
|
|
11
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
bindSession,
|
|
47
|
+
loadExistingSessionContext,
|
|
48
|
+
piSessionDetailsFromHookContext,
|
|
49
|
+
type PiSessionDetails,
|
|
50
|
+
} from "./runtime-binding.js";
|
|
12
51
|
import type { SessionContext } from "./session.js";
|
|
13
52
|
import { isActiveRegisteredWorkspace } from "./workspace.js";
|
|
14
53
|
|
|
15
54
|
interface ReporterLike {
|
|
16
|
-
report(
|
|
55
|
+
report(
|
|
56
|
+
context: { runtimeInstanceId: string },
|
|
57
|
+
event: InternalEvent,
|
|
58
|
+
): Promise<EventReportResult | boolean>;
|
|
17
59
|
}
|
|
18
60
|
|
|
19
61
|
function reportAccepted(result: EventReportResult | boolean): boolean {
|
|
20
62
|
return typeof result === "boolean" ? result : result.accepted;
|
|
21
63
|
}
|
|
22
64
|
|
|
65
|
+
function isPersistentTuiContext(ctx: unknown): boolean {
|
|
66
|
+
if (!ctx || typeof ctx !== "object") return false;
|
|
67
|
+
if ((ctx as Record<string, unknown>).mode !== "tui") return false;
|
|
68
|
+
return piSessionDetailsFromHookContext(ctx).clientSessionFile !== undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
23
71
|
export interface PontiaPiExtensionDependencies {
|
|
24
72
|
env?: EnvLike;
|
|
25
|
-
loadContext?: (env: EnvLike, sessionContext?: SessionContext) => Promise<LoadTurnContextResult>;
|
|
26
73
|
makeReporter?: (logFile: string) => ReporterLike;
|
|
27
74
|
logDiagnostic?: (logFile: string, entry: DiagnosticEntry) => Promise<void>;
|
|
28
|
-
fetch?: typeof fetch;
|
|
29
75
|
loadManagedRuntime?: (env: EnvLike) => Promise<ManagedRuntimeIdentity | undefined>;
|
|
30
76
|
isManagedPane?: (env: EnvLike) => Promise<boolean>;
|
|
77
|
+
makeLiveOutputPublisher?: (context: TurnContext & { turnId: string }) => LiveOutputPublisherLike;
|
|
78
|
+
connectPi?: typeof connectPi;
|
|
31
79
|
}
|
|
32
80
|
|
|
33
81
|
interface ActiveTurnState {
|
|
34
82
|
context: TurnContext & { turnId: string };
|
|
35
83
|
logFile: string;
|
|
36
84
|
reporter: ReporterLike;
|
|
85
|
+
liveOutput: LiveOutputPublisherLike;
|
|
37
86
|
output: string;
|
|
38
87
|
ended: boolean;
|
|
39
88
|
}
|
|
@@ -62,13 +111,12 @@ const NON_MESSAGE_ENTRY_KINDS = new Set([
|
|
|
62
111
|
function topologyEntryKind(entry: Record<string, unknown>): PiTopologyEntryKind {
|
|
63
112
|
if (entry.type !== "message") {
|
|
64
113
|
return typeof entry.type === "string" && NON_MESSAGE_ENTRY_KINDS.has(entry.type)
|
|
65
|
-
? entry.type as PiTopologyEntryKind
|
|
114
|
+
? (entry.type as PiTopologyEntryKind)
|
|
66
115
|
: "other";
|
|
67
116
|
}
|
|
68
117
|
const message = entry.message;
|
|
69
|
-
const role =
|
|
70
|
-
? (message as Record<string, unknown>).role
|
|
71
|
-
: undefined;
|
|
118
|
+
const role =
|
|
119
|
+
message && typeof message === "object" ? (message as Record<string, unknown>).role : undefined;
|
|
72
120
|
if (role === "user") return "user_message";
|
|
73
121
|
if (role === "assistant") return "assistant_message";
|
|
74
122
|
if (role === "toolResult") return "tool_result_message";
|
|
@@ -97,7 +145,10 @@ function topologyContextFromHookContext(ctx: unknown): PiTopologyContext | undef
|
|
|
97
145
|
}
|
|
98
146
|
}
|
|
99
147
|
|
|
100
|
-
export function createPontiaPiExtension(
|
|
148
|
+
export function createPontiaPiExtension(
|
|
149
|
+
pi: ExtensionAPI,
|
|
150
|
+
dependencies: PontiaPiExtensionDependencies = {},
|
|
151
|
+
): void {
|
|
101
152
|
const sourceEnv = dependencies.env ?? process.env;
|
|
102
153
|
if (!pontiaHomeFromEnv(sourceEnv) || !hasTmuxPaneEnvironment(sourceEnv)) return;
|
|
103
154
|
// Environment extensions may load PONTIA_HOME after extension registration.
|
|
@@ -105,12 +156,26 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
105
156
|
const currentEnv = () => ({ ...sourceEnv, PONTIA_HOME: currentPontiaHome() });
|
|
106
157
|
const currentHookLogFile = () => defaultHookLogFile(currentPontiaHome());
|
|
107
158
|
|
|
108
|
-
const
|
|
109
|
-
|
|
159
|
+
const rpcConnection: Pick<PiConnection, "request"> = {
|
|
160
|
+
request(method, params) {
|
|
161
|
+
if (!controlSocket) return Promise.reject(new Error("Pi connection is unavailable"));
|
|
162
|
+
return controlSocket.request(method, params);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
const makeReporter =
|
|
166
|
+
dependencies.makeReporter ??
|
|
167
|
+
((logFile: string) =>
|
|
168
|
+
new EventReporter({
|
|
169
|
+
logFile,
|
|
170
|
+
connection: rpcConnection,
|
|
171
|
+
}));
|
|
110
172
|
const logDiagnostic = dependencies.logDiagnostic ?? appendDiagnostic;
|
|
111
|
-
const fetchImpl = dependencies.fetch ?? fetch;
|
|
112
173
|
const loadManagedRuntime = dependencies.loadManagedRuntime ?? loadPontiaManagedRuntimeIdentity;
|
|
113
174
|
const isManagedPane = dependencies.isManagedPane ?? isPontiaManagedTmuxPane;
|
|
175
|
+
const makeLiveOutputPublisher =
|
|
176
|
+
dependencies.makeLiveOutputPublisher ??
|
|
177
|
+
((context: TurnContext & { turnId: string }) =>
|
|
178
|
+
new LiveOutputPublisher(context, { connection: rpcConnection }));
|
|
114
179
|
|
|
115
180
|
let activeTurn: ActiveTurnState | undefined;
|
|
116
181
|
let readyReported = false;
|
|
@@ -120,6 +185,169 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
120
185
|
let managedPaneConfirmed = false;
|
|
121
186
|
let lastContextUsageJson: string | undefined;
|
|
122
187
|
let pendingPrompt: string | undefined;
|
|
188
|
+
let controlSocket: PiConnection | undefined;
|
|
189
|
+
let controlGeneration = 0;
|
|
190
|
+
let piContext: ExtensionContext | undefined;
|
|
191
|
+
let changingModel = false;
|
|
192
|
+
const directInput = new AsyncLocalStorage<{
|
|
193
|
+
submission: ControlInput;
|
|
194
|
+
sessionContext: SessionContext;
|
|
195
|
+
consumed: boolean;
|
|
196
|
+
}>();
|
|
197
|
+
let queuedControlInput: ControlInput | undefined;
|
|
198
|
+
|
|
199
|
+
const modelId = (model: { provider: string; id: string }) => `${model.provider}/${model.id}`;
|
|
200
|
+
|
|
201
|
+
async function reportModel(): Promise<void> {
|
|
202
|
+
const context = boundSessionContext;
|
|
203
|
+
const model = piContext?.model;
|
|
204
|
+
if (!context || !model || !readyReported || reportingDisabled) return;
|
|
205
|
+
const result = await makeReporter(currentHookLogFile()).report(context, {
|
|
206
|
+
session_id: context.sessionId,
|
|
207
|
+
type: "session.model_updated",
|
|
208
|
+
data: { model: modelId(model), runtime_instance_id: context.runtimeInstanceId },
|
|
209
|
+
});
|
|
210
|
+
if (!reportAccepted(result)) throw new Error("Pi model observation was not accepted");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sendControlInput(submission: ControlInput): void {
|
|
214
|
+
if (!boundSessionContext) throw new Error("Pi session is not bound");
|
|
215
|
+
try {
|
|
216
|
+
directInput.run({ submission, sessionContext: boundSessionContext, consumed: false }, () =>
|
|
217
|
+
pi.sendUserMessage(submission.input),
|
|
218
|
+
);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new RpcError(
|
|
221
|
+
-32007,
|
|
222
|
+
`Pi input delivery uncertain: ${error instanceof Error ? error.message : String(error)}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function controlError(error: unknown): Promise<void> {
|
|
228
|
+
await logDiagnostic(currentHookLogFile(), {
|
|
229
|
+
level: "warn",
|
|
230
|
+
code: "pi_control_unavailable",
|
|
231
|
+
message: "Pi control channel failed",
|
|
232
|
+
details: error instanceof Error ? error.message : String(error),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function closeControlSocket(): Promise<void> {
|
|
237
|
+
controlGeneration += 1;
|
|
238
|
+
queuedControlInput = undefined;
|
|
239
|
+
const socket = controlSocket;
|
|
240
|
+
controlSocket = undefined;
|
|
241
|
+
if (socket) await socket.close().catch(controlError);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function registrationConnection(): Promise<PiConnection> {
|
|
245
|
+
if (controlSocket) return controlSocket;
|
|
246
|
+
const generation = controlGeneration;
|
|
247
|
+
const lifecycleContext = () => {
|
|
248
|
+
if (
|
|
249
|
+
generation !== controlGeneration ||
|
|
250
|
+
reportingDisabled ||
|
|
251
|
+
!boundSessionContext ||
|
|
252
|
+
!piContext ||
|
|
253
|
+
!readyReported
|
|
254
|
+
) {
|
|
255
|
+
throw new Error("Pi lifecycle control session is no longer current");
|
|
256
|
+
}
|
|
257
|
+
return piContext;
|
|
258
|
+
};
|
|
259
|
+
const socket = await (dependencies.connectPi ?? connectPi)(
|
|
260
|
+
currentPontiaHome(),
|
|
261
|
+
(error) => {
|
|
262
|
+
void controlError(error).catch(() => {});
|
|
263
|
+
},
|
|
264
|
+
(submission) => {
|
|
265
|
+
if (generation !== controlGeneration || reportingDisabled || !boundSessionContext) {
|
|
266
|
+
throw new Error("Pi control session is no longer current");
|
|
267
|
+
}
|
|
268
|
+
if (queuedControlInput)
|
|
269
|
+
throw new RpcError(-32010, "Pi already has pending input; input was not submitted");
|
|
270
|
+
if (!piContext?.isIdle()) {
|
|
271
|
+
if (!activeTurn?.ended) throw new RpcError(-32010, "Pi is busy; input was not submitted");
|
|
272
|
+
queuedControlInput = submission;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
sendControlInput(submission);
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
listModels() {
|
|
279
|
+
if (
|
|
280
|
+
generation !== controlGeneration ||
|
|
281
|
+
reportingDisabled ||
|
|
282
|
+
!boundSessionContext ||
|
|
283
|
+
!piContext ||
|
|
284
|
+
!readyReported
|
|
285
|
+
) {
|
|
286
|
+
throw new Error("Pi model control session is no longer current");
|
|
287
|
+
}
|
|
288
|
+
return piContext.modelRegistry.getAvailable().map((model) => ({
|
|
289
|
+
id: modelId(model),
|
|
290
|
+
name: model.name,
|
|
291
|
+
description: model.provider,
|
|
292
|
+
}));
|
|
293
|
+
},
|
|
294
|
+
async setModel(id) {
|
|
295
|
+
this.listModels();
|
|
296
|
+
if (changingModel) throw new Error("Pi already has a model change in progress");
|
|
297
|
+
const model = piContext!.modelRegistry
|
|
298
|
+
.getAvailable()
|
|
299
|
+
.find((model) => modelId(model) === id);
|
|
300
|
+
if (!model) throw new Error("The selected model is not available");
|
|
301
|
+
changingModel = true;
|
|
302
|
+
try {
|
|
303
|
+
if (!(await pi.setModel(model)))
|
|
304
|
+
throw new RpcError(-32006, "Pi could not authenticate the selected model");
|
|
305
|
+
this.listModels();
|
|
306
|
+
// Selecting the current model does not emit model_select; observe it again.
|
|
307
|
+
await reportModel();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof RpcError) throw error;
|
|
310
|
+
throw new RpcError(
|
|
311
|
+
-32007,
|
|
312
|
+
`Pi model change outcome is unknown: ${error instanceof Error ? error.message : String(error)}`,
|
|
313
|
+
);
|
|
314
|
+
} finally {
|
|
315
|
+
changingModel = false;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
async onReconnect() {
|
|
319
|
+
if (generation !== controlGeneration)
|
|
320
|
+
throw new Error("Pi session changed during reconnect");
|
|
321
|
+
await reportModel();
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
(inboxMessageId) => {
|
|
325
|
+
if (generation !== controlGeneration || reportingDisabled || !boundSessionContext) {
|
|
326
|
+
throw new Error("Pi control session is no longer current");
|
|
327
|
+
}
|
|
328
|
+
// Command dispatch supplies the native context required by navigateTree.
|
|
329
|
+
pi.sendUserMessage(`/pontia-edit ${inboxMessageId}`, { expandPromptTemplates: true });
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
interrupt() {
|
|
333
|
+
lifecycleContext().abort();
|
|
334
|
+
},
|
|
335
|
+
shutdown() {
|
|
336
|
+
const ctx = lifecycleContext();
|
|
337
|
+
// Ordinary extension context: abort first, then exit after Pi settles.
|
|
338
|
+
// Unlike double Ctrl+C, this allows turn cleanup before session_shutdown.
|
|
339
|
+
ctx.abort();
|
|
340
|
+
ctx.shutdown();
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
if (generation !== controlGeneration) {
|
|
345
|
+
await socket.close();
|
|
346
|
+
throw new Error("Pi session changed during connection initialization");
|
|
347
|
+
}
|
|
348
|
+
controlSocket = socket;
|
|
349
|
+
return socket;
|
|
350
|
+
}
|
|
123
351
|
|
|
124
352
|
async function confirmManagedPane(refresh = false): Promise<boolean> {
|
|
125
353
|
if (managedPaneConfirmed && !refresh) return true;
|
|
@@ -131,19 +359,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
131
359
|
if (boundSessionContext) return boundSessionContext;
|
|
132
360
|
const runtimeIdentity = await loadManagedRuntime(currentEnv());
|
|
133
361
|
if (!runtimeIdentity) return undefined;
|
|
134
|
-
const connection = await resolvePontiaConnection({ pontiaHome: currentPontiaHome(), fetch: fetchImpl });
|
|
135
|
-
if (!connection?.internalEventUrl) return undefined;
|
|
136
362
|
return {
|
|
137
363
|
sessionId: runtimeIdentity.sessionId,
|
|
138
364
|
runtimeInstanceId: runtimeIdentity.runtimeInstanceId,
|
|
139
365
|
clientType: "pi",
|
|
140
|
-
internalEventUrl: connection.internalEventUrl,
|
|
141
366
|
};
|
|
142
367
|
}
|
|
143
368
|
|
|
144
369
|
pi.registerCommand("pontia-edit", {
|
|
145
370
|
description: "Replay a Pontia Inbox message from a historical Pi entry",
|
|
146
371
|
handler: async (args, ctx) => {
|
|
372
|
+
if (!isPersistentTuiContext(ctx)) return;
|
|
147
373
|
const inboxMessageId = args.trim();
|
|
148
374
|
const logFile = currentHookLogFile();
|
|
149
375
|
if (!/^msg_[^\s]+$/.test(inboxMessageId)) {
|
|
@@ -165,41 +391,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
165
391
|
return;
|
|
166
392
|
}
|
|
167
393
|
const loaded = { logFile };
|
|
168
|
-
const connection = await resolvePontiaConnection({ pontiaHome: currentPontiaHome() });
|
|
169
|
-
if (!connection?.externalApiToken) {
|
|
170
|
-
await logDiagnostic(loaded.logFile, {
|
|
171
|
-
level: "error",
|
|
172
|
-
code: "branch_replay_stale_context",
|
|
173
|
-
message: "pontia-edit requires an authenticated Pontia connection",
|
|
174
|
-
});
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const resolveUrl = commandContext.internalEventUrl.replace(
|
|
178
|
-
/\/events\/?$/,
|
|
179
|
-
"/inbox/branch-replay/resolve",
|
|
180
|
-
);
|
|
181
394
|
|
|
182
395
|
let replacementInput: string;
|
|
183
396
|
let targetEntryId: string;
|
|
184
397
|
try {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
body: JSON.stringify({
|
|
192
|
-
inbox_message_id: inboxMessageId,
|
|
193
|
-
session_id: commandContext.sessionId,
|
|
194
|
-
runtime_instance_id: commandContext.runtimeInstanceId,
|
|
195
|
-
client_type: "pi",
|
|
196
|
-
}),
|
|
398
|
+
const connection = await registrationConnection();
|
|
399
|
+
const body = await connection.request("branch.resolve", {
|
|
400
|
+
inbox_message_id: inboxMessageId,
|
|
401
|
+
session_id: commandContext.sessionId,
|
|
402
|
+
runtime_instance_id: commandContext.runtimeInstanceId,
|
|
403
|
+
client_type: "pi",
|
|
197
404
|
});
|
|
198
|
-
const
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
201
|
-
}
|
|
202
|
-
const replay = asRecord(asRecord(asRecord(body)?.data)?.branch_replay);
|
|
405
|
+
const replay = asRecord(asRecord(body)?.branch_replay);
|
|
203
406
|
replacementInput = optionalString(replay?.replacement_input) ?? "";
|
|
204
407
|
targetEntryId = optionalString(replay?.target_entry_id) ?? "";
|
|
205
408
|
if (
|
|
@@ -246,7 +449,21 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
246
449
|
|
|
247
450
|
try {
|
|
248
451
|
ctx.ui.setEditorText("");
|
|
249
|
-
await
|
|
452
|
+
const current = await currentManagedSessionContext();
|
|
453
|
+
if (
|
|
454
|
+
current?.sessionId !== commandContext.sessionId ||
|
|
455
|
+
current?.runtimeInstanceId !== commandContext.runtimeInstanceId
|
|
456
|
+
) {
|
|
457
|
+
throw new Error("branch replay Runtime is no longer current");
|
|
458
|
+
}
|
|
459
|
+
await directInput.run(
|
|
460
|
+
{
|
|
461
|
+
submission: { input: replacementInput, inboxMessageId },
|
|
462
|
+
sessionContext: current,
|
|
463
|
+
consumed: false,
|
|
464
|
+
},
|
|
465
|
+
() => pi.sendUserMessage(replacementInput),
|
|
466
|
+
);
|
|
250
467
|
} catch (error) {
|
|
251
468
|
await logDiagnostic(loaded.logFile, {
|
|
252
469
|
level: "error",
|
|
@@ -260,11 +477,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
260
477
|
|
|
261
478
|
async function scheduleMessageRefresh(reason: SessionMessageUpdatedReason): Promise<void> {
|
|
262
479
|
if (!activeTurn || activeTurn.ended) return;
|
|
263
|
-
await activeTurn.reporter.report(
|
|
480
|
+
await activeTurn.reporter.report(
|
|
481
|
+
activeTurn.context,
|
|
482
|
+
buildSessionMessageUpdatedEvent(activeTurn.context, reason),
|
|
483
|
+
);
|
|
264
484
|
}
|
|
265
485
|
|
|
266
486
|
async function reportFinalMessageRefresh(state: ActiveTurnState): Promise<void> {
|
|
267
|
-
await state.reporter.report(
|
|
487
|
+
await state.reporter.report(
|
|
488
|
+
state.context,
|
|
489
|
+
buildSessionMessageUpdatedEvent(state.context, "final"),
|
|
490
|
+
);
|
|
268
491
|
}
|
|
269
492
|
|
|
270
493
|
async function reportContextUsageFromHookEvent(event: unknown, ctx?: unknown): Promise<void> {
|
|
@@ -274,20 +497,25 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
274
497
|
const usageJson = JSON.stringify(observation);
|
|
275
498
|
if (usageJson === lastContextUsageJson) return;
|
|
276
499
|
lastContextUsageJson = usageJson;
|
|
277
|
-
await activeTurn.reporter.report(
|
|
500
|
+
await activeTurn.reporter.report(
|
|
501
|
+
activeTurn.context,
|
|
502
|
+
buildSessionContextUsageUpdatedEvent(activeTurn.context, observation.context_usage),
|
|
503
|
+
);
|
|
278
504
|
}
|
|
279
505
|
|
|
280
506
|
pi.on("before_agent_start", async (event) => {
|
|
281
507
|
const eventRecord = event as unknown as Record<string, unknown>;
|
|
282
508
|
pendingPrompt = optionalString(eventRecord.prompt);
|
|
283
|
-
if (reportingDisabled || !await confirmManagedPane()) {
|
|
284
|
-
return {
|
|
509
|
+
if (reportingDisabled || !(await confirmManagedPane())) {
|
|
510
|
+
return {
|
|
511
|
+
systemPrompt: typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "",
|
|
512
|
+
};
|
|
285
513
|
}
|
|
286
|
-
const currentSystemPrompt =
|
|
514
|
+
const currentSystemPrompt =
|
|
515
|
+
typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "";
|
|
287
516
|
try {
|
|
288
517
|
const profilePrompt = await loadProfileSystemPrompt(
|
|
289
|
-
|
|
290
|
-
fetchImpl,
|
|
518
|
+
rpcConnection,
|
|
291
519
|
boundSessionContext?.sessionId,
|
|
292
520
|
);
|
|
293
521
|
if (!profilePrompt) return { systemPrompt: currentSystemPrompt };
|
|
@@ -303,37 +531,53 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
303
531
|
}
|
|
304
532
|
});
|
|
305
533
|
|
|
534
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
535
|
+
piContext = ctx;
|
|
536
|
+
await reportModel().catch(controlError);
|
|
537
|
+
});
|
|
538
|
+
|
|
306
539
|
pi.on("session_start", async (event, ctx) => {
|
|
307
540
|
if (reportingDisabled) return;
|
|
308
541
|
const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
|
|
309
|
-
if (readyReported && reason !== "fork" && reason !== "resume") return;
|
|
310
|
-
if (reason !== "startup" && reason !== "new" && reason !== "resume" && reason !== "fork")
|
|
542
|
+
if (readyReported && reason !== "fork" && reason !== "resume" && reason !== "new") return;
|
|
543
|
+
if (reason !== "startup" && reason !== "new" && reason !== "resume" && reason !== "fork")
|
|
544
|
+
return;
|
|
545
|
+
await closeControlSocket();
|
|
546
|
+
const parentSessionId = boundSessionContext?.sessionId;
|
|
547
|
+
boundSessionContext = undefined;
|
|
548
|
+
readyReported = false;
|
|
549
|
+
if (!isPersistentTuiContext(ctx)) {
|
|
550
|
+
reportingDisabled = true;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
311
553
|
|
|
312
554
|
try {
|
|
313
555
|
const sessionDetails = piSessionDetailsFromHookContext(ctx);
|
|
314
556
|
deferredManualSessionDetails = sessionDetails;
|
|
315
557
|
const logFile = currentHookLogFile();
|
|
316
558
|
let context: SessionContext | undefined;
|
|
317
|
-
const pontiaHome = currentPontiaHome();
|
|
318
559
|
const env = currentEnv();
|
|
319
560
|
|
|
320
|
-
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
321
|
-
|
|
561
|
+
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
562
|
+
await registrationConnection(),
|
|
563
|
+
sessionDetails.clientCwd,
|
|
564
|
+
);
|
|
565
|
+
if (!workspaceActive) {
|
|
566
|
+
await closeControlSocket();
|
|
322
567
|
reportingDisabled = true;
|
|
323
568
|
await logDiagnostic(logFile, {
|
|
324
569
|
level: "info",
|
|
325
|
-
code:
|
|
326
|
-
message:
|
|
327
|
-
|
|
328
|
-
: "could not verify active registered pontia workspace; pontia reporting disabled",
|
|
570
|
+
code: "workspace_not_active",
|
|
571
|
+
message:
|
|
572
|
+
"current pi workspace is not an active registered pontia workspace; pontia reporting disabled",
|
|
329
573
|
details: { client_cwd: sessionDetails.clientCwd },
|
|
330
574
|
});
|
|
331
575
|
return;
|
|
332
576
|
}
|
|
333
577
|
|
|
334
578
|
if (reason === "fork") {
|
|
335
|
-
const parentSessionId = boundSessionContext?.sessionId;
|
|
336
579
|
if (!parentSessionId) {
|
|
580
|
+
await closeControlSocket();
|
|
337
581
|
await logDiagnostic(logFile, {
|
|
338
582
|
level: "error",
|
|
339
583
|
code: "missing_fork_parent_session",
|
|
@@ -341,21 +585,29 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
341
585
|
});
|
|
342
586
|
return;
|
|
343
587
|
}
|
|
344
|
-
context = await bindSession(
|
|
588
|
+
context = await bindSession(await registrationConnection(), env, sessionDetails, {
|
|
589
|
+
startKind: "fork",
|
|
590
|
+
parentSessionId,
|
|
591
|
+
});
|
|
345
592
|
readyReported = false;
|
|
346
593
|
} else {
|
|
347
|
-
const existingSession = await loadExistingSessionContext(
|
|
594
|
+
const existingSession = await loadExistingSessionContext(
|
|
595
|
+
await registrationConnection(),
|
|
596
|
+
sessionDetails,
|
|
597
|
+
);
|
|
348
598
|
if (
|
|
349
|
-
existingSession
|
|
350
|
-
|
|
599
|
+
existingSession &&
|
|
600
|
+
["idle", "busy", "interrupted"].includes(existingSession.sessionState)
|
|
351
601
|
) {
|
|
602
|
+
await closeControlSocket();
|
|
352
603
|
reportingDisabled = true;
|
|
353
604
|
boundSessionContext = undefined;
|
|
354
605
|
readyReported = false;
|
|
355
606
|
await logDiagnostic(logFile, {
|
|
356
607
|
level: "info",
|
|
357
608
|
code: "duplicate_active_client_session",
|
|
358
|
-
message:
|
|
609
|
+
message:
|
|
610
|
+
"native pi session is already bound to an active pontia session; duplicate TUI reporting disabled",
|
|
359
611
|
details: {
|
|
360
612
|
client_session_key: sessionDetails.clientSessionKey,
|
|
361
613
|
session_id: existingSession.sessionId,
|
|
@@ -364,22 +616,35 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
364
616
|
});
|
|
365
617
|
return;
|
|
366
618
|
}
|
|
367
|
-
if (!existingSession && !await confirmManagedPane()) {
|
|
619
|
+
if (!existingSession && !(await confirmManagedPane())) {
|
|
620
|
+
await closeControlSocket();
|
|
368
621
|
boundSessionContext = undefined;
|
|
369
622
|
readyReported = false;
|
|
370
623
|
return;
|
|
371
624
|
}
|
|
372
625
|
|
|
373
|
-
context = await bindSession(
|
|
374
|
-
runtimeInstanceId:
|
|
626
|
+
context = await bindSession(await registrationConnection(), env, sessionDetails, {
|
|
627
|
+
runtimeInstanceId:
|
|
628
|
+
existingSession?.sessionState === "starting"
|
|
629
|
+
? existingSession.runtimeInstanceId
|
|
630
|
+
: undefined,
|
|
375
631
|
});
|
|
376
632
|
if (reason === "resume" || reason === "new") readyReported = false;
|
|
377
633
|
}
|
|
378
|
-
if (!context || !await confirmManagedPane(true))
|
|
634
|
+
if (!context || !(await confirmManagedPane(true))) {
|
|
635
|
+
await closeControlSocket();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
379
638
|
|
|
380
639
|
boundSessionContext = context;
|
|
381
|
-
|
|
640
|
+
piContext = ctx;
|
|
641
|
+
controlSocket!.registered(context);
|
|
642
|
+
readyReported = reportAccepted(
|
|
643
|
+
await makeReporter(logFile).report(context, buildSessionReadyEvent(context)),
|
|
644
|
+
);
|
|
645
|
+
await reportModel().catch(controlError);
|
|
382
646
|
} catch (error) {
|
|
647
|
+
await closeControlSocket();
|
|
383
648
|
const logFile = currentHookLogFile();
|
|
384
649
|
await logDiagnostic(logFile, {
|
|
385
650
|
level: "error",
|
|
@@ -391,14 +656,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
391
656
|
});
|
|
392
657
|
|
|
393
658
|
pi.on("session_shutdown", async (event) => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
|
|
397
|
-
|
|
659
|
+
piContext = undefined;
|
|
660
|
+
readyReported = false;
|
|
398
661
|
try {
|
|
662
|
+
if (reportingDisabled || !(await confirmManagedPane())) return;
|
|
663
|
+
const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
|
|
664
|
+
if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
|
|
399
665
|
const logFile = currentHookLogFile();
|
|
400
666
|
if (!boundSessionContext) return;
|
|
401
|
-
await makeReporter(logFile).report(
|
|
667
|
+
await makeReporter(logFile).report(
|
|
668
|
+
boundSessionContext,
|
|
669
|
+
buildSessionExitedEvent(boundSessionContext, reason),
|
|
670
|
+
);
|
|
402
671
|
} catch (error) {
|
|
403
672
|
const logFile = currentHookLogFile();
|
|
404
673
|
await logDiagnostic(logFile, {
|
|
@@ -407,6 +676,8 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
407
676
|
message: "failed to report pontia exited signal",
|
|
408
677
|
details: error instanceof Error ? error.message : String(error),
|
|
409
678
|
});
|
|
679
|
+
} finally {
|
|
680
|
+
await closeControlSocket();
|
|
410
681
|
}
|
|
411
682
|
});
|
|
412
683
|
|
|
@@ -415,75 +686,81 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
415
686
|
const previousLeafId = leafIdFromHookContext(ctx);
|
|
416
687
|
if (reportingDisabled) return;
|
|
417
688
|
try {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
689
|
+
const dispatch = directInput.getStore();
|
|
690
|
+
const submission =
|
|
691
|
+
dispatch && !dispatch.consumed && dispatch.sessionContext === boundSessionContext
|
|
692
|
+
? dispatch.submission
|
|
693
|
+
: undefined;
|
|
694
|
+
if (dispatch) dispatch.consumed = true;
|
|
695
|
+
const logFile = currentHookLogFile();
|
|
696
|
+
if (!boundSessionContext) {
|
|
697
|
+
const hookSessionDetails = piSessionDetailsFromHookContext(ctx);
|
|
698
|
+
const sessionDetails = hookSessionDetails.clientSessionKey
|
|
699
|
+
? hookSessionDetails
|
|
700
|
+
: deferredManualSessionDetails;
|
|
701
|
+
if (!sessionDetails) {
|
|
426
702
|
activeTurn = undefined;
|
|
427
703
|
pendingPrompt = undefined;
|
|
428
704
|
return;
|
|
429
705
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
level: "info",
|
|
445
|
-
code: workspaceActive === false ? "workspace_not_active" : "workspace_check_unavailable",
|
|
446
|
-
message: workspaceActive === false
|
|
447
|
-
? "current pi workspace is not an active registered pontia workspace; pontia reporting disabled"
|
|
448
|
-
: "could not verify active registered pontia workspace; pontia reporting disabled",
|
|
449
|
-
details: { client_cwd: sessionDetails.clientCwd },
|
|
450
|
-
});
|
|
451
|
-
activeTurn = undefined;
|
|
452
|
-
pendingPrompt = undefined;
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
boundSessionContext = await bindSession(pontiaHome, currentEnv(), fetchImpl, sessionDetails);
|
|
456
|
-
if (boundSessionContext && !readyReported) {
|
|
457
|
-
readyReported = reportAccepted(await makeReporter(logFile).report(boundSessionContext, buildSessionReadyEvent(boundSessionContext)));
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (boundSessionContext) {
|
|
461
|
-
turnContext = {
|
|
462
|
-
sessionId: boundSessionContext.sessionId,
|
|
463
|
-
runtimeInstanceId: boundSessionContext.runtimeInstanceId,
|
|
464
|
-
clientType: "pi",
|
|
465
|
-
internalEventUrl: boundSessionContext.internalEventUrl,
|
|
466
|
-
input: pendingPrompt,
|
|
467
|
-
};
|
|
468
|
-
} else {
|
|
706
|
+
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
707
|
+
await registrationConnection(),
|
|
708
|
+
sessionDetails.clientCwd,
|
|
709
|
+
);
|
|
710
|
+
if (!workspaceActive) {
|
|
711
|
+
await closeControlSocket();
|
|
712
|
+
reportingDisabled = true;
|
|
713
|
+
await logDiagnostic(logFile, {
|
|
714
|
+
level: "info",
|
|
715
|
+
code: "workspace_not_active",
|
|
716
|
+
message:
|
|
717
|
+
"current pi workspace is not an active registered pontia workspace; pontia reporting disabled",
|
|
718
|
+
details: { client_cwd: sessionDetails.clientCwd },
|
|
719
|
+
});
|
|
469
720
|
activeTurn = undefined;
|
|
470
721
|
pendingPrompt = undefined;
|
|
471
722
|
return;
|
|
472
723
|
}
|
|
473
|
-
|
|
724
|
+
boundSessionContext = await bindSession(
|
|
725
|
+
await registrationConnection(),
|
|
726
|
+
currentEnv(),
|
|
727
|
+
sessionDetails,
|
|
728
|
+
);
|
|
729
|
+
if (boundSessionContext && !readyReported) {
|
|
730
|
+
piContext = ctx;
|
|
731
|
+
controlSocket!.registered(boundSessionContext);
|
|
732
|
+
readyReported = reportAccepted(
|
|
733
|
+
await makeReporter(logFile).report(
|
|
734
|
+
boundSessionContext,
|
|
735
|
+
buildSessionReadyEvent(boundSessionContext),
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
await reportModel().catch(controlError);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (!boundSessionContext) {
|
|
474
742
|
activeTurn = undefined;
|
|
475
|
-
|
|
743
|
+
pendingPrompt = undefined;
|
|
476
744
|
return;
|
|
477
745
|
}
|
|
746
|
+
const turnContext: TurnContext = {
|
|
747
|
+
sessionId: boundSessionContext.sessionId,
|
|
748
|
+
runtimeInstanceId: boundSessionContext.runtimeInstanceId,
|
|
749
|
+
clientType: "pi",
|
|
750
|
+
...(submission ?? { input: pendingPrompt }),
|
|
751
|
+
};
|
|
478
752
|
|
|
479
753
|
pendingPrompt = undefined;
|
|
480
|
-
if (!await confirmManagedPane(true)) {
|
|
754
|
+
if (!(await confirmManagedPane(true))) {
|
|
481
755
|
activeTurn = undefined;
|
|
482
756
|
return;
|
|
483
757
|
}
|
|
484
758
|
const reporter = makeReporter(logFile);
|
|
485
759
|
lastContextUsageJson = undefined;
|
|
486
|
-
const started = await reporter.report(
|
|
760
|
+
const started = await reporter.report(
|
|
761
|
+
turnContext,
|
|
762
|
+
buildTurnStartedEvent(turnContext, previousLeafId, topologyContext),
|
|
763
|
+
);
|
|
487
764
|
const canonicalTurnId = typeof started === "boolean" ? turnContext.turnId : started.turnId;
|
|
488
765
|
if (!reportAccepted(started) || !canonicalTurnId) {
|
|
489
766
|
activeTurn = undefined;
|
|
@@ -494,14 +771,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
494
771
|
});
|
|
495
772
|
return;
|
|
496
773
|
}
|
|
774
|
+
const activeContext = { ...turnContext, turnId: canonicalTurnId };
|
|
497
775
|
activeTurn = {
|
|
498
|
-
context:
|
|
776
|
+
context: activeContext,
|
|
499
777
|
logFile,
|
|
500
778
|
reporter,
|
|
779
|
+
liveOutput: makeLiveOutputPublisher(activeContext),
|
|
501
780
|
output: "",
|
|
502
781
|
ended: false,
|
|
503
782
|
};
|
|
504
783
|
} catch (error) {
|
|
784
|
+
if (!boundSessionContext) await closeControlSocket();
|
|
505
785
|
pendingPrompt = undefined;
|
|
506
786
|
activeTurn = undefined;
|
|
507
787
|
const logFile = currentHookLogFile();
|
|
@@ -518,12 +798,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
518
798
|
if (!activeTurn || activeTurn.ended) return;
|
|
519
799
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
520
800
|
|
|
521
|
-
const
|
|
801
|
+
const delta = assistantDeltaFromEvent(event);
|
|
802
|
+
if (delta) activeTurn.liveOutput.appendText(delta);
|
|
803
|
+
const toolCall = completeToolCallFromMessageUpdate(event);
|
|
804
|
+
if (toolCall) activeTurn.liveOutput.appendToolCall(toolCall);
|
|
805
|
+
|
|
806
|
+
const fullText = assistantTextFromMessage(
|
|
807
|
+
(event as unknown as Record<string, unknown> | undefined)?.message,
|
|
808
|
+
);
|
|
522
809
|
if (fullText) {
|
|
523
810
|
activeTurn.output = fullText;
|
|
524
|
-
} else {
|
|
525
|
-
|
|
526
|
-
if (delta) activeTurn.output += delta;
|
|
811
|
+
} else if (delta) {
|
|
812
|
+
activeTurn.output += delta;
|
|
527
813
|
}
|
|
528
814
|
|
|
529
815
|
if (isTranscriptBoundaryMessageUpdate(event)) await scheduleMessageRefresh("update");
|
|
@@ -540,44 +826,71 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
540
826
|
pi.on("message_end", async (event, ctx) => {
|
|
541
827
|
if (!activeTurn || activeTurn.ended) return;
|
|
542
828
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
543
|
-
const fullText = assistantTextFromMessage(
|
|
829
|
+
const fullText = assistantTextFromMessage(
|
|
830
|
+
(event as unknown as Record<string, unknown> | undefined)?.message,
|
|
831
|
+
);
|
|
544
832
|
if (fullText) activeTurn.output = fullText;
|
|
545
833
|
await scheduleMessageRefresh("append");
|
|
546
834
|
});
|
|
547
835
|
|
|
836
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
837
|
+
if (!queuedControlInput || !ctx.isIdle()) return;
|
|
838
|
+
const submission = queuedControlInput;
|
|
839
|
+
queuedControlInput = undefined;
|
|
840
|
+
sendControlInput(submission);
|
|
841
|
+
});
|
|
842
|
+
|
|
548
843
|
pi.on("agent_end", async (event, ctx) => {
|
|
549
844
|
if (!activeTurn || activeTurn.ended) return;
|
|
550
845
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
551
846
|
activeTurn.ended = true;
|
|
552
847
|
|
|
553
848
|
const state = activeTurn;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
849
|
+
try {
|
|
850
|
+
const terminalLeafId = leafIdFromHookContext(ctx);
|
|
851
|
+
if (agentEndWasInterrupted(event, ctx?.signal)) {
|
|
852
|
+
await state.reporter.report(
|
|
853
|
+
state.context,
|
|
854
|
+
buildTurnInterruptedEvent(state.context, terminalLeafId),
|
|
855
|
+
);
|
|
856
|
+
await reportFinalMessageRefresh(state);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
560
859
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
860
|
+
const failureMessage = errorMessageFromAgentEnd(event);
|
|
861
|
+
if (failureMessage) {
|
|
862
|
+
await state.reporter.report(
|
|
863
|
+
state.context,
|
|
864
|
+
buildTurnFailedEvent(state.context, failureMessage, terminalLeafId),
|
|
865
|
+
);
|
|
866
|
+
await reportFinalMessageRefresh(state);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
567
869
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
870
|
+
if (!state.output) {
|
|
871
|
+
const finalText = lastAssistantTextFromMessages(
|
|
872
|
+
(event as unknown as Record<string, unknown> | undefined)?.messages,
|
|
873
|
+
);
|
|
874
|
+
if (finalText) state.output = finalText;
|
|
875
|
+
}
|
|
572
876
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
877
|
+
const output = state.output.trim();
|
|
878
|
+
if (output.length > 0) {
|
|
879
|
+
const outputResult = await state.reporter.report(
|
|
880
|
+
state.context,
|
|
881
|
+
buildTurnOutputEvent(state.context, output),
|
|
882
|
+
);
|
|
883
|
+
if (!reportAccepted(outputResult)) return;
|
|
884
|
+
}
|
|
578
885
|
|
|
579
|
-
|
|
580
|
-
|
|
886
|
+
await state.reporter.report(
|
|
887
|
+
state.context,
|
|
888
|
+
buildTurnCompletedEvent(state.context, terminalLeafId),
|
|
889
|
+
);
|
|
890
|
+
await reportFinalMessageRefresh(state);
|
|
891
|
+
} finally {
|
|
892
|
+
await state.liveOutput.close();
|
|
893
|
+
}
|
|
581
894
|
});
|
|
582
895
|
}
|
|
583
896
|
|