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