antechamber 0.1.1 → 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/CHANGELOG.md +45 -0
- package/README.md +24 -14
- package/bin/opencode-antechamber +14 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +10 -6
- package/dist/pi-event-translation.js +237 -80
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +113 -65
- package/dist/routes-v2.js +407 -95
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +54 -13
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.d.ts +1 -1
- package/dist/session-history.js +110 -44
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +191 -38
- package/dist/session-tree.d.ts +10 -2
- package/dist/session-tree.js +55 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +331 -120
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/vcs.d.ts +5 -0
- package/dist/vcs.js +199 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
|
@@ -21,9 +21,7 @@ export declare function id(prefix: string): string;
|
|
|
21
21
|
*/
|
|
22
22
|
export declare function piModelId(responseModel: unknown, model: unknown): unknown;
|
|
23
23
|
export declare function piStopReason(stopReason: string | undefined, errorMessage?: string): Pick<OCAssistantMessage, "finish" | "error">;
|
|
24
|
-
export declare function textOf(content: Array<Record<string, unknown>> | undefined): string;
|
|
25
|
-
export declare function newPart(kind: "text" | "reasoning", sessionID: string, messageID: string): OCPart;
|
|
26
|
-
export declare function applyUsage(msg: OCAssistantMessage, usage: PiUsage | undefined): void;
|
|
24
|
+
export declare function textOf(content: string | Array<Record<string, unknown>> | undefined): string;
|
|
27
25
|
/**
|
|
28
26
|
* Recompute the session's own spend from its loaded assistant messages. Subagent
|
|
29
27
|
* spend stays on the child sessions: OpenChamber sums the parentID subtree, so
|
|
@@ -33,9 +31,8 @@ export declare function syncSessionCost(s: ManagedSession): boolean;
|
|
|
33
31
|
/**
|
|
34
32
|
* An OmO compaction as OpenCode shows one: a user turn holding a `compaction`
|
|
35
33
|
* part (OpenChamber renders it as `/compact`) answered by a `summary` message.
|
|
36
|
-
* IDs derive from the compaction
|
|
37
|
-
*
|
|
38
|
-
* reconciles with the live messages instead of duplicating them.
|
|
34
|
+
* IDs derive from the native compaction entry, not its retained boundary:
|
|
35
|
+
* multiple compactions can retain the same entry.
|
|
39
36
|
*/
|
|
40
37
|
export declare function compactionMessages(s: ManagedSession, key: string, summary: string, created: number, model: OCModelRef | undefined): [OCMessageWithParts, OCMessageWithParts];
|
|
41
38
|
interface PiEventServices {
|
|
@@ -53,11 +50,18 @@ interface PiEventServices {
|
|
|
53
50
|
export declare class PiEventTranslator {
|
|
54
51
|
private services;
|
|
55
52
|
constructor(services: PiEventServices);
|
|
53
|
+
/** Close dead-runtime UI state before an error or idle event can reach the client. */
|
|
54
|
+
finishInterruptedRun(s: ManagedSession, error: NonNullable<OCAssistantMessage["error"]>): void;
|
|
56
55
|
setStatus(s: ManagedSession, status: OCSessionStatus): void;
|
|
57
56
|
emitPart(s: ManagedSession, part: OCPart, delta?: string): void;
|
|
58
57
|
deliverInput(s: ManagedSession, messageID: string): void;
|
|
59
58
|
private reconcileUser;
|
|
60
59
|
onPiEvent(s: ManagedSession, ev: Record<string, unknown>): void;
|
|
60
|
+
private onTodoEntry;
|
|
61
|
+
private onLifecycleEvent;
|
|
62
|
+
private onToolEvent;
|
|
63
|
+
private onMessageEvent;
|
|
64
|
+
private onCompactionEvent;
|
|
61
65
|
private finishStreaming;
|
|
62
66
|
private onMessageUpdate;
|
|
63
67
|
private onProviderTools;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts, resultMetadata, resultText, } from "./tool-map.js";
|
|
4
4
|
import { TodoBridge } from "./todo-bridge.js";
|
|
5
|
-
import { recordTurn } from "./session-tree.js";
|
|
5
|
+
import { readSessionTree, recordTurn } from "./session-tree.js";
|
|
6
6
|
import { touchedFiles } from "./session-diff.js";
|
|
7
7
|
import { getAgentName } from "./agent-name.js";
|
|
8
8
|
import { debugLog } from "./misc-routes.js";
|
|
@@ -38,11 +38,14 @@ export function piStopReason(stopReason, errorMessage) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
export function textOf(content) {
|
|
41
|
+
// pi allows plain-string message content (extensions, append_user_message).
|
|
42
|
+
if (typeof content === "string")
|
|
43
|
+
return content;
|
|
41
44
|
if (!Array.isArray(content))
|
|
42
45
|
return "";
|
|
43
46
|
return content.map((b) => (b?.type === "text" && typeof b.text === "string" ? b.text : "")).join("");
|
|
44
47
|
}
|
|
45
|
-
|
|
48
|
+
function newPart(kind, sessionID, messageID) {
|
|
46
49
|
const base = {
|
|
47
50
|
id: id("prt"),
|
|
48
51
|
sessionID,
|
|
@@ -54,7 +57,7 @@ export function newPart(kind, sessionID, messageID) {
|
|
|
54
57
|
? { ...base, type: "text" }
|
|
55
58
|
: { ...base, type: "reasoning" };
|
|
56
59
|
}
|
|
57
|
-
|
|
60
|
+
function applyUsage(msg, usage) {
|
|
58
61
|
if (!usage)
|
|
59
62
|
return;
|
|
60
63
|
msg.tokens = {
|
|
@@ -75,9 +78,17 @@ export function syncSessionCost(s) {
|
|
|
75
78
|
if (s.messages === undefined)
|
|
76
79
|
return false;
|
|
77
80
|
let cost = 0;
|
|
81
|
+
const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
|
|
78
82
|
for (const message of s.messages)
|
|
79
|
-
if (message.info.role === "assistant")
|
|
83
|
+
if (message.info.role === "assistant") {
|
|
80
84
|
cost += message.info.cost;
|
|
85
|
+
tokens.input += message.info.tokens.input;
|
|
86
|
+
tokens.output += message.info.tokens.output;
|
|
87
|
+
tokens.reasoning += message.info.tokens.reasoning;
|
|
88
|
+
tokens.cache.read += message.info.tokens.cache.read;
|
|
89
|
+
tokens.cache.write += message.info.tokens.cache.write;
|
|
90
|
+
}
|
|
91
|
+
s.info.tokens = tokens;
|
|
81
92
|
if ((s.info.cost ?? 0) === cost)
|
|
82
93
|
return false; // absent means zero; avoid no-op updates
|
|
83
94
|
s.info.cost = cost;
|
|
@@ -86,9 +97,8 @@ export function syncSessionCost(s) {
|
|
|
86
97
|
/**
|
|
87
98
|
* An OmO compaction as OpenCode shows one: a user turn holding a `compaction`
|
|
88
99
|
* part (OpenChamber renders it as `/compact`) answered by a `summary` message.
|
|
89
|
-
* IDs derive from the compaction
|
|
90
|
-
*
|
|
91
|
-
* reconciles with the live messages instead of duplicating them.
|
|
100
|
+
* IDs derive from the native compaction entry, not its retained boundary:
|
|
101
|
+
* multiple compactions can retain the same entry.
|
|
92
102
|
*/
|
|
93
103
|
export function compactionMessages(s, key, summary, created, model) {
|
|
94
104
|
const sessionID = s.info.id;
|
|
@@ -135,6 +145,33 @@ export class PiEventTranslator {
|
|
|
135
145
|
constructor(services) {
|
|
136
146
|
this.services = services;
|
|
137
147
|
}
|
|
148
|
+
/** Close dead-runtime UI state before an error or idle event can reach the client. */
|
|
149
|
+
finishInterruptedRun(s, error) {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
s.approvals?.finishTurn();
|
|
152
|
+
for (const part of s.streaming?.parts.values() ?? []) {
|
|
153
|
+
if (part.type === "tool")
|
|
154
|
+
s.toolByCallId.set(part.callID, part);
|
|
155
|
+
else if ((part.type === "text" || part.type === "reasoning") &&
|
|
156
|
+
part.time !== undefined &&
|
|
157
|
+
part.time.end === undefined) {
|
|
158
|
+
part.time.end = now;
|
|
159
|
+
this.emitPart(s, part);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.finalizeDanglingTools(s);
|
|
163
|
+
this.services.subagents.finishCalls(s.info.id);
|
|
164
|
+
if (s.streaming !== undefined) {
|
|
165
|
+
const message = s.streaming.message;
|
|
166
|
+
message.finish = "error";
|
|
167
|
+
message.error = error;
|
|
168
|
+
message.time.completed = now;
|
|
169
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message } });
|
|
170
|
+
this.finishStreaming(s);
|
|
171
|
+
}
|
|
172
|
+
delete s.compactingFromIdle;
|
|
173
|
+
delete s.info.time.compacting;
|
|
174
|
+
}
|
|
138
175
|
setStatus(s, status) {
|
|
139
176
|
s.status = status;
|
|
140
177
|
this.services.emit(s.info.directory, {
|
|
@@ -142,6 +179,14 @@ export class PiEventTranslator {
|
|
|
142
179
|
properties: { sessionID: s.info.id, status },
|
|
143
180
|
});
|
|
144
181
|
if (status.type === "idle") {
|
|
182
|
+
s.info.time.idle = Date.now();
|
|
183
|
+
const last = s.messages?.findLast((message) => message.info.role === "assistant");
|
|
184
|
+
s.info.outcome =
|
|
185
|
+
s.interruptRequested || (last?.info.role === "assistant" && last.info.error?.name === "MessageAbortedError")
|
|
186
|
+
? "interrupted"
|
|
187
|
+
: last?.info.role === "assistant" && last.info.finish === "error"
|
|
188
|
+
? "failed"
|
|
189
|
+
: "succeeded";
|
|
145
190
|
this.services.emit(s.info.directory, { type: "session.idle", properties: { sessionID: s.info.id } });
|
|
146
191
|
}
|
|
147
192
|
}
|
|
@@ -206,28 +251,59 @@ export class PiEventTranslator {
|
|
|
206
251
|
}
|
|
207
252
|
onPiEvent(s, ev) {
|
|
208
253
|
this.services.subagents.wake(s, ev);
|
|
209
|
-
const dir = s.info.directory;
|
|
210
254
|
if (envSetting("DEBUG")) {
|
|
211
255
|
const ame = ev.assistantMessageEvent?.type;
|
|
212
256
|
debugLog(`[pi-event] ${String(ev.type)}${ame ? `/${ame}` : ""} ${JSON.stringify(ev).slice(0, 300)}`);
|
|
213
257
|
}
|
|
214
258
|
switch (ev.type) {
|
|
215
|
-
case "entry_appended":
|
|
216
|
-
|
|
217
|
-
const event = s.todoBridge.entry(ev.entry, (part) => this.emitPart(s, part));
|
|
218
|
-
if (event !== undefined)
|
|
219
|
-
this.services.emit(dir, event);
|
|
259
|
+
case "entry_appended":
|
|
260
|
+
this.onTodoEntry(s, ev);
|
|
220
261
|
break;
|
|
221
|
-
}
|
|
222
262
|
case "session_info_changed":
|
|
223
263
|
this.services.applyNativeTitle(s, ev.name);
|
|
224
264
|
break;
|
|
225
265
|
case "agent_start":
|
|
226
|
-
|
|
266
|
+
case "agent_idle":
|
|
267
|
+
case "agent_end":
|
|
268
|
+
case "auto_retry_start":
|
|
269
|
+
case "turn_end":
|
|
270
|
+
case "extension_error":
|
|
271
|
+
this.onLifecycleEvent(s, ev);
|
|
272
|
+
break;
|
|
273
|
+
case "tool_execution_start":
|
|
274
|
+
case "tool_execution_update":
|
|
275
|
+
case "tool_execution_end":
|
|
276
|
+
this.onToolEvent(s, ev);
|
|
227
277
|
break;
|
|
228
|
-
case "
|
|
278
|
+
case "message_start":
|
|
279
|
+
case "message_update":
|
|
280
|
+
case "message_end":
|
|
281
|
+
this.onMessageEvent(s, ev);
|
|
282
|
+
break;
|
|
283
|
+
case "compaction_start":
|
|
284
|
+
case "compaction_end":
|
|
285
|
+
this.onCompactionEvent(s, ev);
|
|
286
|
+
break;
|
|
287
|
+
default:
|
|
288
|
+
break; // turn_start, queue_update — future milestones
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
onTodoEntry(s, ev) {
|
|
292
|
+
s.todoBridge ??= new TodoBridge(s.info.id);
|
|
293
|
+
const event = s.todoBridge.entry(ev.entry, (part) => this.emitPart(s, part));
|
|
294
|
+
if (event !== undefined)
|
|
295
|
+
this.services.emit(s.info.directory, event);
|
|
296
|
+
}
|
|
297
|
+
onLifecycleEvent(s, ev) {
|
|
298
|
+
switch (ev.type) {
|
|
299
|
+
case "agent_start":
|
|
300
|
+
this.setStatus(s, { type: "busy" });
|
|
301
|
+
return;
|
|
302
|
+
case "agent_end":
|
|
229
303
|
if (ev.willRetry === true)
|
|
230
|
-
|
|
304
|
+
return; // auto-retry keeps the session busy
|
|
305
|
+
if (s.interruptRequested)
|
|
306
|
+
this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
|
|
231
307
|
s.approvals?.finishTurn();
|
|
232
308
|
this.finalizeDanglingTools(s);
|
|
233
309
|
this.services.subagents.finishCalls(s.info.id);
|
|
@@ -235,18 +311,28 @@ export class PiEventTranslator {
|
|
|
235
311
|
this.services.emitDiff(s);
|
|
236
312
|
s.info.time.updated = Date.now();
|
|
237
313
|
this.services.persistMeta(s);
|
|
238
|
-
this.services.emit(
|
|
314
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
239
315
|
if (s.piPath !== undefined)
|
|
240
316
|
this.services.store.update(s.info.id, { updated: Date.now() });
|
|
241
317
|
recordTurn(s);
|
|
242
318
|
this.services.persistMeta(s);
|
|
243
|
-
|
|
244
|
-
}
|
|
319
|
+
return;
|
|
245
320
|
case "agent_idle":
|
|
246
321
|
// agent_end closes one run, not the session: OmO may still retry or
|
|
247
322
|
// start a deferred continuation. Only agent_idle settles that work.
|
|
323
|
+
if (s.interruptRequested) {
|
|
324
|
+
this.finishInterruptedRun(s, { name: "MessageAbortedError", data: { message: "Cancelled" } });
|
|
325
|
+
this.services.emit(s.info.directory, {
|
|
326
|
+
type: "session.error",
|
|
327
|
+
properties: {
|
|
328
|
+
sessionID: s.info.id,
|
|
329
|
+
error: { name: "MessageAbortedError", data: { message: "Cancelled" } },
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
delete s.interruptRequested;
|
|
333
|
+
}
|
|
248
334
|
this.setStatus(s, { type: "idle" });
|
|
249
|
-
|
|
335
|
+
return;
|
|
250
336
|
case "turn_end": {
|
|
251
337
|
s.approvals?.finishTurn();
|
|
252
338
|
// pi delivers tool results here, matched back to parts by toolCallId.
|
|
@@ -254,18 +340,41 @@ export class PiEventTranslator {
|
|
|
254
340
|
if (Array.isArray(results))
|
|
255
341
|
this.applyToolResults(s, results);
|
|
256
342
|
this.services.emitDiff(s);
|
|
257
|
-
|
|
343
|
+
return;
|
|
258
344
|
}
|
|
345
|
+
case "auto_retry_start":
|
|
346
|
+
this.setStatus(s, {
|
|
347
|
+
type: "retry",
|
|
348
|
+
attempt: Number(ev.attempt ?? 1),
|
|
349
|
+
message: String(ev.errorMessage ?? "retrying"),
|
|
350
|
+
next: Date.now() + Number(ev.delayMs ?? 0),
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
case "extension_error":
|
|
354
|
+
this.services.emit(s.info.directory, {
|
|
355
|
+
type: "session.error",
|
|
356
|
+
properties: {
|
|
357
|
+
sessionID: s.info.id,
|
|
358
|
+
error: { name: "UnknownError", data: { message: String(ev.error ?? "pi extension error") } },
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
default:
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
onToolEvent(s, ev) {
|
|
367
|
+
switch (ev.type) {
|
|
259
368
|
case "tool_execution_start": {
|
|
260
369
|
if (typeof ev.toolCallId === "string" && typeof ev.toolName === "string") {
|
|
261
370
|
s.approvals?.noteToolStart(ev.toolCallId, ev.toolName, ev.args);
|
|
262
371
|
}
|
|
263
372
|
if (ev.toolName === "task")
|
|
264
|
-
|
|
373
|
+
return; // Expanded when the argument stream closes.
|
|
265
374
|
const callID = typeof ev.toolCallId === "string" ? ev.toolCallId : undefined;
|
|
266
375
|
const part = callID !== undefined ? s.toolByCallId.get(callID) : undefined;
|
|
267
376
|
if (part === undefined)
|
|
268
|
-
|
|
377
|
+
return;
|
|
269
378
|
const name = typeof ev.toolName === "string" ? ev.toolName : part.tool;
|
|
270
379
|
const mapped = mapTool(name, name === "todo" ? part.state.input : ev.args);
|
|
271
380
|
part.tool = mapped.tool;
|
|
@@ -277,36 +386,42 @@ export class PiEventTranslator {
|
|
|
277
386
|
time: { start: part.state.status !== "pending" ? part.state.time.start : Date.now() },
|
|
278
387
|
};
|
|
279
388
|
this.emitPart(s, part);
|
|
280
|
-
|
|
389
|
+
return;
|
|
281
390
|
}
|
|
282
391
|
case "tool_execution_update": {
|
|
283
392
|
if (typeof ev.toolCallId === "string" &&
|
|
284
393
|
this.services.subagents.result(s, ev.toolCallId, ev.partialResult !== null && typeof ev.partialResult === "object" ? ev.partialResult : {}, false))
|
|
285
|
-
|
|
394
|
+
return;
|
|
286
395
|
const part = typeof ev.toolCallId === "string" ? s.toolByCallId.get(ev.toolCallId) : undefined;
|
|
287
396
|
if (part === undefined || part.state.status !== "running")
|
|
288
|
-
|
|
397
|
+
return;
|
|
289
398
|
this.emitPart(s, part);
|
|
290
|
-
|
|
399
|
+
return;
|
|
291
400
|
}
|
|
292
401
|
case "tool_execution_end": {
|
|
293
402
|
if (typeof ev.toolCallId === "string")
|
|
294
403
|
s.approvals?.noteToolEnd(ev.toolCallId);
|
|
295
404
|
const result = ev.result !== null && typeof ev.result === "object" ? ev.result : {};
|
|
296
405
|
this.applyToolResults(s, [{ ...result, toolCallId: ev.toolCallId, isError: ev.isError }]);
|
|
297
|
-
|
|
406
|
+
return;
|
|
298
407
|
}
|
|
408
|
+
default:
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
onMessageEvent(s, ev) {
|
|
413
|
+
switch (ev.type) {
|
|
299
414
|
case "message_start": {
|
|
300
415
|
if (this.reconcileUser(s, ev))
|
|
301
|
-
|
|
416
|
+
return;
|
|
302
417
|
const msg = ev.message;
|
|
303
418
|
if (msg?.role !== "assistant")
|
|
304
|
-
|
|
419
|
+
return; // user message already recorded at prompt()
|
|
305
420
|
// pi stamps the assistant message with the model really serving it.
|
|
306
421
|
// Trust that over our snapshot, which goes stale whenever pi switches on
|
|
307
422
|
// its own side (subagent routing, an in-session /model, a resumed file).
|
|
308
423
|
this.services.applyPiModel(s, msg.provider, piModelId(msg.responseModel, msg.model));
|
|
309
|
-
const parent =
|
|
424
|
+
const parent = s.messages?.findLast((m) => m.info.role === "user");
|
|
310
425
|
const assistant = {
|
|
311
426
|
id: id("msg"),
|
|
312
427
|
sessionID: s.info.id,
|
|
@@ -324,91 +439,121 @@ export class PiEventTranslator {
|
|
|
324
439
|
};
|
|
325
440
|
s.streaming = { message: assistant, parts: new Map() };
|
|
326
441
|
s.messages?.push({ info: assistant, parts: [] });
|
|
327
|
-
this.services.emit(
|
|
328
|
-
|
|
442
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: assistant } });
|
|
443
|
+
return;
|
|
329
444
|
}
|
|
330
445
|
case "message_update":
|
|
331
446
|
this.onMessageUpdate(s, ev);
|
|
332
|
-
|
|
447
|
+
return;
|
|
333
448
|
case "message_end": {
|
|
334
449
|
if (this.reconcileUser(s, ev))
|
|
335
|
-
|
|
450
|
+
return;
|
|
336
451
|
const ended = ev.message;
|
|
337
|
-
if (ended?.role
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// Last chance to correct attribution: some providers only reveal the
|
|
344
|
-
// concrete model once the stream has closed.
|
|
345
|
-
if (this.services.applyPiModel(s, ended.provider, piModelId(ended.responseModel, ended.model)) &&
|
|
346
|
-
s.model !== undefined) {
|
|
347
|
-
s.streaming.message.providerID = s.model.providerID;
|
|
348
|
-
s.streaming.message.modelID = s.model.modelID;
|
|
349
|
-
}
|
|
350
|
-
applyUsage(s.streaming.message, ended.usage);
|
|
351
|
-
Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
|
|
352
|
-
s.streaming.message.time.completed = Date.now();
|
|
353
|
-
this.services.emit(dir, { type: "message.updated", properties: { info: s.streaming.message } });
|
|
354
|
-
this.finishStreaming(s);
|
|
355
|
-
if (syncSessionCost(s))
|
|
356
|
-
this.services.emit(dir, { type: "session.updated", properties: { info: s.info } });
|
|
452
|
+
if (ended?.role !== "assistant" || !s.streaming)
|
|
453
|
+
return;
|
|
454
|
+
this.onProviderTools(s, ev.message);
|
|
455
|
+
if (ended.stopReason === "aborted" || ended.stopReason === "error") {
|
|
456
|
+
for (const part of finishProviderToolParts(s.toolByCallId.values(), Date.now(), ended.stopReason === "aborted" ? "Cancelled" : "No result"))
|
|
457
|
+
this.emitPart(s, part);
|
|
357
458
|
}
|
|
358
|
-
|
|
459
|
+
// Last chance to correct attribution: some providers only reveal the
|
|
460
|
+
// concrete model once the stream has closed.
|
|
461
|
+
if (this.services.applyPiModel(s, ended.provider, piModelId(ended.responseModel, ended.model)) &&
|
|
462
|
+
s.model !== undefined) {
|
|
463
|
+
s.streaming.message.providerID = s.model.providerID;
|
|
464
|
+
s.streaming.message.modelID = s.model.modelID;
|
|
465
|
+
}
|
|
466
|
+
applyUsage(s.streaming.message, ended.usage);
|
|
467
|
+
Object.assign(s.streaming.message, piStopReason(ended.stopReason, ended.errorMessage));
|
|
468
|
+
s.streaming.message.time.completed = Date.now();
|
|
469
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: s.streaming.message } });
|
|
470
|
+
this.finishStreaming(s);
|
|
471
|
+
if (syncSessionCost(s))
|
|
472
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
473
|
+
return;
|
|
359
474
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
type: "session.error",
|
|
475
|
+
default:
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
onCompactionEvent(s, ev) {
|
|
480
|
+
switch (ev.type) {
|
|
481
|
+
case "compaction_start":
|
|
482
|
+
s.compactionID = id("compaction");
|
|
483
|
+
this.services.emit(s.info.directory, {
|
|
484
|
+
type: "session.compaction.started",
|
|
371
485
|
properties: {
|
|
372
486
|
sessionID: s.info.id,
|
|
373
|
-
|
|
487
|
+
inputID: s.compactionID,
|
|
488
|
+
reason: typeof ev.reason === "string" ? ev.reason : "manual",
|
|
374
489
|
},
|
|
375
490
|
});
|
|
376
|
-
break;
|
|
377
|
-
case "compaction_start": {
|
|
378
491
|
s.info.time.compacting = Date.now();
|
|
379
|
-
this.services.emit(
|
|
492
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
380
493
|
// A manual /compact runs no agent turn, so nothing else marks the session busy.
|
|
381
494
|
if (s.status.type === "idle") {
|
|
382
495
|
s.compactingFromIdle = true;
|
|
383
496
|
this.setStatus(s, { type: "busy" });
|
|
384
497
|
}
|
|
385
|
-
|
|
386
|
-
}
|
|
498
|
+
return;
|
|
387
499
|
case "compaction_end": {
|
|
388
500
|
delete s.info.time.compacting;
|
|
389
501
|
const result = ev.result;
|
|
502
|
+
const inputID = s.compactionID ?? id("compaction");
|
|
503
|
+
delete s.compactionID;
|
|
504
|
+
this.services.emit(s.info.directory, ev.aborted === true || typeof result?.summary !== "string"
|
|
505
|
+
? {
|
|
506
|
+
type: "session.compaction.failed",
|
|
507
|
+
properties: {
|
|
508
|
+
sessionID: s.info.id,
|
|
509
|
+
inputID,
|
|
510
|
+
reason: typeof ev.reason === "string" ? ev.reason : "manual",
|
|
511
|
+
error: {
|
|
512
|
+
name: "CompactionError",
|
|
513
|
+
data: { message: ev.aborted === true ? "Compaction cancelled" : "Compaction failed" },
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
: {
|
|
518
|
+
type: "session.compaction.ended",
|
|
519
|
+
properties: {
|
|
520
|
+
sessionID: s.info.id,
|
|
521
|
+
inputID,
|
|
522
|
+
reason: typeof ev.reason === "string" ? ev.reason : "manual",
|
|
523
|
+
text: result.summary,
|
|
524
|
+
recent: "",
|
|
525
|
+
},
|
|
526
|
+
});
|
|
390
527
|
if (ev.aborted !== true && typeof result?.summary === "string" && typeof result.firstKeptEntryId === "string") {
|
|
391
528
|
const messages = this.services.ensureMessages(s);
|
|
392
|
-
|
|
529
|
+
let key;
|
|
530
|
+
// compaction_end follows persistence, but its result omits the entry ID.
|
|
531
|
+
if (s.piPath !== undefined) {
|
|
532
|
+
for (const entry of readSessionTree(s.piPath).entries) {
|
|
533
|
+
if (entry.type === "compaction")
|
|
534
|
+
key = entry.id;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
for (const message of compactionMessages(s, key ?? id("compaction"), result.summary, Date.now(), s.model)) {
|
|
393
538
|
if (messages.some((item) => item.info.id === message.info.id))
|
|
394
539
|
continue;
|
|
395
540
|
// Keep a hidden redo branch after the new turn, as appendMessage does.
|
|
396
541
|
const boundary = messages.findIndex((item) => item.info.id === s.tree.revert?.messageID);
|
|
397
542
|
messages.splice(boundary < 0 ? messages.length : boundary, 0, message);
|
|
398
|
-
this.services.emit(
|
|
543
|
+
this.services.emit(s.info.directory, { type: "message.updated", properties: { info: message.info } });
|
|
399
544
|
for (const part of message.parts)
|
|
400
545
|
this.emitPart(s, part);
|
|
401
546
|
}
|
|
402
547
|
}
|
|
403
|
-
this.services.emit(
|
|
548
|
+
this.services.emit(s.info.directory, { type: "session.updated", properties: { info: s.info } });
|
|
404
549
|
if (s.compactingFromIdle) {
|
|
405
550
|
delete s.compactingFromIdle;
|
|
406
551
|
this.setStatus(s, { type: "idle" });
|
|
407
552
|
}
|
|
408
|
-
|
|
553
|
+
return;
|
|
409
554
|
}
|
|
410
555
|
default:
|
|
411
|
-
|
|
556
|
+
return;
|
|
412
557
|
}
|
|
413
558
|
}
|
|
414
559
|
finishStreaming(s) {
|
|
@@ -449,7 +594,19 @@ export class PiEventTranslator {
|
|
|
449
594
|
const textPart = part;
|
|
450
595
|
if (ame.type === `${wire}_delta` && typeof ame.delta === "string") {
|
|
451
596
|
textPart.text += ame.delta;
|
|
452
|
-
this.
|
|
597
|
+
this.services.emit(dir, {
|
|
598
|
+
type: "message.part.delta",
|
|
599
|
+
properties: {
|
|
600
|
+
sessionID: s.info.id,
|
|
601
|
+
messageID: textPart.messageID,
|
|
602
|
+
partID: textPart.id,
|
|
603
|
+
field: "text",
|
|
604
|
+
delta: ame.delta,
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
// Append before publishing the authoritative snapshot. Sending another
|
|
608
|
+
// append delta on the snapshot would duplicate text in streaming clients.
|
|
609
|
+
this.emitPart(s, textPart);
|
|
453
610
|
}
|
|
454
611
|
else if (ame.type === `${wire}_end`) {
|
|
455
612
|
if (typeof ame.content === "string")
|
package/dist/pi-rpc.d.ts
CHANGED
|
@@ -21,7 +21,20 @@ export declare const RPC_COMMAND_TIMEOUT_MS = 60000;
|
|
|
21
21
|
* long before a large session finished, leaving the run orphaned.
|
|
22
22
|
*/
|
|
23
23
|
export declare const COMPACT_RPC_TIMEOUT_MS: number;
|
|
24
|
-
export declare
|
|
24
|
+
export declare class UpstreamProtocolError extends Error {
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
/** A process's group and kernel start time: with the PID, the identity that survives PID reuse checks. */
|
|
28
|
+
interface ProcessIdentity {
|
|
29
|
+
readonly pid: number;
|
|
30
|
+
readonly group: number;
|
|
31
|
+
readonly start: string;
|
|
32
|
+
}
|
|
33
|
+
/** Reads process identities so disposal can tell owned survivors from a reused PID. */
|
|
34
|
+
export interface ProcessTable {
|
|
35
|
+
identity(pid: number): ProcessIdentity | undefined;
|
|
36
|
+
groupMembers(group: number): ProcessIdentity[];
|
|
37
|
+
}
|
|
25
38
|
export type PiRpcResponse<T = unknown> = {
|
|
26
39
|
readonly id: string;
|
|
27
40
|
readonly type: "response";
|
|
@@ -35,13 +48,14 @@ export type PiRpcResponse<T = unknown> = {
|
|
|
35
48
|
readonly error?: string;
|
|
36
49
|
readonly data?: never;
|
|
37
50
|
});
|
|
38
|
-
|
|
51
|
+
interface PiRpcClientOptions {
|
|
39
52
|
cwd: string;
|
|
40
53
|
/** Path/name of the pi binary. Defaults to ANTECHAMBER_PI_BINARY env or "omo" from PATH. */
|
|
41
54
|
piBinary?: string;
|
|
42
55
|
/** Extra CLI flags, e.g. ["--no-session"] or ["--provider", "x"]. */
|
|
43
56
|
args?: string[];
|
|
44
57
|
clock?: LifecycleClock;
|
|
58
|
+
processTable?: ProcessTable;
|
|
45
59
|
}
|
|
46
60
|
export declare class PiRpcClient extends EventEmitter {
|
|
47
61
|
private options;
|
|
@@ -61,6 +75,9 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
61
75
|
constructor(options: PiRpcClientOptions);
|
|
62
76
|
private drain;
|
|
63
77
|
private frameOverflow;
|
|
78
|
+
/** Node has reaped the leader, so its PID (and group ID) may already belong to another process. */
|
|
79
|
+
private reaped;
|
|
80
|
+
private closeForTransportFailure;
|
|
64
81
|
private handleLine;
|
|
65
82
|
private handleExtensionUI;
|
|
66
83
|
respondExtensionUI(response: Record<string, unknown>): void;
|
|
@@ -72,3 +89,4 @@ export declare class PiRpcClient extends EventEmitter {
|
|
|
72
89
|
notify(cmd: Omit<PiRpcCommand, "id">): void;
|
|
73
90
|
dispose(): Promise<void>;
|
|
74
91
|
}
|
|
92
|
+
export {};
|