@linxiraos/pi-agent-core 1.1.14 → 1.1.16

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.
@@ -66,14 +66,14 @@ export * from "./compaction-v2-streaming";
66
66
  export const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
67
67
 
68
68
  /**
69
- * Hard ceiling on remote compaction HTTP requests. Unlike every provider
70
- * stream (guarded by first-event/idle watchdogs in pi-ai), these are raw
71
- * fetches awaiting one non-streamed JSON body — a connection silently dropped
72
- * by a middlebox would otherwise hang the whole compaction pipeline forever
73
- * (frozen "Auto context-full maintenance…" spinner, manual /compact queueing
74
- * behind it). On timeout the caller falls back to local summarization.
69
+ * Hard ceiling on remote compaction HTTP requests (5 minutes). Unlike every
70
+ * provider stream (guarded by first-event/idle watchdogs in pi-ai), these are
71
+ * raw fetches awaiting one non-streamed JSON body — a connection silently
72
+ * dropped by a middlebox would otherwise hang the whole compaction pipeline
73
+ * forever (frozen "Auto context-full maintenance…" spinner, manual /compact
74
+ * queueing behind it). On timeout the caller falls back to local summarization.
75
75
  */
76
- export const REMOTE_COMPACTION_TIMEOUT_MS = 180_000;
76
+ export const REMOTE_COMPACTION_TIMEOUT_MS = 300_000;
77
77
 
78
78
  const DEFAULT_AZURE_API_VERSION = "v1";
79
79
 
