@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.
- package/CHANGELOG.md +169 -26
- package/THIRD-PARTY-NOTICES.txt +54 -25
- package/dist/types/agent.d.ts +17 -4
- package/dist/types/append-only-context.d.ts +0 -4
- package/dist/types/compaction/anthropic.d.ts +108 -0
- package/dist/types/compaction/compaction-v2-streaming.d.ts +2 -2
- package/dist/types/compaction/compaction.d.ts +33 -18
- package/dist/types/compaction/entries.d.ts +2 -0
- package/dist/types/compaction/index.d.ts +1 -0
- package/dist/types/compaction/openai.d.ts +9 -8
- package/dist/types/index.d.ts +1 -0
- package/dist/types/speculative-execution.d.ts +63 -0
- package/dist/types/thinking.d.ts +1 -1
- package/dist/types/types.d.ts +212 -3
- package/package.json +10 -10
- package/src/agent-loop.ts +582 -86
- package/src/agent.ts +183 -38
- package/src/append-only-context.ts +122 -1
- package/src/compaction/anthropic.ts +310 -0
- package/src/compaction/compaction-v2-streaming.ts +4 -3
- package/src/compaction/compaction.ts +299 -131
- package/src/compaction/entries.ts +2 -0
- package/src/compaction/index.ts +1 -0
- package/src/compaction/openai.ts +17 -10
- package/src/compaction/prompts/anthropic-compaction-instructions.md +17 -0
- package/src/index.ts +2 -0
- package/src/proxy.ts +49 -8
- package/src/speculative-execution.ts +953 -0
- package/src/telemetry.ts +1 -0
- package/src/thinking.ts +1 -1
- package/src/tokenizer.ts +11 -2
- package/src/types.ts +232 -4
package/src/compaction/openai.ts
CHANGED
|
@@ -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
|
|
70
|
-
* stream (guarded by first-event/idle watchdogs in pi-ai), these are
|
|
71
|
-
* fetches awaiting one non-streamed JSON body — a connection silently
|
|
72
|
-
* by a middlebox would otherwise hang the whole compaction pipeline
|
|
73
|
-
* (frozen "Auto context-full maintenance…" spinner, manual /compact
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|