@pontia/pi-client-plugin 0.2.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 +26 -19
- package/src/context.ts +0 -104
- package/src/control-socket.ts +456 -0
- package/src/discovery.ts +2 -65
- package/src/events.ts +99 -20
- package/src/index.ts +468 -166
- package/src/live-output.ts +339 -0
- package/src/managed-runtime.ts +5 -1
- package/src/pi-message.ts +13 -3
- 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,19 +1,61 @@
|
|
|
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 {
|
|
@@ -28,18 +70,19 @@ function isPersistentTuiContext(ctx: unknown): boolean {
|
|
|
28
70
|
|
|
29
71
|
export interface PontiaPiExtensionDependencies {
|
|
30
72
|
env?: EnvLike;
|
|
31
|
-
loadContext?: (env: EnvLike, sessionContext?: SessionContext) => Promise<LoadTurnContextResult>;
|
|
32
73
|
makeReporter?: (logFile: string) => ReporterLike;
|
|
33
74
|
logDiagnostic?: (logFile: string, entry: DiagnosticEntry) => Promise<void>;
|
|
34
|
-
fetch?: typeof fetch;
|
|
35
75
|
loadManagedRuntime?: (env: EnvLike) => Promise<ManagedRuntimeIdentity | undefined>;
|
|
36
76
|
isManagedPane?: (env: EnvLike) => Promise<boolean>;
|
|
77
|
+
makeLiveOutputPublisher?: (context: TurnContext & { turnId: string }) => LiveOutputPublisherLike;
|
|
78
|
+
connectPi?: typeof connectPi;
|
|
37
79
|
}
|
|
38
80
|
|
|
39
81
|
interface ActiveTurnState {
|
|
40
82
|
context: TurnContext & { turnId: string };
|
|
41
83
|
logFile: string;
|
|
42
84
|
reporter: ReporterLike;
|
|
85
|
+
liveOutput: LiveOutputPublisherLike;
|
|
43
86
|
output: string;
|
|
44
87
|
ended: boolean;
|
|
45
88
|
}
|
|
@@ -68,13 +111,12 @@ const NON_MESSAGE_ENTRY_KINDS = new Set([
|
|
|
68
111
|
function topologyEntryKind(entry: Record<string, unknown>): PiTopologyEntryKind {
|
|
69
112
|
if (entry.type !== "message") {
|
|
70
113
|
return typeof entry.type === "string" && NON_MESSAGE_ENTRY_KINDS.has(entry.type)
|
|
71
|
-
? entry.type as PiTopologyEntryKind
|
|
114
|
+
? (entry.type as PiTopologyEntryKind)
|
|
72
115
|
: "other";
|
|
73
116
|
}
|
|
74
117
|
const message = entry.message;
|
|
75
|
-
const role =
|
|
76
|
-
? (message as Record<string, unknown>).role
|
|
77
|
-
: undefined;
|
|
118
|
+
const role =
|
|
119
|
+
message && typeof message === "object" ? (message as Record<string, unknown>).role : undefined;
|
|
78
120
|
if (role === "user") return "user_message";
|
|
79
121
|
if (role === "assistant") return "assistant_message";
|
|
80
122
|
if (role === "toolResult") return "tool_result_message";
|
|
@@ -103,7 +145,10 @@ function topologyContextFromHookContext(ctx: unknown): PiTopologyContext | undef
|
|
|
103
145
|
}
|
|
104
146
|
}
|
|
105
147
|
|
|
106
|
-
export function createPontiaPiExtension(
|
|
148
|
+
export function createPontiaPiExtension(
|
|
149
|
+
pi: ExtensionAPI,
|
|
150
|
+
dependencies: PontiaPiExtensionDependencies = {},
|
|
151
|
+
): void {
|
|
107
152
|
const sourceEnv = dependencies.env ?? process.env;
|
|
108
153
|
if (!pontiaHomeFromEnv(sourceEnv) || !hasTmuxPaneEnvironment(sourceEnv)) return;
|
|
109
154
|
// Environment extensions may load PONTIA_HOME after extension registration.
|
|
@@ -111,12 +156,26 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
111
156
|
const currentEnv = () => ({ ...sourceEnv, PONTIA_HOME: currentPontiaHome() });
|
|
112
157
|
const currentHookLogFile = () => defaultHookLogFile(currentPontiaHome());
|
|
113
158
|
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
+
}));
|
|
116
172
|
const logDiagnostic = dependencies.logDiagnostic ?? appendDiagnostic;
|
|
117
|
-
const fetchImpl = dependencies.fetch ?? fetch;
|
|
118
173
|
const loadManagedRuntime = dependencies.loadManagedRuntime ?? loadPontiaManagedRuntimeIdentity;
|
|
119
174
|
const isManagedPane = dependencies.isManagedPane ?? isPontiaManagedTmuxPane;
|
|
175
|
+
const makeLiveOutputPublisher =
|
|
176
|
+
dependencies.makeLiveOutputPublisher ??
|
|
177
|
+
((context: TurnContext & { turnId: string }) =>
|
|
178
|
+
new LiveOutputPublisher(context, { connection: rpcConnection }));
|
|
120
179
|
|
|
121
180
|
let activeTurn: ActiveTurnState | undefined;
|
|
122
181
|
let readyReported = false;
|
|
@@ -126,6 +185,169 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
126
185
|
let managedPaneConfirmed = false;
|
|
127
186
|
let lastContextUsageJson: string | undefined;
|
|
128
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
|
+
}
|
|
129
351
|
|
|
130
352
|
async function confirmManagedPane(refresh = false): Promise<boolean> {
|
|
131
353
|
if (managedPaneConfirmed && !refresh) return true;
|
|
@@ -137,13 +359,10 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
137
359
|
if (boundSessionContext) return boundSessionContext;
|
|
138
360
|
const runtimeIdentity = await loadManagedRuntime(currentEnv());
|
|
139
361
|
if (!runtimeIdentity) return undefined;
|
|
140
|
-
const connection = await resolvePontiaConnection({ pontiaHome: currentPontiaHome(), fetch: fetchImpl });
|
|
141
|
-
if (!connection?.internalEventUrl) return undefined;
|
|
142
362
|
return {
|
|
143
363
|
sessionId: runtimeIdentity.sessionId,
|
|
144
364
|
runtimeInstanceId: runtimeIdentity.runtimeInstanceId,
|
|
145
365
|
clientType: "pi",
|
|
146
|
-
internalEventUrl: connection.internalEventUrl,
|
|
147
366
|
};
|
|
148
367
|
}
|
|
149
368
|
|
|
@@ -172,41 +391,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
172
391
|
return;
|
|
173
392
|
}
|
|
174
393
|
const loaded = { logFile };
|
|
175
|
-
const connection = await resolvePontiaConnection({ pontiaHome: currentPontiaHome() });
|
|
176
|
-
if (!connection?.externalApiToken) {
|
|
177
|
-
await logDiagnostic(loaded.logFile, {
|
|
178
|
-
level: "error",
|
|
179
|
-
code: "branch_replay_stale_context",
|
|
180
|
-
message: "pontia-edit requires an authenticated Pontia connection",
|
|
181
|
-
});
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const resolveUrl = commandContext.internalEventUrl.replace(
|
|
185
|
-
/\/events\/?$/,
|
|
186
|
-
"/inbox/branch-replay/resolve",
|
|
187
|
-
);
|
|
188
394
|
|
|
189
395
|
let replacementInput: string;
|
|
190
396
|
let targetEntryId: string;
|
|
191
397
|
try {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
body: JSON.stringify({
|
|
199
|
-
inbox_message_id: inboxMessageId,
|
|
200
|
-
session_id: commandContext.sessionId,
|
|
201
|
-
runtime_instance_id: commandContext.runtimeInstanceId,
|
|
202
|
-
client_type: "pi",
|
|
203
|
-
}),
|
|
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",
|
|
204
404
|
});
|
|
205
|
-
const
|
|
206
|
-
if (!response.ok) {
|
|
207
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
208
|
-
}
|
|
209
|
-
const replay = asRecord(asRecord(asRecord(body)?.data)?.branch_replay);
|
|
405
|
+
const replay = asRecord(asRecord(body)?.branch_replay);
|
|
210
406
|
replacementInput = optionalString(replay?.replacement_input) ?? "";
|
|
211
407
|
targetEntryId = optionalString(replay?.target_entry_id) ?? "";
|
|
212
408
|
if (
|
|
@@ -253,7 +449,21 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
253
449
|
|
|
254
450
|
try {
|
|
255
451
|
ctx.ui.setEditorText("");
|
|
256
|
-
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
|
+
);
|
|
257
467
|
} catch (error) {
|
|
258
468
|
await logDiagnostic(loaded.logFile, {
|
|
259
469
|
level: "error",
|
|
@@ -267,11 +477,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
267
477
|
|
|
268
478
|
async function scheduleMessageRefresh(reason: SessionMessageUpdatedReason): Promise<void> {
|
|
269
479
|
if (!activeTurn || activeTurn.ended) return;
|
|
270
|
-
await activeTurn.reporter.report(
|
|
480
|
+
await activeTurn.reporter.report(
|
|
481
|
+
activeTurn.context,
|
|
482
|
+
buildSessionMessageUpdatedEvent(activeTurn.context, reason),
|
|
483
|
+
);
|
|
271
484
|
}
|
|
272
485
|
|
|
273
486
|
async function reportFinalMessageRefresh(state: ActiveTurnState): Promise<void> {
|
|
274
|
-
await state.reporter.report(
|
|
487
|
+
await state.reporter.report(
|
|
488
|
+
state.context,
|
|
489
|
+
buildSessionMessageUpdatedEvent(state.context, "final"),
|
|
490
|
+
);
|
|
275
491
|
}
|
|
276
492
|
|
|
277
493
|
async function reportContextUsageFromHookEvent(event: unknown, ctx?: unknown): Promise<void> {
|
|
@@ -281,20 +497,25 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
281
497
|
const usageJson = JSON.stringify(observation);
|
|
282
498
|
if (usageJson === lastContextUsageJson) return;
|
|
283
499
|
lastContextUsageJson = usageJson;
|
|
284
|
-
await activeTurn.reporter.report(
|
|
500
|
+
await activeTurn.reporter.report(
|
|
501
|
+
activeTurn.context,
|
|
502
|
+
buildSessionContextUsageUpdatedEvent(activeTurn.context, observation.context_usage),
|
|
503
|
+
);
|
|
285
504
|
}
|
|
286
505
|
|
|
287
506
|
pi.on("before_agent_start", async (event) => {
|
|
288
507
|
const eventRecord = event as unknown as Record<string, unknown>;
|
|
289
508
|
pendingPrompt = optionalString(eventRecord.prompt);
|
|
290
|
-
if (reportingDisabled || !await confirmManagedPane()) {
|
|
291
|
-
return {
|
|
509
|
+
if (reportingDisabled || !(await confirmManagedPane())) {
|
|
510
|
+
return {
|
|
511
|
+
systemPrompt: typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "",
|
|
512
|
+
};
|
|
292
513
|
}
|
|
293
|
-
const currentSystemPrompt =
|
|
514
|
+
const currentSystemPrompt =
|
|
515
|
+
typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "";
|
|
294
516
|
try {
|
|
295
517
|
const profilePrompt = await loadProfileSystemPrompt(
|
|
296
|
-
|
|
297
|
-
fetchImpl,
|
|
518
|
+
rpcConnection,
|
|
298
519
|
boundSessionContext?.sessionId,
|
|
299
520
|
);
|
|
300
521
|
if (!profilePrompt) return { systemPrompt: currentSystemPrompt };
|
|
@@ -310,11 +531,21 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
310
531
|
}
|
|
311
532
|
});
|
|
312
533
|
|
|
534
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
535
|
+
piContext = ctx;
|
|
536
|
+
await reportModel().catch(controlError);
|
|
537
|
+
});
|
|
538
|
+
|
|
313
539
|
pi.on("session_start", async (event, ctx) => {
|
|
314
540
|
if (reportingDisabled) return;
|
|
315
541
|
const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
|
|
316
|
-
if (readyReported && reason !== "fork" && reason !== "resume") return;
|
|
317
|
-
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;
|
|
318
549
|
if (!isPersistentTuiContext(ctx)) {
|
|
319
550
|
reportingDisabled = true;
|
|
320
551
|
return;
|
|
@@ -325,26 +556,28 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
325
556
|
deferredManualSessionDetails = sessionDetails;
|
|
326
557
|
const logFile = currentHookLogFile();
|
|
327
558
|
let context: SessionContext | undefined;
|
|
328
|
-
const pontiaHome = currentPontiaHome();
|
|
329
559
|
const env = currentEnv();
|
|
330
560
|
|
|
331
|
-
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
332
|
-
|
|
561
|
+
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
562
|
+
await registrationConnection(),
|
|
563
|
+
sessionDetails.clientCwd,
|
|
564
|
+
);
|
|
565
|
+
if (!workspaceActive) {
|
|
566
|
+
await closeControlSocket();
|
|
333
567
|
reportingDisabled = true;
|
|
334
568
|
await logDiagnostic(logFile, {
|
|
335
569
|
level: "info",
|
|
336
|
-
code:
|
|
337
|
-
message:
|
|
338
|
-
|
|
339
|
-
: "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",
|
|
340
573
|
details: { client_cwd: sessionDetails.clientCwd },
|
|
341
574
|
});
|
|
342
575
|
return;
|
|
343
576
|
}
|
|
344
577
|
|
|
345
578
|
if (reason === "fork") {
|
|
346
|
-
const parentSessionId = boundSessionContext?.sessionId;
|
|
347
579
|
if (!parentSessionId) {
|
|
580
|
+
await closeControlSocket();
|
|
348
581
|
await logDiagnostic(logFile, {
|
|
349
582
|
level: "error",
|
|
350
583
|
code: "missing_fork_parent_session",
|
|
@@ -352,21 +585,29 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
352
585
|
});
|
|
353
586
|
return;
|
|
354
587
|
}
|
|
355
|
-
context = await bindSession(
|
|
588
|
+
context = await bindSession(await registrationConnection(), env, sessionDetails, {
|
|
589
|
+
startKind: "fork",
|
|
590
|
+
parentSessionId,
|
|
591
|
+
});
|
|
356
592
|
readyReported = false;
|
|
357
593
|
} else {
|
|
358
|
-
const existingSession = await loadExistingSessionContext(
|
|
594
|
+
const existingSession = await loadExistingSessionContext(
|
|
595
|
+
await registrationConnection(),
|
|
596
|
+
sessionDetails,
|
|
597
|
+
);
|
|
359
598
|
if (
|
|
360
|
-
existingSession
|
|
361
|
-
|
|
599
|
+
existingSession &&
|
|
600
|
+
["idle", "busy", "interrupted"].includes(existingSession.sessionState)
|
|
362
601
|
) {
|
|
602
|
+
await closeControlSocket();
|
|
363
603
|
reportingDisabled = true;
|
|
364
604
|
boundSessionContext = undefined;
|
|
365
605
|
readyReported = false;
|
|
366
606
|
await logDiagnostic(logFile, {
|
|
367
607
|
level: "info",
|
|
368
608
|
code: "duplicate_active_client_session",
|
|
369
|
-
message:
|
|
609
|
+
message:
|
|
610
|
+
"native pi session is already bound to an active pontia session; duplicate TUI reporting disabled",
|
|
370
611
|
details: {
|
|
371
612
|
client_session_key: sessionDetails.clientSessionKey,
|
|
372
613
|
session_id: existingSession.sessionId,
|
|
@@ -375,22 +616,35 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
375
616
|
});
|
|
376
617
|
return;
|
|
377
618
|
}
|
|
378
|
-
if (!existingSession && !await confirmManagedPane()) {
|
|
619
|
+
if (!existingSession && !(await confirmManagedPane())) {
|
|
620
|
+
await closeControlSocket();
|
|
379
621
|
boundSessionContext = undefined;
|
|
380
622
|
readyReported = false;
|
|
381
623
|
return;
|
|
382
624
|
}
|
|
383
625
|
|
|
384
|
-
context = await bindSession(
|
|
385
|
-
runtimeInstanceId:
|
|
626
|
+
context = await bindSession(await registrationConnection(), env, sessionDetails, {
|
|
627
|
+
runtimeInstanceId:
|
|
628
|
+
existingSession?.sessionState === "starting"
|
|
629
|
+
? existingSession.runtimeInstanceId
|
|
630
|
+
: undefined,
|
|
386
631
|
});
|
|
387
632
|
if (reason === "resume" || reason === "new") readyReported = false;
|
|
388
633
|
}
|
|
389
|
-
if (!context || !await confirmManagedPane(true))
|
|
634
|
+
if (!context || !(await confirmManagedPane(true))) {
|
|
635
|
+
await closeControlSocket();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
390
638
|
|
|
391
639
|
boundSessionContext = context;
|
|
392
|
-
|
|
640
|
+
piContext = ctx;
|
|
641
|
+
controlSocket!.registered(context);
|
|
642
|
+
readyReported = reportAccepted(
|
|
643
|
+
await makeReporter(logFile).report(context, buildSessionReadyEvent(context)),
|
|
644
|
+
);
|
|
645
|
+
await reportModel().catch(controlError);
|
|
393
646
|
} catch (error) {
|
|
647
|
+
await closeControlSocket();
|
|
394
648
|
const logFile = currentHookLogFile();
|
|
395
649
|
await logDiagnostic(logFile, {
|
|
396
650
|
level: "error",
|
|
@@ -402,14 +656,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
402
656
|
});
|
|
403
657
|
|
|
404
658
|
pi.on("session_shutdown", async (event) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
|
|
408
|
-
|
|
659
|
+
piContext = undefined;
|
|
660
|
+
readyReported = false;
|
|
409
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;
|
|
410
665
|
const logFile = currentHookLogFile();
|
|
411
666
|
if (!boundSessionContext) return;
|
|
412
|
-
await makeReporter(logFile).report(
|
|
667
|
+
await makeReporter(logFile).report(
|
|
668
|
+
boundSessionContext,
|
|
669
|
+
buildSessionExitedEvent(boundSessionContext, reason),
|
|
670
|
+
);
|
|
413
671
|
} catch (error) {
|
|
414
672
|
const logFile = currentHookLogFile();
|
|
415
673
|
await logDiagnostic(logFile, {
|
|
@@ -418,6 +676,8 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
418
676
|
message: "failed to report pontia exited signal",
|
|
419
677
|
details: error instanceof Error ? error.message : String(error),
|
|
420
678
|
});
|
|
679
|
+
} finally {
|
|
680
|
+
await closeControlSocket();
|
|
421
681
|
}
|
|
422
682
|
});
|
|
423
683
|
|
|
@@ -426,75 +686,81 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
426
686
|
const previousLeafId = leafIdFromHookContext(ctx);
|
|
427
687
|
if (reportingDisabled) return;
|
|
428
688
|
try {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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) {
|
|
437
702
|
activeTurn = undefined;
|
|
438
703
|
pendingPrompt = undefined;
|
|
439
704
|
return;
|
|
440
705
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
level: "info",
|
|
456
|
-
code: workspaceActive === false ? "workspace_not_active" : "workspace_check_unavailable",
|
|
457
|
-
message: workspaceActive === false
|
|
458
|
-
? "current pi workspace is not an active registered pontia workspace; pontia reporting disabled"
|
|
459
|
-
: "could not verify active registered pontia workspace; pontia reporting disabled",
|
|
460
|
-
details: { client_cwd: sessionDetails.clientCwd },
|
|
461
|
-
});
|
|
462
|
-
activeTurn = undefined;
|
|
463
|
-
pendingPrompt = undefined;
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
boundSessionContext = await bindSession(pontiaHome, currentEnv(), fetchImpl, sessionDetails);
|
|
467
|
-
if (boundSessionContext && !readyReported) {
|
|
468
|
-
readyReported = reportAccepted(await makeReporter(logFile).report(boundSessionContext, buildSessionReadyEvent(boundSessionContext)));
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (boundSessionContext) {
|
|
472
|
-
turnContext = {
|
|
473
|
-
sessionId: boundSessionContext.sessionId,
|
|
474
|
-
runtimeInstanceId: boundSessionContext.runtimeInstanceId,
|
|
475
|
-
clientType: "pi",
|
|
476
|
-
internalEventUrl: boundSessionContext.internalEventUrl,
|
|
477
|
-
input: pendingPrompt,
|
|
478
|
-
};
|
|
479
|
-
} 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
|
+
});
|
|
480
720
|
activeTurn = undefined;
|
|
481
721
|
pendingPrompt = undefined;
|
|
482
722
|
return;
|
|
483
723
|
}
|
|
484
|
-
|
|
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) {
|
|
485
742
|
activeTurn = undefined;
|
|
486
|
-
|
|
743
|
+
pendingPrompt = undefined;
|
|
487
744
|
return;
|
|
488
745
|
}
|
|
746
|
+
const turnContext: TurnContext = {
|
|
747
|
+
sessionId: boundSessionContext.sessionId,
|
|
748
|
+
runtimeInstanceId: boundSessionContext.runtimeInstanceId,
|
|
749
|
+
clientType: "pi",
|
|
750
|
+
...(submission ?? { input: pendingPrompt }),
|
|
751
|
+
};
|
|
489
752
|
|
|
490
753
|
pendingPrompt = undefined;
|
|
491
|
-
if (!await confirmManagedPane(true)) {
|
|
754
|
+
if (!(await confirmManagedPane(true))) {
|
|
492
755
|
activeTurn = undefined;
|
|
493
756
|
return;
|
|
494
757
|
}
|
|
495
758
|
const reporter = makeReporter(logFile);
|
|
496
759
|
lastContextUsageJson = undefined;
|
|
497
|
-
const started = await reporter.report(
|
|
760
|
+
const started = await reporter.report(
|
|
761
|
+
turnContext,
|
|
762
|
+
buildTurnStartedEvent(turnContext, previousLeafId, topologyContext),
|
|
763
|
+
);
|
|
498
764
|
const canonicalTurnId = typeof started === "boolean" ? turnContext.turnId : started.turnId;
|
|
499
765
|
if (!reportAccepted(started) || !canonicalTurnId) {
|
|
500
766
|
activeTurn = undefined;
|
|
@@ -505,14 +771,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
505
771
|
});
|
|
506
772
|
return;
|
|
507
773
|
}
|
|
774
|
+
const activeContext = { ...turnContext, turnId: canonicalTurnId };
|
|
508
775
|
activeTurn = {
|
|
509
|
-
context:
|
|
776
|
+
context: activeContext,
|
|
510
777
|
logFile,
|
|
511
778
|
reporter,
|
|
779
|
+
liveOutput: makeLiveOutputPublisher(activeContext),
|
|
512
780
|
output: "",
|
|
513
781
|
ended: false,
|
|
514
782
|
};
|
|
515
783
|
} catch (error) {
|
|
784
|
+
if (!boundSessionContext) await closeControlSocket();
|
|
516
785
|
pendingPrompt = undefined;
|
|
517
786
|
activeTurn = undefined;
|
|
518
787
|
const logFile = currentHookLogFile();
|
|
@@ -529,12 +798,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
529
798
|
if (!activeTurn || activeTurn.ended) return;
|
|
530
799
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
531
800
|
|
|
532
|
-
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
|
+
);
|
|
533
809
|
if (fullText) {
|
|
534
810
|
activeTurn.output = fullText;
|
|
535
|
-
} else {
|
|
536
|
-
|
|
537
|
-
if (delta) activeTurn.output += delta;
|
|
811
|
+
} else if (delta) {
|
|
812
|
+
activeTurn.output += delta;
|
|
538
813
|
}
|
|
539
814
|
|
|
540
815
|
if (isTranscriptBoundaryMessageUpdate(event)) await scheduleMessageRefresh("update");
|
|
@@ -551,44 +826,71 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
551
826
|
pi.on("message_end", async (event, ctx) => {
|
|
552
827
|
if (!activeTurn || activeTurn.ended) return;
|
|
553
828
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
554
|
-
const fullText = assistantTextFromMessage(
|
|
829
|
+
const fullText = assistantTextFromMessage(
|
|
830
|
+
(event as unknown as Record<string, unknown> | undefined)?.message,
|
|
831
|
+
);
|
|
555
832
|
if (fullText) activeTurn.output = fullText;
|
|
556
833
|
await scheduleMessageRefresh("append");
|
|
557
834
|
});
|
|
558
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
|
+
|
|
559
843
|
pi.on("agent_end", async (event, ctx) => {
|
|
560
844
|
if (!activeTurn || activeTurn.ended) return;
|
|
561
845
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
562
846
|
activeTurn.ended = true;
|
|
563
847
|
|
|
564
848
|
const state = activeTurn;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
}
|
|
571
859
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
+
}
|
|
578
869
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
}
|
|
583
876
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
}
|
|
589
885
|
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
}
|
|
592
894
|
});
|
|
593
895
|
}
|
|
594
896
|
|