@@ -282,15 +282,22 @@ export interface RemoteCompactionResponse {
282
282
  // OpenAI provider gating + endpoint resolution
283
283
  // ============================================================================
284
284
 
285
- function isOpenAiRemoteCompactionApi(api: Api | undefined): boolean {
285
+ export function isOpenAiRemoteCompactionApi(api: Api | undefined): boolean {
286
286
  return api === "openai-responses" || api === "azure-openai-responses" || api === "openai-codex-responses";
287
287
  }
288
288
 
289
289
  export function shouldUseOpenAiRemoteCompaction(model: Model): boolean {
290
290
  if (model.remoteCompaction?.enabled === false) return false;
291
- if (model.provider === "openai" || model.provider === "openai-codex") return true;
291
+ const compactionApi = model.remoteCompaction?.api ?? model.api;
292
+ // ChatGPT's Codex backend exposes V2 compaction on /codex/responses, but
293
+ // does not expose the OpenAI V1 /responses/compact endpoint. Only use the
294
+ // V1 path for Codex when an explicit compatible endpoint was configured.
295
+ if (model.provider === "openai-codex") {
296
+ return (model.remoteCompaction?.endpoint?.trim().length ?? 0) > 0;
297
+ }
298
+ if (model.provider === "openai") return true;
292
299
  if (model.remoteCompaction?.enabled !== true) return false;
293
- return isOpenAiRemoteCompactionApi(model.remoteCompaction.api ?? model.api);
300
+ return isOpenAiRemoteCompactionApi(compactionApi);
294
301
  }
295
302
 
296
303
  function resolveOpenAiCompactEndpoint(model: Model): string {
@@ -0,0 +1,17 @@
1
+ {{#if retainedTail}}
2
+ SCOPE: The conversation's final {{#when retainedTail.count "==" 1}}{{retainedTail.role}} message stays{{else}}{{retainedTail.count}} messages, starting with a {{retainedTail.role}} message, stay{{/when}} in context verbatim after your summary. Summarize ONLY the history before those messages. You MUST NOT restate anything from those final messages — the reader sees them right after the summary — and you MUST treat them as the most recent state when describing progress and next steps.
3
+ {{else}}
4
+ SCOPE: The conversation above is the transcript to summarize. The API replaces everything before your summary with it, so nothing you leave out survives into the next context window.
5
+ {{/if}}
6
+
7
+ {{#if extraContext}}
8
+ {{extraContext}}
9
+
10
+ {{/if}}
11
+ {{basePrompt}}
12
+ {{#if customInstructions}}
13
+
14
+ Additional focus: {{customInstructions}}
15
+ {{/if}}
16
+
17
+ You MUST NOT call any tools while writing the summary; respond with the summary text only.
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export * from "./proxy";
14
14
  export * from "./replay-policy";
15
15
  // Run-level telemetry collector + aggregators
16
16
  export * from "./run-collector";
17
+ // Speculative execution coordinator
18
+ export * from "./speculative-execution";
17
19
  // Telemetry
18
20
  export * from "./telemetry";
19
21
  // Thinking selectors
package/src/proxy.ts CHANGED
@@ -20,8 +20,7 @@ import {
20
20
  type StreamingPartialJsonCarrier,
21
21
  setStreamingPartialJson,
22
22
  } from "@linxiraos/pi-ai/utils/block-symbols";
23
- import { calculateCost } from "@linxiraos/pi-catalog/models";
24
- import { parseStreamingJson, readSseJson } from "@linxiraos/pi-utils";
23
+ import { parseStreamingJson, parseStreamingJsonThrottled, readSseJson } from "@linxiraos/pi-utils";
25
24
 
26
25
  // Event stream adapter for proxy SSE events
27
26
  export class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
@@ -118,6 +117,10 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
118
117
  };
119
118
 
120
119
  let response: Response | null = null;
120
+ // Declared outside try so the disconnect path (catch) can finalize
121
+ // still-buffered tool-call arguments before emitting the error event.
122
+ const partialJsonByIndex = new Map<number, string>();
123
+ const parsedLenByIndex = new Map<number, number>();
121
124
  const abortHandler = () => {
122
125
  const body = response?.body;
123
126
  if (body) {
@@ -166,12 +169,11 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
166
169
  }
167
170
 
168
171
  let sawTerminalEvent = false;
169
- const partialJsonByIndex = new Map<number, string>();
170
172
  for await (const event of readSseJson<ProxyAssistantMessageEvent>(
171
173
  response.body as ReadableStream<Uint8Array>,
172
174
  options.signal,
173
175
  )) {
174
- const parsedEvent = processProxyEvent(model, event, partial, partialJsonByIndex);
176
+ const parsedEvent = processProxyEvent(event, partial, partialJsonByIndex, parsedLenByIndex);
175
177
  if (parsedEvent) {
176
178
  if (parsedEvent.type === "done" || parsedEvent.type === "error") {
177
179
  sawTerminalEvent = true;
@@ -194,6 +196,10 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
194
196
  const reason = options.signal?.aborted ? "aborted" : "error";
195
197
  partial.stopReason = reason;
196
198
  partial.errorMessage = errorMessage;
199
+ // Disconnect/abort path: no terminal event ran, so trailing
200
+ // throttled deltas never reached content.arguments. Finalize
201
+ // them here so the persisted/retried transcript is complete.
202
+ finalizeBufferedArguments(partial, partialJsonByIndex);
197
203
  scrubPartialJson(partial);
198
204
  stream.push({
199
205
  type: "error",
@@ -233,10 +239,10 @@ function scrubPartialJson(partial: AssistantMessage): void {
233
239
  * reads as still-streaming.
234
240
  */
235
241
  function processProxyEvent(
236
- model: Model,
237
242
  proxyEvent: ProxyAssistantMessageEvent,
238
243
  partial: AssistantMessage,
239
244
  partialJsonByIndex: Map<number, string>,
245
+ parsedLenByIndex: Map<number, number>,
240
246
  ): AssistantMessageEvent | undefined {
241
247
  switch (proxyEvent.type) {
242
248
  case "start":
@@ -337,13 +343,22 @@ function processProxyEvent(
337
343
  [kStreamingPartialJson]: "",
338
344
  } as ToolCall & StreamingPartialJsonCarrier;
339
345
  partialJsonByIndex.set(proxyEvent.contentIndex, "");
346
+ parsedLenByIndex.set(proxyEvent.contentIndex, 0);
340
347
  return { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
341
348
  case "toolcall_delta": {
342
349
  const content = partial.content[proxyEvent.contentIndex];
343
350
  if (content?.type === "toolCall") {
344
351
  const acc = (partialJsonByIndex.get(proxyEvent.contentIndex) ?? "") + proxyEvent.delta;
345
352
  partialJsonByIndex.set(proxyEvent.contentIndex, acc);
346
- content.arguments = parseStreamingJson(acc) || {};
353
+ // Geometric throttle (same contract as native providers): the
354
+ // authoritative parse lands at toolcall_end; mid-stream
355
+ // previews refresh at most ~3% late on large buffers.
356
+ const lastLen = parsedLenByIndex.get(proxyEvent.contentIndex) ?? 0;
357
+ const parsed = parseStreamingJsonThrottled(acc, lastLen);
358
+ if (parsed !== null) {
359
+ content.arguments = parsed.value || {};
360
+ parsedLenByIndex.set(proxyEvent.contentIndex, parsed.parsedLen);
361
+ }
347
362
  setStreamingPartialJson(content, acc);
348
363
  partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
349
364
  return {
@@ -359,7 +374,13 @@ function processProxyEvent(
359
374
  case "toolcall_end": {
360
375
  const content = partial.content[proxyEvent.contentIndex];
361
376
  if (content?.type === "toolCall") {
377
+ // Authoritative final parse (mirrors native providers): the
378
+ // throttle may have skipped trailing deltas, so the last
379
+ // parsed arguments can lag the accumulated buffer.
380
+ const acc = partialJsonByIndex.get(proxyEvent.contentIndex);
381
+ if (acc !== undefined && acc.length > 0) content.arguments = parseStreamingJson(acc) || {};
362
382
  partialJsonByIndex.delete(proxyEvent.contentIndex);
383
+ parsedLenByIndex.delete(proxyEvent.contentIndex);
363
384
  clearStreamingPartialJson(content);
364
385
  return {
365
386
  type: "toolcall_end",
@@ -375,8 +396,10 @@ function processProxyEvent(
375
396
  partial.stopReason = proxyEvent.reason;
376
397
  partial.usage = proxyEvent.usage;
377
398
  if (proxyEvent.content !== undefined) partial.content = proxyEvent.content;
378
- calculateCost(model, partial.usage);
399
+ else finalizeBufferedArguments(partial, partialJsonByIndex);
379
400
  scrubPartialJson(partial);
401
+ partialJsonByIndex.clear();
402
+ parsedLenByIndex.clear();
380
403
  return { type: "done", reason: proxyEvent.reason, message: partial };
381
404
 
382
405
  case "error":
@@ -384,8 +407,26 @@ function processProxyEvent(
384
407
  partial.errorMessage = proxyEvent.errorMessage;
385
408
  partial.usage = proxyEvent.usage;
386
409
  if (proxyEvent.content !== undefined) partial.content = proxyEvent.content;
387
- calculateCost(model, partial.usage);
410
+ else finalizeBufferedArguments(partial, partialJsonByIndex);
388
411
  scrubPartialJson(partial);
412
+ partialJsonByIndex.clear();
413
+ parsedLenByIndex.clear();
389
414
  return { type: "error", reason: proxyEvent.reason, error: partial };
390
415
  }
391
416
  }
417
+
418
+ /**
419
+ * Authoritatively parse every still-buffered tool-call argument accumulator
420
+ * into its content block. Terminal `done`/`error` events without a preceding
421
+ * `toolcall_end` (and with `content` omitted) finalize the partial message as
422
+ *-is; without this, trailing deltas below the throttle gate never reach
423
+ * `content.arguments` and the finalized call carries stale arguments.
424
+ */
425
+ function finalizeBufferedArguments(partial: AssistantMessage, partialJsonByIndex: Map<number, string>): void {
426
+ if (partialJsonByIndex.size === 0) return;
427
+ for (const [index, acc] of partialJsonByIndex) {
428
+ if (acc.length === 0) continue;
429
+ const content = partial.content[index];
430
+ if (content?.type === "toolCall") content.arguments = parseStreamingJson(acc) || {};
431
+ }
432
+ }