omnirush 0.9.1 → 0.10.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/assets/extensions/omnirush/agents-lib.ts +53 -15
- package/assets/extensions/omnirush/bgshell.ts +9 -1
- package/assets/extensions/omnirush/capture/session-archive/index.ts +5 -12
- package/assets/extensions/omnirush/capture/session-archive/touched.ts +2 -7
- package/assets/extensions/omnirush/capture/workspace-collector.ts +1 -2
- package/assets/extensions/omnirush/index.ts +4 -0
- package/assets/extensions/omnirush/mcp.ts +2 -1
- package/assets/extensions/omnirush/pi-engine.ts +399 -45
- package/assets/extensions/omnirush/secret-env.ts +15 -0
- package/assets/extensions/omnirush/stream-timing.ts +140 -0
- package/assets/extensions/omnirush/subagents-lib.ts +5 -0
- package/package.json +3 -3
- package/src/bin.js +102 -22
- package/src/lib.js +7 -2
|
@@ -49,13 +49,35 @@
|
|
|
49
49
|
// Ids are stable across processes (derived from the pi entry ids), so a
|
|
50
50
|
// resumed session or a repeated flush merges instead of duplicating.
|
|
51
51
|
//
|
|
52
|
+
// The 2.x engine's own fields ride along, the way the desktop's 2.x engine
|
|
53
|
+
// adds them to the same 1.x records (omnirush-gui engine2/native.ts, whose
|
|
54
|
+
// merge rules `mergeNative` repeats): a 2.x field the 1.x record does not
|
|
55
|
+
// have is added under its 2.x name (info.type, info.text, info.model,
|
|
56
|
+
// info.time.streamed, info.rawFinish, info.providerState, info.snapshot,
|
|
57
|
+
// text/reasoning `state`, reasoning `time`, tool `name` / `executed` /
|
|
58
|
+
// `providerState` / `time` {created, ran, completed}, tool state `content`,
|
|
59
|
+
// the shell and compaction records' fields); objects both have (time,
|
|
60
|
+
// tokens, error, tool state) are merged key by key; a 2.x value under a name
|
|
61
|
+
// the 1.x record already uses with another value goes under `v2.<name>` on
|
|
62
|
+
// the same record (tokens.v2.output: 2.x counts visible output without the
|
|
63
|
+
// reasoning tokens pi's output includes; a tool state's v2.error); and 2.x
|
|
64
|
+
// records 1.x never listed become parts of the nearest message, typed by
|
|
65
|
+
// their 2.x type: "model-switched" (pi's model and thinking-level changes,
|
|
66
|
+
// and a prompt sent to another model than the last one recorded) and "idle"
|
|
67
|
+
// (the run's outcome). What pi does not record is left out: the
|
|
68
|
+
// step snapshots' hashes, provider-executed tools and their result state,
|
|
69
|
+
// the service tier, encrypted reasoning and signatures (kept out of the
|
|
70
|
+
// trace, as before), retries, agent and location switches. Stream timing
|
|
71
|
+
// (block and tool-call starts and ends, a tool's run start, the run's idle
|
|
72
|
+
// outcome) comes from stream-timing.ts's `omnirush-timing` entries.
|
|
73
|
+
//
|
|
52
74
|
// Also here, pure and shared with the tests: the helpers the desktop's turn
|
|
53
75
|
// observer (apps/server/src/collector-observer.ts) applies to the engine's
|
|
54
76
|
// messages when a turn settles: the turn's model, the message budget of a
|
|
55
77
|
// flush and its split into `turn.messages` parts.
|
|
56
78
|
|
|
57
79
|
import { createHash } from "node:crypto";
|
|
58
|
-
import { isAbsolute, resolve } from "node:path";
|
|
80
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
59
81
|
|
|
60
82
|
/** customType of bgshell.ts's finished-background-command message. */
|
|
61
83
|
export const BACKGROUND_BASH_TYPE = "omnirush-bash-result";
|
|
@@ -67,6 +89,8 @@ export const BACKGROUND_BASH_TYPE = "omnirush-bash-result";
|
|
|
67
89
|
export const SUBAGENT_ENTRY_TYPE = "omnirush-subagent";
|
|
68
90
|
/** A sub-agent's prompt: the task the parent agent wrote, after this prefix (agents-lib.ts buildChildArgs). */
|
|
69
91
|
export const SUBAGENT_TASK_PREFIX = "Task: ";
|
|
92
|
+
/** customType of stream-timing.ts's entries (a model step's stream timing, a run's idle outcome). */
|
|
93
|
+
export const TIMING_ENTRY_TYPE = "omnirush-timing";
|
|
70
94
|
|
|
71
95
|
export type EngineMessage = { info: Record<string, unknown>; parts: Array<Record<string, unknown>> };
|
|
72
96
|
|
|
@@ -262,6 +286,172 @@ export function spawnedSessionIds(details: unknown): string[] {
|
|
|
262
286
|
|
|
263
287
|
type ToolResult = { message: Record<string, any>; at: number | null };
|
|
264
288
|
|
|
289
|
+
// --- the 2.x engine's own fields (omnirush-gui apps/server/src/engine2/native.ts) ---
|
|
290
|
+
|
|
291
|
+
/** Where a 2.x value goes when its name is taken on the 1.x record by another value. */
|
|
292
|
+
export const NATIVE_COLLISIONS = "v2";
|
|
293
|
+
const INFO_NESTED = { time: {}, tokens: { cache: {} }, error: { data: {} } };
|
|
294
|
+
const STATE_NESTED = { metadata: {}, time: {} };
|
|
295
|
+
|
|
296
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
297
|
+
if (a === b) return true;
|
|
298
|
+
if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") return false;
|
|
299
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
300
|
+
if (Array.isArray(a)) {
|
|
301
|
+
const other = b as unknown[];
|
|
302
|
+
return a.length === other.length && a.every((item, index) => deepEqual(item, other[index]));
|
|
303
|
+
}
|
|
304
|
+
const ka = Object.keys(a as object);
|
|
305
|
+
const kb = Object.keys(b as object);
|
|
306
|
+
return ka.length === kb.length && ka.every((key) => deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Adds 2.x fields (`native`) to a 1.x record, as the desktop's 2.x engine
|
|
311
|
+
* does: a new name is added as-is, an equal value is not repeated, the
|
|
312
|
+
* objects `nested` names are merged key by key (its value: the keys merged
|
|
313
|
+
* one level further down), and any other value under a taken name goes under
|
|
314
|
+
* `v2.<name>`. Objects of `target` are copied before they change.
|
|
315
|
+
*/
|
|
316
|
+
export function mergeNative(
|
|
317
|
+
target: Record<string, unknown>,
|
|
318
|
+
native: Record<string, unknown>,
|
|
319
|
+
nested: Record<string, Record<string, unknown>> = {},
|
|
320
|
+
): Record<string, unknown> {
|
|
321
|
+
for (const [key, value] of Object.entries(native)) {
|
|
322
|
+
if (value === undefined || key === NATIVE_COLLISIONS) continue;
|
|
323
|
+
if (!(key in target)) {
|
|
324
|
+
target[key] = value;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const current = target[key];
|
|
328
|
+
if (deepEqual(current, value)) continue;
|
|
329
|
+
if (key in nested && isRecord(current) && isRecord(value)) {
|
|
330
|
+
target[key] = mergeNative({ ...current }, value, nested[key] as Record<string, Record<string, unknown>>);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const bucket: Record<string, unknown> = isRecord(target[NATIVE_COLLISIONS]) ? { ...(target[NATIVE_COLLISIONS] as Record<string, unknown>) } : {};
|
|
334
|
+
bucket[key] = value;
|
|
335
|
+
target[NATIVE_COLLISIONS] = bucket;
|
|
336
|
+
}
|
|
337
|
+
return target;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** `record` without its undefined values. */
|
|
341
|
+
function defined<T extends Record<string, unknown>>(record: T): T {
|
|
342
|
+
for (const key of Object.keys(record)) if (record[key] === undefined) delete record[key];
|
|
343
|
+
return record;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseJson(text: string): any {
|
|
347
|
+
try {
|
|
348
|
+
return JSON.parse(text);
|
|
349
|
+
} catch {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** OpenAI Responses APIs: pi keeps their item and response ids, the 2.x engine's provider state. */
|
|
355
|
+
function responsesApi(api: unknown): boolean {
|
|
356
|
+
return typeof api === "string" && api.includes("responses");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** A text block's 2.x provider state: its Responses message item id and phase (pi's textSignature). */
|
|
360
|
+
function textState(block: Record<string, any>, api: unknown): Record<string, unknown> | undefined {
|
|
361
|
+
const signature = typeof block.textSignature === "string" ? block.textSignature : "";
|
|
362
|
+
if (!responsesApi(api) || !signature) return undefined;
|
|
363
|
+
if (!signature.startsWith("{")) return { itemId: signature };
|
|
364
|
+
const parsed = parseJson(signature);
|
|
365
|
+
if (!isRecord(parsed) || typeof parsed.id !== "string") return undefined;
|
|
366
|
+
return { itemId: parsed.id, ...(typeof parsed.phase === "string" ? { phase: parsed.phase } : {}) };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A thinking block's 2.x provider state: its Responses reasoning item id.
|
|
371
|
+
* The encrypted reasoning the item also holds stays out of the trace.
|
|
372
|
+
*/
|
|
373
|
+
function reasoningState(block: Record<string, any>, api: unknown): Record<string, unknown> | undefined {
|
|
374
|
+
if (!responsesApi(api) || typeof block.thinkingSignature !== "string") return undefined;
|
|
375
|
+
const item = parseJson(block.thinkingSignature);
|
|
376
|
+
return isRecord(item) && typeof item.id === "string" ? { itemId: item.id } : undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** A pi tool call id as the 2.x call id and item id (Responses calls are `<call_id>|<item id>`). */
|
|
380
|
+
function nativeCallId(id: string, api: unknown): { id: string; itemId?: string } {
|
|
381
|
+
if (!responsesApi(api)) return { id };
|
|
382
|
+
const [callId, itemId] = id.split("|");
|
|
383
|
+
return callId && itemId ? { id: callId, itemId } : { id };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** The provider's own finish reason, as the 2.x engine keeps it (Responses: the incomplete reason only). */
|
|
387
|
+
function nativeRawFinish(message: Record<string, any>): string | undefined {
|
|
388
|
+
const raw = typeof message.rawStopReason === "string" ? message.rawStopReason : "";
|
|
389
|
+
if (!raw) return undefined;
|
|
390
|
+
if (!responsesApi(message.api)) return raw;
|
|
391
|
+
const incomplete = /^incomplete\.(.+)$/.exec(raw);
|
|
392
|
+
return incomplete ? incomplete[1] : undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** A failed or aborted step's 2.x error ({type, message}). */
|
|
396
|
+
function nativeError(message: Record<string, any>): Record<string, unknown> | undefined {
|
|
397
|
+
const text = typeof message.errorMessage === "string" && message.errorMessage ? message.errorMessage : undefined;
|
|
398
|
+
if (message.stopReason === "aborted") return { type: "aborted", message: text ?? "aborted" };
|
|
399
|
+
if (message.stopReason === "error") return { type: "provider.unknown", message: text ?? "error" };
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** pi usage as the 2.x step tokens: output without the reasoning tokens pi counts in it. */
|
|
404
|
+
function nativeTokens(usage: unknown): Record<string, unknown> | undefined {
|
|
405
|
+
if (!isRecord(usage)) return undefined;
|
|
406
|
+
const tokens = stepTokens(usage) as { input: number; output: number; reasoning: number; cache: { read: number; write: number } };
|
|
407
|
+
return { input: tokens.input, output: Math.max(0, tokens.output - tokens.reasoning), reasoning: tokens.reasoning, cache: tokens.cache };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** A tool result's content as the 2.x tool content (images: the 1.x attachment holds their description). */
|
|
411
|
+
function nativeToolContent(content: unknown): Array<Record<string, unknown>> {
|
|
412
|
+
const out: Array<Record<string, unknown>> = [];
|
|
413
|
+
let images = 0;
|
|
414
|
+
for (const block of Array.isArray(content) ? content : typeof content === "string" ? [content] : []) {
|
|
415
|
+
if (typeof block === "string") {
|
|
416
|
+
if (block) out.push({ type: "text", text: block });
|
|
417
|
+
} else if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
418
|
+
out.push({ type: "text", text: block.text });
|
|
419
|
+
} else if (isRecord(block) && block.type === "image") {
|
|
420
|
+
const file = imagePart(block, images);
|
|
421
|
+
out.push({ type: "file", uri: `sameAs:state.attachments[${images}].url`, mime: file.mime, name: file.filename });
|
|
422
|
+
images += 1;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Stream timing a step's `omnirush-timing` entry recorded (stream-timing.ts). */
|
|
429
|
+
type StepTiming = { streamed?: number; blocks: Map<number, { created?: number; completed?: number }>; tools: Record<string, number> };
|
|
430
|
+
|
|
431
|
+
function stepTimings(entries: readonly PiEntry[]): Map<number, StepTiming> {
|
|
432
|
+
const timings = new Map<number, StepTiming>();
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
if (entry.type !== "custom" || entry.customType !== TIMING_ENTRY_TYPE || !isRecord(entry.data)) continue;
|
|
435
|
+
const data = entry.data;
|
|
436
|
+
if (typeof data.step !== "number") continue;
|
|
437
|
+
const blocks = new Map<number, { created?: number; completed?: number }>();
|
|
438
|
+
for (const block of Array.isArray(data.blocks) ? data.blocks : []) {
|
|
439
|
+
if (!isRecord(block) || typeof block.index !== "number") continue;
|
|
440
|
+
blocks.set(block.index, defined({ created: epochMs(block.created) ?? undefined, completed: epochMs(block.completed) ?? undefined }));
|
|
441
|
+
}
|
|
442
|
+
const tools: Record<string, number> = {};
|
|
443
|
+
if (isRecord(data.tools)) for (const [id, at] of Object.entries(data.tools)) if (epochMs(at) !== null) tools[id] = epochMs(at)!;
|
|
444
|
+
timings.set(data.step, { ...(epochMs(data.streamed) !== null ? { streamed: epochMs(data.streamed)! } : {}), blocks, tools });
|
|
445
|
+
}
|
|
446
|
+
return timings;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** A user message the viewer never takes for a prompt (only synthetic text or a compaction): context records do not join it. */
|
|
450
|
+
function engineWritten(message: EngineMessage): boolean {
|
|
451
|
+
return message.info.role === "user"
|
|
452
|
+
&& message.parts.every((part) => (part.type === "text" && part.synthetic === true) || part.type === "compaction");
|
|
453
|
+
}
|
|
454
|
+
|
|
265
455
|
/**
|
|
266
456
|
* The engine messages of a pi session branch (entries root to leaf, as
|
|
267
457
|
* SessionManager.getBranch() returns them or a session file lists them).
|
|
@@ -291,15 +481,69 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
291
481
|
if (message?.role === "user") upcoming = null;
|
|
292
482
|
}
|
|
293
483
|
|
|
484
|
+
const timings = stepTimings(entries);
|
|
485
|
+
|
|
294
486
|
const out: EngineMessage[] = [];
|
|
295
487
|
let model: { providerID: string; modelID: string } | null = options.initialModel ?? null;
|
|
296
488
|
let thinkingLevel: string | null = options.initialThinkingLevel ?? null;
|
|
297
489
|
let lastUserId: string | null = null;
|
|
298
490
|
const variant = () => (thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : null);
|
|
299
491
|
|
|
492
|
+
// 2.x context records (model switches) wait for the next message that takes them.
|
|
493
|
+
const pendingRecords: Array<Record<string, unknown>> = [];
|
|
494
|
+
const contextPart = (record: Record<string, unknown>, host: EngineMessage) => ({ ...record, sessionID: sessionId, messageID: host.info.id });
|
|
495
|
+
/** Every message goes out through here: waiting context records join the front of the first one the viewer does not take for engine-written. */
|
|
496
|
+
const emit = (message: EngineMessage): void => {
|
|
497
|
+
if (pendingRecords.length > 0 && !engineWritten(message)) {
|
|
498
|
+
message.parts = [...pendingRecords.splice(0).map((record) => contextPart(record, message)), ...message.parts];
|
|
499
|
+
}
|
|
500
|
+
out.push(message);
|
|
501
|
+
};
|
|
502
|
+
/** A 2.x record at the end of the latest message that takes one (the run's idle marker). */
|
|
503
|
+
const appendRecord = (record: Record<string, unknown>): void => {
|
|
504
|
+
for (let index = out.length - 1; index >= 0; index -= 1) {
|
|
505
|
+
const host = out[index]!;
|
|
506
|
+
if (engineWritten(host)) continue;
|
|
507
|
+
host.parts = [...host.parts, contextPart(record, host)];
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
// 2.x model-switched records: a model or thinking-level change pi recorded
|
|
512
|
+
// (the latest of a run of changes, once a message follows), or a prompt
|
|
513
|
+
// sent to another model than the last one recorded (pi records no change
|
|
514
|
+
// for `--model` on a resumed session). Tracked apart from the 1.x state.
|
|
515
|
+
type ModelState = { model: { providerID: string; modelID: string } | null; thinkingLevel: string | null };
|
|
516
|
+
let recorded: ModelState = { model, thinkingLevel };
|
|
517
|
+
const modelRef = (state: ModelState) => {
|
|
518
|
+
if (!state.model) return undefined;
|
|
519
|
+
const effort = state.thinkingLevel && state.thinkingLevel !== "off" ? state.thinkingLevel : undefined;
|
|
520
|
+
return { id: state.model.modelID, providerID: state.model.providerID, ...(effort ? { variant: effort } : {}) };
|
|
521
|
+
};
|
|
522
|
+
const recordSwitch = (id: string, at: number | null, next: ModelState) => {
|
|
523
|
+
const now = modelRef(next);
|
|
524
|
+
const previous = modelRef(recorded);
|
|
525
|
+
recorded = next;
|
|
526
|
+
if (!now || deepEqual(now, previous)) return;
|
|
527
|
+
pendingRecords.push(defined({ id, time: { created: at ?? 0 }, type: "model-switched", model: now, previous }));
|
|
528
|
+
};
|
|
529
|
+
let pendingSwitch: { entry: PiEntry; at: number | null } | null = null;
|
|
530
|
+
const settleSwitch = () => {
|
|
531
|
+
if (!pendingSwitch) return;
|
|
532
|
+
const { entry, at } = pendingSwitch;
|
|
533
|
+
pendingSwitch = null;
|
|
534
|
+
recordSwitch(engineMessageId(String(entry.id)), at, { model, thinkingLevel });
|
|
535
|
+
};
|
|
536
|
+
|
|
300
537
|
// The effort of the prompt the current turn answers (a live setting when known).
|
|
301
538
|
let turnVariant: string | null = null;
|
|
302
|
-
const userMessage = (
|
|
539
|
+
const userMessage = (
|
|
540
|
+
id: string,
|
|
541
|
+
created: number | null,
|
|
542
|
+
parts: Array<Record<string, unknown>>,
|
|
543
|
+
index: number,
|
|
544
|
+
extraInfo: Record<string, unknown> = {},
|
|
545
|
+
native?: Record<string, unknown>,
|
|
546
|
+
): string | null => {
|
|
303
547
|
const live = typeof entries[index]?.id === "string" ? options.promptSettings?.get(entries[index]!.id as string) : undefined;
|
|
304
548
|
const messageModel = nextAssistantModel.get(index) ?? live?.model ?? model ?? null;
|
|
305
549
|
const messageVariant = live && live.variant !== undefined ? live.variant : variant();
|
|
@@ -313,10 +557,23 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
313
557
|
...(messageVariant ? { variant: messageVariant } : {}),
|
|
314
558
|
...extraInfo,
|
|
315
559
|
};
|
|
316
|
-
|
|
560
|
+
// A prompt sent to another model than the last one recorded: that switch first.
|
|
561
|
+
if (native?.type === "user") recordSwitch(engineMessageId(String(entries[index]?.id ?? id), "_model"), created, { model: messageModel, thinkingLevel: messageVariant ?? null });
|
|
562
|
+
const message: EngineMessage = {
|
|
317
563
|
info,
|
|
318
564
|
parts: parts.map((part, partIndex) => ({ id: partId(id, partIndex), sessionID: sessionId, messageID: id, ...part })),
|
|
319
|
-
}
|
|
565
|
+
};
|
|
566
|
+
if (native) {
|
|
567
|
+
// 2.x prompt fields (engine2/native.ts enrichPrompt): an attached image's
|
|
568
|
+
// entry points at its 1.x file part (the bytes are not embedded), and its
|
|
569
|
+
// other fields join that part.
|
|
570
|
+
const files = message.parts.flatMap((part, partIndex) => part.type === "file"
|
|
571
|
+
? [defined({ uri: `sameAs:parts[${partIndex}].url`, mime: part.mime as string | undefined, name: part.filename as string | undefined })]
|
|
572
|
+
: []);
|
|
573
|
+
message.info = mergeNative({ ...message.info }, { ...native, ...(files.length > 0 ? { files } : {}) }, INFO_NESTED);
|
|
574
|
+
for (const part of message.parts) if (part.type === "file") mergeNative(part, defined({ mime: part.mime, name: part.filename }));
|
|
575
|
+
}
|
|
576
|
+
emit(message);
|
|
320
577
|
lastUserId = id;
|
|
321
578
|
return messageVariant ?? null;
|
|
322
579
|
};
|
|
@@ -356,7 +613,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
356
613
|
},
|
|
357
614
|
};
|
|
358
615
|
});
|
|
359
|
-
|
|
616
|
+
emit({
|
|
360
617
|
info: {
|
|
361
618
|
id: shellId,
|
|
362
619
|
sessionID: sessionId,
|
|
@@ -380,13 +637,28 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
380
637
|
if (!entryId) return;
|
|
381
638
|
const at = epochMs(entry.timestamp);
|
|
382
639
|
if (entry.type === "model_change") {
|
|
383
|
-
if (typeof entry.provider === "string" && typeof entry.modelId === "string")
|
|
640
|
+
if (typeof entry.provider === "string" && typeof entry.modelId === "string") {
|
|
641
|
+
model = { providerID: entry.provider, modelID: entry.modelId };
|
|
642
|
+
pendingSwitch = { entry, at };
|
|
643
|
+
}
|
|
384
644
|
return;
|
|
385
645
|
}
|
|
386
646
|
if (entry.type === "thinking_level_change") {
|
|
387
|
-
if (typeof entry.thinkingLevel === "string")
|
|
647
|
+
if (typeof entry.thinkingLevel === "string") {
|
|
648
|
+
thinkingLevel = entry.thinkingLevel;
|
|
649
|
+
pendingSwitch = { entry, at };
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (entry.type === "custom" && entry.customType === TIMING_ENTRY_TYPE) {
|
|
654
|
+
// The run went idle here: its 2.x idle marker closes the run's last message.
|
|
655
|
+
const idle = isRecord(entry.data) && isRecord(entry.data.idle) ? entry.data.idle : null;
|
|
656
|
+
const outcome = idle && typeof idle.outcome === "string" ? idle.outcome : null;
|
|
657
|
+
if (idle && outcome) appendRecord({ id: engineMessageId(entryId), time: { created: epochMs(idle.at) ?? at ?? 0 }, type: "idle", outcome });
|
|
388
658
|
return;
|
|
389
659
|
}
|
|
660
|
+
// A message follows: the model switch before it is recorded.
|
|
661
|
+
settleSwitch();
|
|
390
662
|
if (entry.type === "compaction") {
|
|
391
663
|
const id = engineMessageId(entryId);
|
|
392
664
|
userMessage(id, at, [{ type: "compaction" }], index);
|
|
@@ -394,8 +666,9 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
394
666
|
const summaryParts: Array<Record<string, unknown>> = [];
|
|
395
667
|
if (typeof entry.summary === "string" && entry.summary) summaryParts.push({ type: "text", text: entry.summary });
|
|
396
668
|
if (isRecord(entry.usage)) summaryParts.push({ type: "step-finish", reason: "stop", tokens: stepTokens(entry.usage), cost: stepCost(entry.usage) });
|
|
397
|
-
|
|
398
|
-
|
|
669
|
+
emit({
|
|
670
|
+
// The 2.x compaction record's fields on its summary reply, as the desktop keeps them.
|
|
671
|
+
info: mergeNative({
|
|
399
672
|
id: summaryId,
|
|
400
673
|
sessionID: sessionId,
|
|
401
674
|
role: "assistant",
|
|
@@ -407,21 +680,26 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
407
680
|
path: { cwd, root: cwd },
|
|
408
681
|
...(isRecord(entry.usage) ? { tokens: stepTokens(entry.usage), cost: stepCost(entry.usage) } : {}),
|
|
409
682
|
finish: "stop",
|
|
410
|
-
},
|
|
683
|
+
}, defined({
|
|
684
|
+
type: "compaction",
|
|
685
|
+
status: "completed",
|
|
686
|
+
summary: typeof entry.summary === "string" ? entry.summary : undefined,
|
|
687
|
+
tokens: nativeTokens(entry.usage),
|
|
688
|
+
}), INFO_NESTED),
|
|
411
689
|
parts: summaryParts.map((part, partIndex) => ({ id: partId(summaryId, partIndex), sessionID: sessionId, messageID: summaryId, ...part })),
|
|
412
690
|
});
|
|
413
691
|
return;
|
|
414
692
|
}
|
|
415
693
|
if (entry.type === "branch_summary") {
|
|
416
694
|
if (typeof entry.summary === "string" && entry.summary) {
|
|
417
|
-
userMessage(engineMessageId(entryId), at, [{ type: "text", text: entry.summary, synthetic: true, metadata: { branchSummary: true } }], index);
|
|
695
|
+
userMessage(engineMessageId(entryId), at, [{ type: "text", text: entry.summary, synthetic: true, metadata: { branchSummary: true } }], index, {}, { type: "synthetic", text: entry.summary });
|
|
418
696
|
}
|
|
419
697
|
return;
|
|
420
698
|
}
|
|
421
699
|
if (entry.type === "custom_message") {
|
|
422
700
|
const parts = contentParts(entry.content, { synthetic: true, metadata: { customType: entry.customType ?? null } });
|
|
423
701
|
const id = engineMessageId(entryId);
|
|
424
|
-
if (parts.length > 0) userMessage(id, at, parts, index);
|
|
702
|
+
if (parts.length > 0) userMessage(id, at, parts, index, {}, { type: "synthetic", text: contentText(entry.content) });
|
|
425
703
|
if (entry.customType === BACKGROUND_BASH_TYPE) backgroundBash(id, entryId, (entry as any).details, at, index);
|
|
426
704
|
return;
|
|
427
705
|
}
|
|
@@ -433,19 +711,20 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
433
711
|
case "user": {
|
|
434
712
|
const injected = options.injectedPrompts?.get(entryId);
|
|
435
713
|
const parts = contentParts(message.content, injected ? { synthetic: true, metadata: { ...injected } } : {});
|
|
436
|
-
|
|
714
|
+
const native = { type: injected ? "synthetic" : "user", text: contentText(message.content) };
|
|
715
|
+
if (parts.length > 0) turnVariant = userMessage(id, created, parts, index, {}, native);
|
|
437
716
|
return;
|
|
438
717
|
}
|
|
439
718
|
case "custom": {
|
|
440
719
|
const parts = contentParts(message.content, { synthetic: true, metadata: { customType: message.customType ?? null } });
|
|
441
|
-
if (parts.length > 0) userMessage(id, created, parts, index);
|
|
720
|
+
if (parts.length > 0) userMessage(id, created, parts, index, {}, { type: "synthetic", text: contentText(message.content) });
|
|
442
721
|
if (message.customType === BACKGROUND_BASH_TYPE) backgroundBash(id, entryId, message.details, created, index);
|
|
443
722
|
return;
|
|
444
723
|
}
|
|
445
724
|
case "branchSummary":
|
|
446
725
|
case "compactionSummary": {
|
|
447
726
|
if (typeof message.summary === "string" && message.summary) {
|
|
448
|
-
userMessage(id, created, [{ type: "text", text: message.summary, synthetic: true, metadata: { [message.role]: true } }], index);
|
|
727
|
+
userMessage(id, created, [{ type: "text", text: message.summary, synthetic: true, metadata: { [message.role]: true } }], index, {}, { type: "synthetic", text: message.summary });
|
|
449
728
|
}
|
|
450
729
|
return;
|
|
451
730
|
}
|
|
@@ -478,8 +757,10 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
478
757
|
},
|
|
479
758
|
},
|
|
480
759
|
];
|
|
481
|
-
|
|
482
|
-
|
|
760
|
+
const outputBytes = Buffer.byteLength(output);
|
|
761
|
+
emit({
|
|
762
|
+
// The 2.x shell record's fields on the reply, as the desktop keeps them (pi keeps no shell id).
|
|
763
|
+
info: mergeNative({
|
|
483
764
|
id: shellId,
|
|
484
765
|
sessionID: sessionId,
|
|
485
766
|
role: "assistant",
|
|
@@ -492,32 +773,54 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
492
773
|
cost: 0,
|
|
493
774
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
494
775
|
finish: "stop",
|
|
495
|
-
},
|
|
776
|
+
}, defined({
|
|
777
|
+
type: "shell",
|
|
778
|
+
command,
|
|
779
|
+
status: message.cancelled === true ? "killed" : "exited",
|
|
780
|
+
exit: typeof message.exitCode === "number" ? message.exitCode : undefined,
|
|
781
|
+
output: { output, cursor: outputBytes, size: outputBytes, truncated: message.truncated === true },
|
|
782
|
+
}), INFO_NESTED),
|
|
496
783
|
parts: parts.map((part, partIndex) => ({ id: partId(shellId, partIndex), sessionID: sessionId, messageID: shellId, ...part })),
|
|
497
784
|
});
|
|
498
785
|
return;
|
|
499
786
|
}
|
|
500
787
|
case "assistant": {
|
|
501
788
|
const completed = at ?? created;
|
|
789
|
+
const api = message.api;
|
|
790
|
+
const timing = typeof message.timestamp === "number" ? timings.get(message.timestamp) : undefined;
|
|
502
791
|
const parts: Array<Record<string, unknown>> = [{ type: "step-start" }];
|
|
792
|
+
// The 2.x content entry of each part (by part index), added once the parts have their ids.
|
|
793
|
+
const natives = new Map<number, Record<string, unknown>>();
|
|
503
794
|
const changed: string[] = [];
|
|
795
|
+
// When the step's tools settled (2.x time.completed): the last result, once every call has one.
|
|
796
|
+
let settled: number | null = completed;
|
|
797
|
+
let pending = false;
|
|
504
798
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
505
|
-
|
|
506
|
-
if (!isRecord(block))
|
|
799
|
+
content.forEach((block: unknown, blockIndex: number) => {
|
|
800
|
+
if (!isRecord(block)) return;
|
|
801
|
+
const blockTime = timing?.blocks.get(blockIndex);
|
|
507
802
|
if (block.type === "thinking") {
|
|
508
803
|
if (typeof block.thinking === "string" && block.thinking.trim()) {
|
|
509
804
|
parts.push({ type: "reasoning", text: block.thinking, ...(block.redacted === true ? { metadata: { redacted: true } } : {}) });
|
|
805
|
+
const time = blockTime && (blockTime.created !== undefined || blockTime.completed !== undefined) ? { ...blockTime } : undefined;
|
|
806
|
+
natives.set(parts.length - 1, defined({ type: "reasoning", text: block.thinking, state: reasoningState(block, api), time }));
|
|
510
807
|
}
|
|
511
808
|
} else if (block.type === "text") {
|
|
512
|
-
if (typeof block.text === "string" && block.text)
|
|
809
|
+
if (typeof block.text === "string" && block.text) {
|
|
810
|
+
parts.push({ type: "text", text: block.text });
|
|
811
|
+
natives.set(parts.length - 1, defined({ type: "text", text: block.text, state: textState(block, api) }));
|
|
812
|
+
}
|
|
513
813
|
} else if (block.type === "toolCall") {
|
|
514
814
|
const callId = typeof block.id === "string" ? block.id : "";
|
|
515
815
|
const input = isRecord(block.arguments) ? block.arguments : {};
|
|
516
816
|
const result = callId ? results.get(callId) : undefined;
|
|
517
817
|
const start = (callId ? options.toolStarts?.get(callId) : undefined) ?? completed ?? 0;
|
|
518
818
|
let state: Record<string, unknown>;
|
|
819
|
+
let nativeState: Record<string, unknown>;
|
|
519
820
|
if (!result) {
|
|
520
821
|
state = { status: "running", input, time: { start } };
|
|
822
|
+
nativeState = { status: "running", input };
|
|
823
|
+
pending = true;
|
|
521
824
|
} else {
|
|
522
825
|
const text = contentText(result.message.content);
|
|
523
826
|
const attachments = contentParts(result.message.content).filter((part) => part.type === "file");
|
|
@@ -540,6 +843,11 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
540
843
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
541
844
|
time,
|
|
542
845
|
};
|
|
846
|
+
const toolContent = nativeToolContent(result.message.content);
|
|
847
|
+
nativeState = result.message.isError === true
|
|
848
|
+
? { status: "error", input, error: { type: "tool.execution", message: text } }
|
|
849
|
+
: { status: "completed", input, ...(toolContent.length > 0 ? { content: toolContent } : {}) };
|
|
850
|
+
if (result.at !== null && (settled === null || result.at > settled)) settled = result.at;
|
|
543
851
|
const target = typeof input.path === "string" ? input.path : null;
|
|
544
852
|
if (result.message.isError !== true && FILE_TOOLS.has(String(block.name)) && target) {
|
|
545
853
|
const absolute = isAbsolute(target) ? target : resolve(cwd, target);
|
|
@@ -547,8 +855,21 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
547
855
|
}
|
|
548
856
|
}
|
|
549
857
|
parts.push({ type: "tool", callID: callId, tool: typeof block.name === "string" ? block.name : "tool", state });
|
|
858
|
+
const call = nativeCallId(callId, api);
|
|
859
|
+
const ran = (callId ? timing?.tools[callId] ?? options.toolStarts?.get(callId) : undefined) ?? undefined;
|
|
860
|
+
const time = defined({ created: blockTime?.created, ran, completed: result?.at ?? undefined });
|
|
861
|
+
natives.set(parts.length - 1, defined({
|
|
862
|
+
type: "tool",
|
|
863
|
+
id: call.id,
|
|
864
|
+
name: typeof block.name === "string" ? block.name : "tool",
|
|
865
|
+
// pi runs every tool itself, none provider-side.
|
|
866
|
+
executed: false,
|
|
867
|
+
providerState: call.itemId ? { itemId: call.itemId } : undefined,
|
|
868
|
+
state: nativeState,
|
|
869
|
+
time: Object.keys(time).length > 0 ? time : undefined,
|
|
870
|
+
}));
|
|
550
871
|
}
|
|
551
|
-
}
|
|
872
|
+
});
|
|
552
873
|
if (changed.length > 0) parts.push({ type: "patch", files: changed });
|
|
553
874
|
const finish = finishReason(message.stopReason);
|
|
554
875
|
const tokens = stepTokens(message.usage);
|
|
@@ -559,29 +880,59 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
559
880
|
}
|
|
560
881
|
const error = assistantError(message);
|
|
561
882
|
const effort = typeof message.providerThinkingLevel === "string" && message.providerThinkingLevel ? message.providerThinkingLevel : turnVariant ?? variant();
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
883
|
+
const stepParts = parts.map((part, partIndex) => ({ id: partId(id, partIndex), sessionID: sessionId, messageID: id, ...part }));
|
|
884
|
+
// The 2.x content entries' own fields on their parts (engine2/native.ts enrichPart).
|
|
885
|
+
for (const [partIndex, native] of natives) {
|
|
886
|
+
const part = stepParts[partIndex]!;
|
|
887
|
+
if (part.type !== "tool") {
|
|
888
|
+
mergeNative(part, native, { time: {} });
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const { id: nativeId, state: nativeState, ...rest } = native;
|
|
892
|
+
mergeNative(part, rest);
|
|
893
|
+
if (nativeId !== part.callID) mergeNative(part, { id: nativeId });
|
|
894
|
+
if (isRecord(part.state) && isRecord(nativeState)) part.state = mergeNative({ ...part.state }, nativeState, STATE_NESTED);
|
|
895
|
+
}
|
|
896
|
+
const streamed = timing?.streamed ?? at ?? undefined;
|
|
897
|
+
const relativeChanged = changed
|
|
898
|
+
.map((file) => relative(cwd, file))
|
|
899
|
+
.filter((file) => file && !file.startsWith("..") && !isAbsolute(file));
|
|
900
|
+
const info = mergeNative({
|
|
901
|
+
id,
|
|
902
|
+
sessionID: sessionId,
|
|
903
|
+
role: "assistant",
|
|
904
|
+
...(lastUserId ? { parentID: lastUserId } : {}),
|
|
905
|
+
time: { created: created ?? 0, completed: completed ?? 0 },
|
|
906
|
+
...(typeof message.model === "string" ? { modelID: message.model } : {}),
|
|
907
|
+
...(typeof message.provider === "string" ? { providerID: message.provider } : {}),
|
|
908
|
+
...(typeof message.responseModel === "string" && message.responseModel && message.responseModel !== message.model
|
|
909
|
+
? { responseModelID: message.responseModel }
|
|
910
|
+
: {}),
|
|
911
|
+
agent,
|
|
912
|
+
mode: agent,
|
|
913
|
+
...(effort ? { variant: effort } : {}),
|
|
914
|
+
path: { cwd, root: cwd },
|
|
915
|
+
cost,
|
|
916
|
+
tokens,
|
|
917
|
+
...(finish ? { finish } : {}),
|
|
918
|
+
...(error ? { error } : {}),
|
|
919
|
+
}, defined({
|
|
920
|
+
// The 2.x assistant record's own fields (engine2/native.ts enrichAssistant).
|
|
921
|
+
type: "assistant",
|
|
922
|
+
agent,
|
|
923
|
+
model: typeof message.model === "string" && typeof message.provider === "string"
|
|
924
|
+
? { id: message.model, providerID: message.provider, ...(effort ? { variant: effort } : {}) }
|
|
925
|
+
: undefined,
|
|
926
|
+
snapshot: relativeChanged.length > 0 ? { files: relativeChanged } : undefined,
|
|
927
|
+
finish,
|
|
928
|
+
rawFinish: nativeRawFinish(message),
|
|
929
|
+
providerState: responsesApi(api) && typeof message.responseId === "string" && message.responseId ? { responseId: message.responseId } : undefined,
|
|
930
|
+
cost,
|
|
931
|
+
tokens: nativeTokens(message.usage),
|
|
932
|
+
error: nativeError(message),
|
|
933
|
+
time: defined({ created: created ?? 0, streamed, completed: pending ? undefined : settled ?? undefined }),
|
|
934
|
+
}), INFO_NESTED);
|
|
935
|
+
emit({ info, parts: stepParts });
|
|
585
936
|
return;
|
|
586
937
|
}
|
|
587
938
|
default:
|
|
@@ -590,6 +941,9 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
590
941
|
return;
|
|
591
942
|
}
|
|
592
943
|
});
|
|
944
|
+
// Changes after the last message: at the end of the last message that takes them.
|
|
945
|
+
settleSwitch();
|
|
946
|
+
for (const record of pendingRecords.splice(0)) appendRecord(record);
|
|
593
947
|
return out;
|
|
594
948
|
}
|
|
595
949
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Environment passed to user-controlled tools and MCP servers. */
|
|
2
|
+
const SECRET_ENV_NAMES = new Set([
|
|
3
|
+
"OMNIRUSH_TOKEN",
|
|
4
|
+
"OMNIRUSH_ACCESS_TOKEN",
|
|
5
|
+
"OMNIRUSH_APPROVAL_TOKEN",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
/** Keep provider credentials in the agent process, never in shell/MCP children. */
|
|
9
|
+
export function sanitizeToolEnvironment(input: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
10
|
+
const output: NodeJS.ProcessEnv = {};
|
|
11
|
+
for (const [key, value] of Object.entries(input)) {
|
|
12
|
+
if (!SECRET_ENV_NAMES.has(key) && value !== undefined) output[key] = value;
|
|
13
|
+
}
|
|
14
|
+
return output;
|
|
15
|
+
}
|