@pontia/pi-client-plugin 0.2.0 → 0.2.2
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 +504 -177
- 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,24 +497,23 @@ 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
|
-
|
|
289
|
-
|
|
290
|
-
if (reportingDisabled || !await confirmManagedPane()) {
|
|
291
|
-
return { systemPrompt: typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "" };
|
|
292
|
-
}
|
|
293
|
-
const currentSystemPrompt = typeof eventRecord.systemPrompt === "string" ? eventRecord.systemPrompt : "";
|
|
507
|
+
pendingPrompt = event.prompt;
|
|
508
|
+
if (reportingDisabled || !(await confirmManagedPane())) return;
|
|
294
509
|
try {
|
|
295
510
|
const profilePrompt = await loadProfileSystemPrompt(
|
|
296
|
-
|
|
297
|
-
fetchImpl,
|
|
511
|
+
rpcConnection,
|
|
298
512
|
boundSessionContext?.sessionId,
|
|
299
513
|
);
|
|
300
|
-
if (
|
|
301
|
-
|
|
514
|
+
if (profilePrompt) {
|
|
515
|
+
event.systemPromptOptions.sections.pontia_execution_profile = profilePrompt;
|
|
516
|
+
}
|
|
302
517
|
} catch (error) {
|
|
303
518
|
await logDiagnostic(currentHookLogFile(), {
|
|
304
519
|
level: "warn",
|
|
@@ -306,15 +521,30 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
306
521
|
message: "failed to append pontia execution profile system prompt",
|
|
307
522
|
details: error instanceof Error ? error.message : String(error),
|
|
308
523
|
});
|
|
309
|
-
return { systemPrompt: currentSystemPrompt };
|
|
310
524
|
}
|
|
311
525
|
});
|
|
312
526
|
|
|
527
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
528
|
+
piContext = ctx;
|
|
529
|
+
await reportModel().catch(controlError);
|
|
530
|
+
});
|
|
531
|
+
|
|
313
532
|
pi.on("session_start", async (event, ctx) => {
|
|
314
533
|
if (reportingDisabled) return;
|
|
315
534
|
const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
|
|
316
|
-
if (readyReported && reason !== "fork" && reason !== "resume") return;
|
|
317
|
-
if (
|
|
535
|
+
if (readyReported && reason !== "fork" && reason !== "resume" && reason !== "new") return;
|
|
536
|
+
if (
|
|
537
|
+
reason !== "startup" &&
|
|
538
|
+
reason !== "reload" &&
|
|
539
|
+
reason !== "new" &&
|
|
540
|
+
reason !== "resume" &&
|
|
541
|
+
reason !== "fork"
|
|
542
|
+
)
|
|
543
|
+
return;
|
|
544
|
+
await closeControlSocket();
|
|
545
|
+
const parentSessionId = boundSessionContext?.sessionId;
|
|
546
|
+
boundSessionContext = undefined;
|
|
547
|
+
readyReported = false;
|
|
318
548
|
if (!isPersistentTuiContext(ctx)) {
|
|
319
549
|
reportingDisabled = true;
|
|
320
550
|
return;
|
|
@@ -325,26 +555,28 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
325
555
|
deferredManualSessionDetails = sessionDetails;
|
|
326
556
|
const logFile = currentHookLogFile();
|
|
327
557
|
let context: SessionContext | undefined;
|
|
328
|
-
const pontiaHome = currentPontiaHome();
|
|
329
558
|
const env = currentEnv();
|
|
330
559
|
|
|
331
|
-
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
332
|
-
|
|
560
|
+
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
561
|
+
await registrationConnection(),
|
|
562
|
+
sessionDetails.clientCwd,
|
|
563
|
+
);
|
|
564
|
+
if (!workspaceActive) {
|
|
565
|
+
await closeControlSocket();
|
|
333
566
|
reportingDisabled = true;
|
|
334
567
|
await logDiagnostic(logFile, {
|
|
335
568
|
level: "info",
|
|
336
|
-
code:
|
|
337
|
-
message:
|
|
338
|
-
|
|
339
|
-
: "could not verify active registered pontia workspace; pontia reporting disabled",
|
|
569
|
+
code: "workspace_not_active",
|
|
570
|
+
message:
|
|
571
|
+
"current pi workspace is not an active registered pontia workspace; pontia reporting disabled",
|
|
340
572
|
details: { client_cwd: sessionDetails.clientCwd },
|
|
341
573
|
});
|
|
342
574
|
return;
|
|
343
575
|
}
|
|
344
576
|
|
|
345
577
|
if (reason === "fork") {
|
|
346
|
-
const parentSessionId = boundSessionContext?.sessionId;
|
|
347
578
|
if (!parentSessionId) {
|
|
579
|
+
await closeControlSocket();
|
|
348
580
|
await logDiagnostic(logFile, {
|
|
349
581
|
level: "error",
|
|
350
582
|
code: "missing_fork_parent_session",
|
|
@@ -352,21 +584,42 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
352
584
|
});
|
|
353
585
|
return;
|
|
354
586
|
}
|
|
355
|
-
context = await bindSession(
|
|
587
|
+
context = await bindSession(await registrationConnection(), env, sessionDetails, {
|
|
588
|
+
startKind: "fork",
|
|
589
|
+
parentSessionId,
|
|
590
|
+
});
|
|
356
591
|
readyReported = false;
|
|
357
592
|
} else {
|
|
358
|
-
const existingSession = await loadExistingSessionContext(
|
|
593
|
+
const existingSession = await loadExistingSessionContext(
|
|
594
|
+
await registrationConnection(),
|
|
595
|
+
sessionDetails,
|
|
596
|
+
);
|
|
597
|
+
if (reason === "reload" && !existingSession) {
|
|
598
|
+
reportingDisabled = true;
|
|
599
|
+
deferredManualSessionDetails = undefined;
|
|
600
|
+
await closeControlSocket();
|
|
601
|
+
await logDiagnostic(logFile, {
|
|
602
|
+
level: "error",
|
|
603
|
+
code: "reload_session_context_missing",
|
|
604
|
+
message: "pi reload could not recover the existing pontia runtime binding",
|
|
605
|
+
details: { client_session_key: sessionDetails.clientSessionKey },
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
359
609
|
if (
|
|
360
|
-
|
|
361
|
-
&&
|
|
610
|
+
reason !== "reload" &&
|
|
611
|
+
existingSession &&
|
|
612
|
+
["idle", "busy", "interrupted"].includes(existingSession.sessionState)
|
|
362
613
|
) {
|
|
614
|
+
await closeControlSocket();
|
|
363
615
|
reportingDisabled = true;
|
|
364
616
|
boundSessionContext = undefined;
|
|
365
617
|
readyReported = false;
|
|
366
618
|
await logDiagnostic(logFile, {
|
|
367
619
|
level: "info",
|
|
368
620
|
code: "duplicate_active_client_session",
|
|
369
|
-
message:
|
|
621
|
+
message:
|
|
622
|
+
"native pi session is already bound to an active pontia session; duplicate TUI reporting disabled",
|
|
370
623
|
details: {
|
|
371
624
|
client_session_key: sessionDetails.clientSessionKey,
|
|
372
625
|
session_id: existingSession.sessionId,
|
|
@@ -375,22 +628,48 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
375
628
|
});
|
|
376
629
|
return;
|
|
377
630
|
}
|
|
378
|
-
if (!existingSession && !await confirmManagedPane()) {
|
|
631
|
+
if (!existingSession && !(await confirmManagedPane())) {
|
|
632
|
+
await closeControlSocket();
|
|
379
633
|
boundSessionContext = undefined;
|
|
380
634
|
readyReported = false;
|
|
381
635
|
return;
|
|
382
636
|
}
|
|
383
637
|
|
|
384
|
-
context = await bindSession(
|
|
385
|
-
runtimeInstanceId:
|
|
638
|
+
context = await bindSession(await registrationConnection(), env, sessionDetails, {
|
|
639
|
+
runtimeInstanceId:
|
|
640
|
+
reason === "reload" || existingSession?.sessionState === "starting"
|
|
641
|
+
? existingSession?.runtimeInstanceId
|
|
642
|
+
: undefined,
|
|
386
643
|
});
|
|
387
|
-
if (
|
|
644
|
+
if (
|
|
645
|
+
reason === "reload" &&
|
|
646
|
+
(context?.sessionId !== existingSession!.sessionId ||
|
|
647
|
+
context.runtimeInstanceId !== existingSession!.runtimeInstanceId)
|
|
648
|
+
) {
|
|
649
|
+
reportingDisabled = true;
|
|
650
|
+
deferredManualSessionDetails = undefined;
|
|
651
|
+
throw new Error("pi reload runtime binding changed identity");
|
|
652
|
+
}
|
|
653
|
+
if (reason === "reload" || reason === "resume" || reason === "new") readyReported = false;
|
|
654
|
+
}
|
|
655
|
+
if (!context || !(await confirmManagedPane(true))) {
|
|
656
|
+
if (reason === "reload") {
|
|
657
|
+
reportingDisabled = true;
|
|
658
|
+
deferredManualSessionDetails = undefined;
|
|
659
|
+
}
|
|
660
|
+
await closeControlSocket();
|
|
661
|
+
return;
|
|
388
662
|
}
|
|
389
|
-
if (!context || !await confirmManagedPane(true)) return;
|
|
390
663
|
|
|
391
664
|
boundSessionContext = context;
|
|
392
|
-
|
|
665
|
+
piContext = ctx;
|
|
666
|
+
controlSocket!.registered(context);
|
|
667
|
+
readyReported = reportAccepted(
|
|
668
|
+
await makeReporter(logFile).report(context, buildSessionReadyEvent(context)),
|
|
669
|
+
);
|
|
670
|
+
await reportModel().catch(controlError);
|
|
393
671
|
} catch (error) {
|
|
672
|
+
await closeControlSocket();
|
|
394
673
|
const logFile = currentHookLogFile();
|
|
395
674
|
await logDiagnostic(logFile, {
|
|
396
675
|
level: "error",
|
|
@@ -402,14 +681,18 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
402
681
|
});
|
|
403
682
|
|
|
404
683
|
pi.on("session_shutdown", async (event) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
|
|
408
|
-
|
|
684
|
+
piContext = undefined;
|
|
685
|
+
readyReported = false;
|
|
409
686
|
try {
|
|
687
|
+
if (reportingDisabled || !(await confirmManagedPane())) return;
|
|
688
|
+
const reason = (event as unknown as Record<string, unknown> | undefined)?.reason;
|
|
689
|
+
if (reason !== "quit" && reason !== "new" && reason !== "resume" && reason !== "fork") return;
|
|
410
690
|
const logFile = currentHookLogFile();
|
|
411
691
|
if (!boundSessionContext) return;
|
|
412
|
-
await makeReporter(logFile).report(
|
|
692
|
+
await makeReporter(logFile).report(
|
|
693
|
+
boundSessionContext,
|
|
694
|
+
buildSessionExitedEvent(boundSessionContext, reason),
|
|
695
|
+
);
|
|
413
696
|
} catch (error) {
|
|
414
697
|
const logFile = currentHookLogFile();
|
|
415
698
|
await logDiagnostic(logFile, {
|
|
@@ -418,6 +701,8 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
418
701
|
message: "failed to report pontia exited signal",
|
|
419
702
|
details: error instanceof Error ? error.message : String(error),
|
|
420
703
|
});
|
|
704
|
+
} finally {
|
|
705
|
+
await closeControlSocket();
|
|
421
706
|
}
|
|
422
707
|
});
|
|
423
708
|
|
|
@@ -426,75 +711,81 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
426
711
|
const previousLeafId = leafIdFromHookContext(ctx);
|
|
427
712
|
if (reportingDisabled) return;
|
|
428
713
|
try {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
714
|
+
const dispatch = directInput.getStore();
|
|
715
|
+
const submission =
|
|
716
|
+
dispatch && !dispatch.consumed && dispatch.sessionContext === boundSessionContext
|
|
717
|
+
? dispatch.submission
|
|
718
|
+
: undefined;
|
|
719
|
+
if (dispatch) dispatch.consumed = true;
|
|
720
|
+
const logFile = currentHookLogFile();
|
|
721
|
+
if (!boundSessionContext) {
|
|
722
|
+
const hookSessionDetails = piSessionDetailsFromHookContext(ctx);
|
|
723
|
+
const sessionDetails = hookSessionDetails.clientSessionKey
|
|
724
|
+
? hookSessionDetails
|
|
725
|
+
: deferredManualSessionDetails;
|
|
726
|
+
if (!sessionDetails) {
|
|
437
727
|
activeTurn = undefined;
|
|
438
728
|
pendingPrompt = undefined;
|
|
439
729
|
return;
|
|
440
730
|
}
|
|
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 {
|
|
731
|
+
const workspaceActive = await isActiveRegisteredWorkspace(
|
|
732
|
+
await registrationConnection(),
|
|
733
|
+
sessionDetails.clientCwd,
|
|
734
|
+
);
|
|
735
|
+
if (!workspaceActive) {
|
|
736
|
+
await closeControlSocket();
|
|
737
|
+
reportingDisabled = true;
|
|
738
|
+
await logDiagnostic(logFile, {
|
|
739
|
+
level: "info",
|
|
740
|
+
code: "workspace_not_active",
|
|
741
|
+
message:
|
|
742
|
+
"current pi workspace is not an active registered pontia workspace; pontia reporting disabled",
|
|
743
|
+
details: { client_cwd: sessionDetails.clientCwd },
|
|
744
|
+
});
|
|
480
745
|
activeTurn = undefined;
|
|
481
746
|
pendingPrompt = undefined;
|
|
482
747
|
return;
|
|
483
748
|
}
|
|
484
|
-
|
|
749
|
+
boundSessionContext = await bindSession(
|
|
750
|
+
await registrationConnection(),
|
|
751
|
+
currentEnv(),
|
|
752
|
+
sessionDetails,
|
|
753
|
+
);
|
|
754
|
+
if (boundSessionContext && !readyReported) {
|
|
755
|
+
piContext = ctx;
|
|
756
|
+
controlSocket!.registered(boundSessionContext);
|
|
757
|
+
readyReported = reportAccepted(
|
|
758
|
+
await makeReporter(logFile).report(
|
|
759
|
+
boundSessionContext,
|
|
760
|
+
buildSessionReadyEvent(boundSessionContext),
|
|
761
|
+
),
|
|
762
|
+
);
|
|
763
|
+
await reportModel().catch(controlError);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!boundSessionContext) {
|
|
485
767
|
activeTurn = undefined;
|
|
486
|
-
|
|
768
|
+
pendingPrompt = undefined;
|
|
487
769
|
return;
|
|
488
770
|
}
|
|
771
|
+
const turnContext: TurnContext = {
|
|
772
|
+
sessionId: boundSessionContext.sessionId,
|
|
773
|
+
runtimeInstanceId: boundSessionContext.runtimeInstanceId,
|
|
774
|
+
clientType: "pi",
|
|
775
|
+
...(submission ?? { input: pendingPrompt }),
|
|
776
|
+
};
|
|
489
777
|
|
|
490
778
|
pendingPrompt = undefined;
|
|
491
|
-
if (!await confirmManagedPane(true)) {
|
|
779
|
+
if (!(await confirmManagedPane(true))) {
|
|
492
780
|
activeTurn = undefined;
|
|
493
781
|
return;
|
|
494
782
|
}
|
|
495
783
|
const reporter = makeReporter(logFile);
|
|
496
784
|
lastContextUsageJson = undefined;
|
|
497
|
-
const started = await reporter.report(
|
|
785
|
+
const started = await reporter.report(
|
|
786
|
+
turnContext,
|
|
787
|
+
buildTurnStartedEvent(turnContext, previousLeafId, topologyContext),
|
|
788
|
+
);
|
|
498
789
|
const canonicalTurnId = typeof started === "boolean" ? turnContext.turnId : started.turnId;
|
|
499
790
|
if (!reportAccepted(started) || !canonicalTurnId) {
|
|
500
791
|
activeTurn = undefined;
|
|
@@ -505,14 +796,17 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
505
796
|
});
|
|
506
797
|
return;
|
|
507
798
|
}
|
|
799
|
+
const activeContext = { ...turnContext, turnId: canonicalTurnId };
|
|
508
800
|
activeTurn = {
|
|
509
|
-
context:
|
|
801
|
+
context: activeContext,
|
|
510
802
|
logFile,
|
|
511
803
|
reporter,
|
|
804
|
+
liveOutput: makeLiveOutputPublisher(activeContext),
|
|
512
805
|
output: "",
|
|
513
806
|
ended: false,
|
|
514
807
|
};
|
|
515
808
|
} catch (error) {
|
|
809
|
+
if (!boundSessionContext) await closeControlSocket();
|
|
516
810
|
pendingPrompt = undefined;
|
|
517
811
|
activeTurn = undefined;
|
|
518
812
|
const logFile = currentHookLogFile();
|
|
@@ -529,66 +823,99 @@ export function createPontiaPiExtension(pi: ExtensionAPI, dependencies: PontiaPi
|
|
|
529
823
|
if (!activeTurn || activeTurn.ended) return;
|
|
530
824
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
531
825
|
|
|
532
|
-
const
|
|
826
|
+
const delta = assistantDeltaFromEvent(event);
|
|
827
|
+
if (delta) activeTurn.liveOutput.appendText(delta);
|
|
828
|
+
const toolCall = completeToolCallFromMessageUpdate(event);
|
|
829
|
+
if (toolCall) activeTurn.liveOutput.appendToolCall(toolCall);
|
|
830
|
+
|
|
831
|
+
const fullText = assistantTextFromMessage(
|
|
832
|
+
(event as unknown as Record<string, unknown> | undefined)?.message,
|
|
833
|
+
);
|
|
533
834
|
if (fullText) {
|
|
534
835
|
activeTurn.output = fullText;
|
|
535
|
-
} else {
|
|
536
|
-
|
|
537
|
-
if (delta) activeTurn.output += delta;
|
|
836
|
+
} else if (delta) {
|
|
837
|
+
activeTurn.output += delta;
|
|
538
838
|
}
|
|
539
839
|
|
|
540
840
|
if (isTranscriptBoundaryMessageUpdate(event)) await scheduleMessageRefresh("update");
|
|
541
841
|
});
|
|
542
842
|
|
|
543
|
-
pi.on("tool_execution_start", async () => {
|
|
544
|
-
await scheduleMessageRefresh("update");
|
|
843
|
+
pi.on("tool_execution_start", async (event) => {
|
|
844
|
+
if (!event.parentToolCallId) await scheduleMessageRefresh("update");
|
|
545
845
|
});
|
|
546
846
|
|
|
547
|
-
pi.on("tool_execution_end", async () => {
|
|
548
|
-
await scheduleMessageRefresh("update");
|
|
847
|
+
pi.on("tool_execution_end", async (event) => {
|
|
848
|
+
if (!event.parentToolCallId) await scheduleMessageRefresh("update");
|
|
549
849
|
});
|
|
550
850
|
|
|
551
851
|
pi.on("message_end", async (event, ctx) => {
|
|
552
852
|
if (!activeTurn || activeTurn.ended) return;
|
|
553
853
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
554
|
-
const fullText = assistantTextFromMessage(
|
|
854
|
+
const fullText = assistantTextFromMessage(
|
|
855
|
+
(event as unknown as Record<string, unknown> | undefined)?.message,
|
|
856
|
+
);
|
|
555
857
|
if (fullText) activeTurn.output = fullText;
|
|
556
858
|
await scheduleMessageRefresh("append");
|
|
557
859
|
});
|
|
558
860
|
|
|
861
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
862
|
+
if (!queuedControlInput || !ctx.isIdle()) return;
|
|
863
|
+
const submission = queuedControlInput;
|
|
864
|
+
queuedControlInput = undefined;
|
|
865
|
+
sendControlInput(submission);
|
|
866
|
+
});
|
|
867
|
+
|
|
559
868
|
pi.on("agent_end", async (event, ctx) => {
|
|
560
869
|
if (!activeTurn || activeTurn.ended) return;
|
|
561
870
|
await reportContextUsageFromHookEvent(event, ctx);
|
|
562
871
|
activeTurn.ended = true;
|
|
563
872
|
|
|
564
873
|
const state = activeTurn;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
874
|
+
try {
|
|
875
|
+
const terminalLeafId = leafIdFromHookContext(ctx);
|
|
876
|
+
if (agentEndWasInterrupted(event, ctx?.signal)) {
|
|
877
|
+
await state.reporter.report(
|
|
878
|
+
state.context,
|
|
879
|
+
buildTurnInterruptedEvent(state.context, terminalLeafId),
|
|
880
|
+
);
|
|
881
|
+
await reportFinalMessageRefresh(state);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
571
884
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
885
|
+
const failureMessage = errorMessageFromAgentEnd(event);
|
|
886
|
+
if (failureMessage) {
|
|
887
|
+
await state.reporter.report(
|
|
888
|
+
state.context,
|
|
889
|
+
buildTurnFailedEvent(state.context, failureMessage, terminalLeafId),
|
|
890
|
+
);
|
|
891
|
+
await reportFinalMessageRefresh(state);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
578
894
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
895
|
+
if (!state.output) {
|
|
896
|
+
const finalText = lastAssistantTextFromMessages(
|
|
897
|
+
(event as unknown as Record<string, unknown> | undefined)?.messages,
|
|
898
|
+
);
|
|
899
|
+
if (finalText) state.output = finalText;
|
|
900
|
+
}
|
|
583
901
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
902
|
+
const output = state.output.trim();
|
|
903
|
+
if (output.length > 0) {
|
|
904
|
+
const outputResult = await state.reporter.report(
|
|
905
|
+
state.context,
|
|
906
|
+
buildTurnOutputEvent(state.context, output),
|
|
907
|
+
);
|
|
908
|
+
if (!reportAccepted(outputResult)) return;
|
|
909
|
+
}
|
|
589
910
|
|
|
590
|
-
|
|
591
|
-
|
|
911
|
+
await state.reporter.report(
|
|
912
|
+
state.context,
|
|
913
|
+
buildTurnCompletedEvent(state.context, terminalLeafId),
|
|
914
|
+
);
|
|
915
|
+
await reportFinalMessageRefresh(state);
|
|
916
|
+
} finally {
|
|
917
|
+
await state.liveOutput.close();
|
|
918
|
+
}
|
|
592
919
|
});
|
|
593
920
|
}
|
|
594
921
|
|