@linxiraos/pi-agent-core 1.1.8 → 1.1.9

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +4 -6
  2. package/package.json +73 -79
  3. package/src/agent-loop.ts +80 -15
  4. package/src/compaction/compaction-v2-streaming.ts +43 -32
  5. package/src/compaction/compaction.ts +88 -17
  6. package/src/compaction/openai.ts +3 -0
  7. package/src/telemetry.ts +2 -2
  8. package/src/types.ts +34 -7
  9. package/THIRD-PARTY-NOTICES.txt +0 -22909
  10. package/dist/types/agent-loop.d.ts +0 -150
  11. package/dist/types/agent.d.ts +0 -472
  12. package/dist/types/append-only-context.d.ts +0 -131
  13. package/dist/types/compaction/branch-summarization.d.ts +0 -102
  14. package/dist/types/compaction/compaction-v2-streaming.d.ts +0 -85
  15. package/dist/types/compaction/compaction.d.ts +0 -341
  16. package/dist/types/compaction/entries.d.ts +0 -120
  17. package/dist/types/compaction/errors.d.ts +0 -38
  18. package/dist/types/compaction/index.d.ts +0 -14
  19. package/dist/types/compaction/message-cache.d.ts +0 -25
  20. package/dist/types/compaction/messages.d.ts +0 -103
  21. package/dist/types/compaction/openai.d.ts +0 -118
  22. package/dist/types/compaction/pruning.d.ts +0 -106
  23. package/dist/types/compaction/shake.d.ts +0 -104
  24. package/dist/types/compaction/tool-protection.d.ts +0 -19
  25. package/dist/types/compaction/transcript-tokens.d.ts +0 -76
  26. package/dist/types/compaction/utils.d.ts +0 -63
  27. package/dist/types/compaction.d.ts +0 -1
  28. package/dist/types/index.d.ts +0 -13
  29. package/dist/types/pause.d.ts +0 -46
  30. package/dist/types/proxy.d.ts +0 -91
  31. package/dist/types/replay-policy.d.ts +0 -5
  32. package/dist/types/run-collector.d.ts +0 -196
  33. package/dist/types/telemetry.d.ts +0 -625
  34. package/dist/types/thinking.d.ts +0 -18
  35. package/dist/types/tokenizer.d.ts +0 -78
  36. package/dist/types/types.d.ts +0 -786
  37. package/dist/types/utils/yield.d.ts +0 -71
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.1.9] - 2026-09-05
6
+
7
+ - v18.1.10 sync baseline (compaction and tool-arg stream updates).
8
+
5
9
  ## [1.1.8] - 2026-09-04
6
10
 
7
11
  - OMP sync v18.1.2–v18.1.5: Agent Hub activity stream groundwork, declarative provider auth registry, and tool-roster notice plumbing shared with the session layer.
@@ -20,12 +24,6 @@
20
24
 
21
25
  - 同步上游 OMP v18.0.5 / v18.0.6(内部运行时与构建改进)。
22
26
 
23
- ## [1.1.1] - 2026-08-25
24
-
25
- ### Fixed
26
-
27
- - Published tarballs now carry real dependency versions instead of Bun's `catalog:` protocol (1.1.0 installs failed with "Unsupported URL Type catalog:").
28
-
29
27
  ## [1.1.0] - 2026-08-25
30
28
 
31
29
  ### Changed
package/package.json CHANGED
@@ -1,81 +1,75 @@
1
1
  {
2
- "type": "module",
3
- "name": "@linxiraos/pi-agent-core",
4
- "version": "1.1.8",
5
- "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
- "homepage": "https://linxira-os.github.io/zeta/",
7
- "author": "Can Boluk",
8
- "contributors": [
9
- "Mario Zechner"
10
- ],
11
- "license": "MIT",
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
15
- "directory": "packages/agent"
16
- },
17
- "bugs": {
18
- "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
19
- },
20
- "keywords": [
21
- "ai",
22
- "agent",
23
- "llm",
24
- "transport",
25
- "state-management"
26
- ],
27
- "main": "./src/index.ts",
28
- "types": "./dist/types/index.d.ts",
29
- "scripts": {
30
- "check": "biome check . && bun run check:types",
31
- "check:types": "tsgo -p tsconfig.json --noEmit",
32
- "lint": "biome lint .",
33
- "test": "bun test --parallel",
34
- "fix": "biome check --write --unsafe .",
35
- "fmt": "biome format --write ."
36
- },
37
- "dependencies": {
38
- "@linxiraos/pi-ai": "1.1.8",
39
- "@linxiraos/pi-catalog": "1.1.8",
40
- "@linxiraos/pi-natives": "1.1.8",
41
- "@linxiraos/pi-utils": "1.1.8",
42
- "@linxiraos/pi-wire": "1.1.8",
43
- "@linxiraos/pi-snapcompact": "1.1.8",
44
- "@opentelemetry/api": "^1.9.1"
45
- },
46
- "devDependencies": {
47
- "@linxiraos/pi-omptype": "1.1.8",
48
- "@opentelemetry/context-async-hooks": "^2.9.0",
49
- "@opentelemetry/sdk-trace-base": "^2.9.0",
50
- "@types/bun": "^1.3.14"
51
- },
52
- "engines": {
53
- "bun": ">=1.3.14"
54
- },
55
- "files": [
56
- "src",
57
- "README.md",
58
- "CHANGELOG.md",
59
- "LICENSE",
60
- "THIRD-PARTY-NOTICES.txt",
61
- "dist/types"
62
- ],
63
- "exports": {
64
- ".": {
65
- "types": "./dist/types/index.d.ts",
66
- "import": "./src/index.ts"
67
- },
68
- "./compaction": {
69
- "types": "./dist/types/compaction.d.ts",
70
- "import": "./src/compaction.ts"
71
- },
72
- "./compaction/*": {
73
- "types": "./dist/types/compaction/*.d.ts",
74
- "import": "./src/compaction/*.ts"
75
- },
76
- "./*": {
77
- "types": "./dist/types/*.d.ts",
78
- "import": "./src/*.ts"
79
- }
80
- }
2
+ "exports": {
3
+ "./*": {
4
+ "import": "./src/*.ts",
5
+ "types": "./src/*.ts"
6
+ },
7
+ "./compaction/*": {
8
+ "import": "./src/compaction/*.ts",
9
+ "types": "./src/compaction/*.ts"
10
+ },
11
+ "./compaction": {
12
+ "import": "./src/compaction.ts",
13
+ "types": "./src/compaction.ts"
14
+ },
15
+ ".": {
16
+ "import": "./src/index.ts",
17
+ "types": "./src/index.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "src",
22
+ "README.md",
23
+ "CHANGELOG.md"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.3.14"
27
+ },
28
+ "devDependencies": {
29
+ "@linxiraos/pi-omptype": "1.1.9",
30
+ "@types/bun": "^1.3.14",
31
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
32
+ "@opentelemetry/context-async-hooks": "^2.9.0"
33
+ },
34
+ "dependencies": {
35
+ "@linxiraos/pi-snapcompact": "1.1.9",
36
+ "@linxiraos/pi-wire": "1.1.9",
37
+ "@linxiraos/pi-utils": "1.1.9",
38
+ "@linxiraos/pi-natives": "1.1.9",
39
+ "@linxiraos/pi-catalog": "1.1.9",
40
+ "@linxiraos/pi-ai": "1.1.9",
41
+ "@opentelemetry/api": "^1.9.1"
42
+ },
43
+ "scripts": {
44
+ "test": "bun test --parallel",
45
+ "check:types": "tsgo -p tsconfig.json --noEmit",
46
+ "check": "tsgo -p tsconfig.json --noEmit"
47
+ },
48
+ "types": "./src/index.ts",
49
+ "main": "./src/index.ts",
50
+ "keywords": [
51
+ "ai",
52
+ "agent",
53
+ "llm",
54
+ "transport",
55
+ "state-management"
56
+ ],
57
+ "bugs": {
58
+ "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
59
+ },
60
+ "repository": {
61
+ "directory": "packages/agent",
62
+ "url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
63
+ "type": "git"
64
+ },
65
+ "license": "MIT",
66
+ "contributors": [
67
+ "Mario Zechner"
68
+ ],
69
+ "author": "Can Boluk",
70
+ "homepage": "https://linxira-os.github.io/zeta/",
71
+ "description": "General-purpose agent with transport abstraction, state management, and attachment support",
72
+ "version": "1.1.9",
73
+ "name": "@linxiraos/pi-agent-core",
74
+ "type": "module"
81
75
  }
package/src/agent-loop.ts CHANGED
@@ -73,6 +73,7 @@ import type {
73
73
  AgentMessage,
74
74
  AgentPreModelCallResult,
75
75
  AgentTool,
76
+ AgentToolArgStream,
76
77
  AgentToolCall,
77
78
  AgentToolResult,
78
79
  AgentTurnEndContext,
@@ -924,7 +925,10 @@ function extractIntent(args: Record<string, unknown>): { intent?: string; stripp
924
925
  if (typeof intent !== "string") {
925
926
  return { strippedArgs };
926
927
  }
927
- const trimmed = intent.trim();
928
+ const trimmed = intent
929
+ .trim()
930
+ .replace(/\s*\.+$/, "")
931
+ .trim();
928
932
  return { intent: trimmed.length > 0 ? trimmed : undefined, strippedArgs };
929
933
  }
930
934
 
@@ -1775,6 +1779,17 @@ async function streamAssistantResponse(
1775
1779
  let partialMessage: AssistantMessage | null = null;
1776
1780
  let addedPartial = false;
1777
1781
  const completedToolCallIds = new Set<string>();
1782
+ const argStreams = new Map<number, { id: string; stream: AgentToolArgStream }>();
1783
+ const cancelArgStreams = (): void => {
1784
+ for (const { id, stream: argStream } of argStreams.values()) {
1785
+ try {
1786
+ argStream.cancel();
1787
+ } catch (error) {
1788
+ logger.debug("Tool argument stream cancel failed", { toolCallId: id, error });
1789
+ }
1790
+ }
1791
+ argStreams.clear();
1792
+ };
1778
1793
 
1779
1794
  const responseIterator = response[Symbol.asyncIterator]();
1780
1795
  const finishAbortedStream = async (): Promise<AssistantMessage> => {
@@ -1888,6 +1903,52 @@ async function streamAssistantResponse(
1888
1903
  // when the LLM is streaming chunks faster than the loop can rest.
1889
1904
  await yieldIfDue();
1890
1905
 
1906
+ if (event.type === "toolcall_start") {
1907
+ const block = event.partial.content[event.contentIndex];
1908
+ if (block?.type === "toolCall") {
1909
+ const tool = resolveToolForCall(context.tools, block, config.resolveFallbackTool);
1910
+ if (tool?.openArgStream) {
1911
+ try {
1912
+ const argStream = tool.openArgStream({
1913
+ toolCallId: block.id,
1914
+ toolName: block.name,
1915
+ customWireName: block.customWireName,
1916
+ emit: update =>
1917
+ stream.push({
1918
+ type: "tool_stream_update",
1919
+ toolCallId: block.id,
1920
+ toolName: block.name,
1921
+ update,
1922
+ }),
1923
+ });
1924
+ if (argStream) argStreams.set(event.contentIndex, { id: block.id, stream: argStream });
1925
+ } catch (error) {
1926
+ logger.debug("Tool argument stream open failed", { toolCallId: block.id, error });
1927
+ }
1928
+ }
1929
+ }
1930
+ } else if (event.type === "toolcall_delta") {
1931
+ const entry = argStreams.get(event.contentIndex);
1932
+ if (entry) {
1933
+ try {
1934
+ entry.stream.push(event.delta);
1935
+ } catch (error) {
1936
+ logger.debug("Tool argument stream push failed", { toolCallId: entry.id, error });
1937
+ }
1938
+ }
1939
+ } else if (event.type === "toolcall_end") {
1940
+ const entry = argStreams.get(event.contentIndex);
1941
+ if (entry) {
1942
+ try {
1943
+ entry.stream.end(event.toolCall.arguments);
1944
+ } catch (error) {
1945
+ logger.debug("Tool argument stream end failed", { toolCallId: entry.id, error });
1946
+ } finally {
1947
+ argStreams.delete(event.contentIndex);
1948
+ }
1949
+ }
1950
+ }
1951
+
1891
1952
  switch (event.type) {
1892
1953
  case "start":
1893
1954
  partialMessage = event.partial;
@@ -1944,6 +2005,7 @@ async function streamAssistantResponse(
1944
2005
  }
1945
2006
  } finally {
1946
2007
  detachAbortListener?.();
2008
+ cancelArgStreams();
1947
2009
  }
1948
2010
 
1949
2011
  let trailing = await response.result();
@@ -2440,8 +2502,9 @@ async function executeToolCalls(
2440
2502
  // Queued steering hard-aborts only interruptible waits and raises the
2441
2503
  // cooperative soft signal for everything else: the boundary dequeue
2442
2504
  // below injects the message as soon as running tools finish (or
2443
- // background themselves), and not-yet-started tools are skipped.
2444
- // Idempotent — a second steer poll after the abort is a no-op.
2505
+ // background themselves), and not-yet-started interruptible waits
2506
+ // are skipped. Idempotent — a second steer poll after the abort is
2507
+ // a no-op.
2445
2508
  if (!steeringAbortController.signal.aborted) {
2446
2509
  interruptState.triggered = true;
2447
2510
  interruptState.source = steeringSource ?? "unknown";
@@ -2495,16 +2558,18 @@ async function executeToolCalls(
2495
2558
  };
2496
2559
 
2497
2560
  const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
2498
- // A pending interrupt preempts not-yet-started tools so the message
2499
- // injects promptly. A peer-IRC interrupt is the exception: it aborts
2500
- // interruptible waits only and leaves non-interruptible foreground work
2501
- // untouched (see the emit branch below and the `does not abort a
2502
- // non-interruptible foreground tool` case). That guarantee must hold for
2503
- // work still queued behind the aborted wait too — otherwise a batched
2504
- // `todo`/`write` gets dropped as "Skipped due to pending peer interrupt"
2505
- // purely for being ordered after the wait (#7493). User/system steering
2506
- // still preempts everything queued.
2507
- if (interruptState.triggered && (record.interruptible || interruptState.source !== "irc")) {
2561
+ // A pending interrupt preempts not-yet-started *interruptible* waits so
2562
+ // the message injects promptly instead of sitting out a `hub wait`.
2563
+ // Non-interruptible work is never skipped, whatever the source: the
2564
+ // expensive part — generating the call — is already paid, the tool
2565
+ // itself is cheap, and a skip only makes the model re-emit the same
2566
+ // call after the steer lands (#10439). The same guarantee is what keeps
2567
+ // a batched `todo`/`write` queued behind an aborted wait alive (#7493)
2568
+ // and lets a subagent's already-emitted terminal `yield` commit when
2569
+ // the parent steers mid-stream (#10645). The steer still injects at the
2570
+ // batch boundary; the cooperative soft signal lets long-running tools
2571
+ // step aside on their own.
2572
+ if (interruptState.triggered && record.interruptible) {
2508
2573
  // Skip both span emission and the collector orphan record here. The
2509
2574
  // tail sweep below (after `Promise.allSettled`) is the single path
2510
2575
  // that handles "no result message was produced" — it calls
@@ -2719,8 +2784,8 @@ async function executeToolCalls(
2719
2784
 
2720
2785
  // While tool calls are in flight, queued steering or interrupting IRC would
2721
2786
  // otherwise wait out the tools' own window. Poll only non-consuming queues:
2722
- // detection hard-aborts interruptible waits, soft-signals cooperative tools
2723
- // (auto-background bash), and skips not-yet-started tools, so the boundary
2787
+ // detection hard-aborts interruptible waits (running or not yet started)
2788
+ // and soft-signals cooperative tools (auto-background bash), so the boundary
2724
2789
  // dequeue below injects the message promptly. Gated on immediate-interrupt
2725
2790
  // mode; checkSteering is idempotent (no-op once triggered).
2726
2791
  const watchSteeringWhileRunning =
@@ -27,6 +27,7 @@ import { captureOpenAIHttpError } from "@linxiraos/pi-ai/utils/openai-http";
27
27
  import {
28
28
  applyCodexResidencyHeader,
29
29
  CODEX_BASE_URL,
30
+ codexRoutingHint,
30
31
  getCodexAccountId,
31
32
  OPENAI_HEADER_VALUES,
32
33
  OPENAI_HEADERS,
@@ -70,15 +71,11 @@ export interface CompactionV2Usage {
70
71
  reasoningOutputTokens?: number;
71
72
  }
72
73
 
73
- /** Request body fields needed for Responses-stream V2 compaction. */
74
+ /** Provider-ready Responses body and local state needed for V2 compaction. */
74
75
  export interface CompactionV2Request {
75
- model: string;
76
+ body: OpenAICodexCompactionBody;
76
77
  input: unknown[];
77
- instructions: string;
78
78
  retainedMessageBudget: number;
79
- tools?: unknown[];
80
- /** Responses reasoning param (effort + summary), matching a normal turn; omitted for non-reasoning models. */
81
- reasoning?: { effort: string; summary: string };
82
79
  sessionId?: string;
83
80
  promptCacheKey?: string;
84
81
  }
@@ -204,13 +201,44 @@ export function buildCompactionV2Request(
204
201
  retainedMessageBudget?: number;
205
202
  },
206
203
  ): CompactionV2Request {
207
- return {
204
+ const cacheOptions = { sessionId: options?.sessionId, promptCacheKey: options?.promptCacheKey };
205
+ const promptCacheKey = getOpenAIPromptCacheKey(cacheOptions);
206
+ const body: OpenAICodexCompactionBody = {
208
207
  model: resolveCompactionV2Model(model),
209
208
  input,
210
209
  instructions,
210
+ stream: true,
211
+ store: false,
212
+ ...(options?.reasoning || model.useResponsesLite
213
+ ? {
214
+ reasoning: model.useResponsesLite ? { ...options?.reasoning, context: "all_turns" } : options?.reasoning,
215
+ include: ["reasoning.encrypted_content"],
216
+ }
217
+ : {}),
218
+ ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
219
+ ...(options?.tools && options.tools.length > 0 ? { tools: options.tools, tool_choice: "auto" } : {}),
220
+ };
221
+ if (model.useResponsesLite) {
222
+ applyCodexResponsesLiteShape(body);
223
+ }
224
+ return buildCompactionV2RequestFromBody(model, body, options);
225
+ }
226
+
227
+ /** Wrap a body built by the normal Codex serializer for V2 compaction transport. */
228
+ export function buildCompactionV2RequestFromBody(
229
+ model: Model,
230
+ body: OpenAICodexCompactionBody,
231
+ options?: {
232
+ sessionId?: string;
233
+ promptCacheKey?: string;
234
+ retainedMessageBudget?: number;
235
+ },
236
+ ): CompactionV2Request {
237
+ const input = Array.isArray(body.input) ? body.input : [];
238
+ return {
239
+ body: { ...body, model: resolveCompactionV2Model(model), input },
240
+ input,
211
241
  retainedMessageBudget: resolveCompactionV2RetainedMessageBudget(options?.retainedMessageBudget),
212
- reasoning: options?.reasoning,
213
- tools: options?.tools,
214
242
  sessionId: options?.sessionId,
215
243
  promptCacheKey: options?.promptCacheKey,
216
244
  };
@@ -311,35 +339,17 @@ async function attemptCompactionV2Streaming(
311
339
  },
312
340
  ): Promise<CompactionV2Response> {
313
341
  // Faithful to Codex: append the compaction trigger as the final input item
314
- // of an otherwise-normal Responses request, then stream the result. `store`
315
- // stays false — compaction must never persist a server-side response object.
316
- const cacheOptions = { sessionId: request.sessionId, promptCacheKey: request.promptCacheKey };
317
- const promptCacheKey = getOpenAIPromptCacheKey(cacheOptions);
342
+ // of an otherwise-normal Responses request. `store` remains false —
343
+ // compaction must never persist a server-side response object.
318
344
  const body: OpenAICodexCompactionBody = {
319
- model: request.model,
345
+ ...request.body,
320
346
  input: [...request.input, COMPACTION_TRIGGER_ITEM],
321
- instructions: request.instructions,
322
- stream: true,
323
347
  store: false,
324
- ...(request.reasoning || model.useResponsesLite
325
- ? {
326
- // Lite implies gpt-5.4+, where codex-rs sends `all_turns` replay.
327
- reasoning: model.useResponsesLite ? { ...request.reasoning, context: "all_turns" } : request.reasoning,
328
- include: ["reasoning.encrypted_content"],
329
- }
330
- : {}),
331
- ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
332
- ...(request.tools && request.tools.length > 0 ? { tools: request.tools, tool_choice: "auto" } : {}),
348
+ stream: true,
333
349
  };
334
350
  if (options.codexMetadata) {
335
351
  body.client_metadata = options.codexMetadata.clientMetadata;
336
352
  }
337
- // Responses Lite models take the same rewrite on the compaction stream:
338
- // instructions/tools ride as input items (codex-rs `compact_remote_v2`
339
- // builds through `build_responses_request`).
340
- if (model.useResponsesLite) {
341
- applyCodexResponsesLiteShape(body);
342
- }
343
353
 
344
354
  if (shouldUseCodexProviderTransport(model)) {
345
355
  const eventStream = await openCodexCompactionEventStream(model, body, {
@@ -407,7 +417,7 @@ function buildCompactionV2Headers(
407
417
  "content-type": "application/json",
408
418
  ...resolveOpenAIRequestSetup(
409
419
  { provider: model.provider, id: model.id, baseUrl: model.baseUrl, headers: model.headers },
410
- { apiKey, messages: [], openAISessionId: routingSessionId, promptCacheSessionId },
420
+ { apiKey, messages: [], sessionId: request.sessionId ?? routingSessionId, promptCacheSessionId },
411
421
  ).headers,
412
422
  };
413
423
  if (api === "openai-codex-responses" || model.provider === "openai-codex") {
@@ -424,6 +434,7 @@ function buildCompactionV2Headers(
424
434
  headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
425
435
  headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
426
436
  headers[OPENAI_HEADERS.CODEX_BETA_FEATURES] = OPENAI_HEADER_VALUES.REMOTE_COMPACTION_V2;
437
+ headers[OPENAI_HEADERS.ROUTING_HINT] = codexRoutingHint(request.body.model, undefined);
427
438
  if (model.useResponsesLite) {
428
439
  headers[OPENAI_HEADERS.RESPONSES_LITE] = "true";
429
440
  }
@@ -25,7 +25,12 @@ import {
25
25
  } from "@linxiraos/pi-ai";
26
26
  import type { Dialect } from "@linxiraos/pi-ai/dialect";
27
27
  import * as AIError from "@linxiraos/pi-ai/error";
28
- import { createOpenAICodexCompactionRequestContext } from "@linxiraos/pi-ai/providers/openai-codex-responses";
28
+ import type { InputItem as CodexInputItem } from "@linxiraos/pi-ai/providers/openai-codex/request-transformer";
29
+ import {
30
+ buildTransformedCodexRequestBody,
31
+ createOpenAICodexCompactionRequestContext,
32
+ type OpenAICodexCompactionBody,
33
+ } from "@linxiraos/pi-ai/providers/openai-codex-responses";
29
34
  import { convertTools } from "@linxiraos/pi-ai/providers/openai-responses";
30
35
  import { buildResponsesInput, resolveOpenAICompatPolicy } from "@linxiraos/pi-ai/providers/openai-shared";
31
36
  import { stripOpenAIResponsesOutputOnlyStatusesForReplay } from "@linxiraos/pi-ai/utils";
@@ -39,6 +44,7 @@ import { Tokenizer } from "../tokenizer";
39
44
  import type { AgentMessage } from "../types";
40
45
  import {
41
46
  buildCompactionV2Request,
47
+ buildCompactionV2RequestFromBody,
42
48
  getCompactionV2PreserveData,
43
49
  requestCompactionV2Streaming,
44
50
  shouldUseCompactionV2Streaming,
@@ -660,7 +666,8 @@ export interface SummaryOptions {
660
666
  promptOverride?: string;
661
667
  extraContext?: string[];
662
668
  remoteEndpoint?: string;
663
- remoteInstructions?: string;
669
+ /** Stable system-prompt segments from the live turn, preserved for provider cache reuse. */
670
+ remoteSystemPrompt?: string[];
664
671
  initiatorOverride?: MessageAttribution;
665
672
  metadata?: Record<string, unknown>;
666
673
  convertToLlm?: ConvertToLlm;
@@ -1433,6 +1440,19 @@ export function prepareCompaction(
1433
1440
  // ============================================================================
1434
1441
 
1435
1442
  const TURN_PREFIX_SUMMARIZATION_PROMPT = prompt.render(compactionTurnPrefixPrompt);
1443
+ function isCodexResponsesModel(model: Model): model is Model<"openai-codex-responses"> {
1444
+ return model.api === "openai-codex-responses";
1445
+ }
1446
+
1447
+ function isCodexInputItem(item: Record<string, unknown>): item is CodexInputItem & Record<string, unknown> {
1448
+ return (
1449
+ (item.id === undefined || item.id === null || typeof item.id === "string") &&
1450
+ (item.type === undefined || item.type === null || typeof item.type === "string") &&
1451
+ (item.role === undefined || typeof item.role === "string") &&
1452
+ (item.call_id === undefined || item.call_id === null || typeof item.call_id === "string") &&
1453
+ (item.name === undefined || typeof item.name === "string")
1454
+ );
1455
+ }
1436
1456
 
1437
1457
  function openAiCompatSupportsImageDetailOriginal(model: Model): boolean {
1438
1458
  const compat = model.compat;
@@ -1546,7 +1566,7 @@ export async function compact(
1546
1566
  promptOverride: options?.promptOverride,
1547
1567
  extraContext: options?.extraContext,
1548
1568
  remoteEndpoint: settings.remoteEnabled === false ? undefined : settings.remoteEndpoint,
1549
- remoteInstructions: options?.remoteInstructions,
1569
+ remoteSystemPrompt: options?.remoteSystemPrompt,
1550
1570
  initiatorOverride: options?.initiatorOverride,
1551
1571
  metadata: options?.metadata,
1552
1572
  convertToLlm: options?.convertToLlm,
@@ -1598,17 +1618,63 @@ export async function compact(
1598
1618
  previousRemoteCompaction?.provider === model.provider
1599
1619
  ? previousRemoteCompaction.replacementHistory
1600
1620
  : undefined;
1601
- const remoteHistory = buildOpenAiResponsesCompactionInput(
1602
- (summaryOptions.convertToLlm ?? defaultConvertToLlm)(remoteMessages),
1603
- model,
1604
- previousReplacementHistory,
1605
- );
1621
+ const messages = (summaryOptions.convertToLlm ?? defaultConvertToLlm)(remoteMessages);
1622
+ const remoteSystemPrompt = summaryOptions.remoteSystemPrompt ?? [SUMMARIZATION_SYSTEM_PROMPT];
1623
+ let codexBody: OpenAICodexCompactionBody | undefined;
1624
+ let remoteHistory: Array<Record<string, unknown>>;
1625
+ if (isCodexResponsesModel(model)) {
1626
+ const previousCodexInput: CodexInputItem[] = [];
1627
+ for (const item of previousReplacementHistory ?? []) {
1628
+ if (!isCodexInputItem(item)) {
1629
+ throw new Error("Stored Codex V2 compaction history contains an invalid input item");
1630
+ }
1631
+ previousCodexInput.push(item);
1632
+ }
1633
+ codexBody = await buildTransformedCodexRequestBody(
1634
+ model,
1635
+ { systemPrompt: remoteSystemPrompt, messages, tools: summaryOptions.tools },
1636
+ {
1637
+ reasoning: resolveCompactionEffort(model, summaryOptions.thinkingLevel),
1638
+ forceReasoningOff: summaryOptions.thinkingLevel === ThinkingLevel.Off,
1639
+ responsesLite: model.useResponsesLite,
1640
+ sessionId: summaryOptions.sessionId,
1641
+ promptCacheKey: summaryOptions.promptCacheKey,
1642
+ providerSessionState: summaryOptions.providerSessionState,
1643
+ codexCompaction: createOpenAICodexCompactionRequestContext({
1644
+ context: summaryOptions.codexCompaction,
1645
+ implementation: "responses_compaction_v2",
1646
+ }),
1647
+ },
1648
+ undefined,
1649
+ previousCodexInput,
1650
+ );
1651
+ const input = Array.isArray(codexBody.input) ? codexBody.input : [];
1652
+ const nativeInput: Array<Record<string, unknown>> = [];
1653
+ for (const item of input) {
1654
+ if (!isRecord(item)) {
1655
+ throw new Error("Codex V2 compaction input contains a non-object item");
1656
+ }
1657
+ nativeInput.push(item);
1658
+ }
1659
+ remoteHistory = stripOpenAIResponsesOutputOnlyStatusesForReplay(nativeInput);
1660
+ codexBody.input = remoteHistory;
1661
+ } else {
1662
+ remoteHistory = buildOpenAiResponsesCompactionInput(messages, model, previousReplacementHistory);
1663
+ }
1606
1664
  if (remoteHistory.length > 0) {
1607
1665
  try {
1608
- const instructions = summaryOptions.remoteInstructions ?? SUMMARIZATION_SYSTEM_PROMPT;
1609
- const tools = summaryOptions.tools
1610
- ? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
1611
- : undefined;
1666
+ const instructions = codexBody
1667
+ ? typeof codexBody.instructions === "string"
1668
+ ? codexBody.instructions
1669
+ : ""
1670
+ : remoteSystemPrompt.join("\n\n");
1671
+ const tools = codexBody
1672
+ ? Array.isArray(codexBody.tools)
1673
+ ? codexBody.tools
1674
+ : undefined
1675
+ : summaryOptions.tools
1676
+ ? convertTools(summaryOptions.tools, model.compat.supportsStrictMode, model)
1677
+ : undefined;
1612
1678
  const trimmed = trimRemoteCompactionInputToContextWindow(
1613
1679
  remoteHistory,
1614
1680
  new Tokenizer(model),
@@ -1626,13 +1692,18 @@ export async function compact(
1626
1692
  contextWindow: model.contextWindow,
1627
1693
  });
1628
1694
  }
1629
- const request = buildCompactionV2Request(model, trimmed.input, instructions, {
1630
- tools,
1631
- reasoning: buildCompactionV2Reasoning(model, summaryOptions.thinkingLevel),
1695
+ const requestOptions = {
1632
1696
  sessionId: summaryOptions.sessionId,
1633
1697
  promptCacheKey: summaryOptions.promptCacheKey,
1634
1698
  retainedMessageBudget: settings.v2RetainedMessageBudget,
1635
- });
1699
+ };
1700
+ const request = codexBody
1701
+ ? buildCompactionV2RequestFromBody(model, { ...codexBody, input: trimmed.input }, requestOptions)
1702
+ : buildCompactionV2Request(model, trimmed.input, instructions, {
1703
+ ...requestOptions,
1704
+ tools,
1705
+ reasoning: buildCompactionV2Reasoning(model, summaryOptions.thinkingLevel),
1706
+ });
1636
1707
  const remote = await withAuth(
1637
1708
  apiKey,
1638
1709
  key =>
@@ -1685,7 +1756,7 @@ export async function compact(
1685
1756
  model,
1686
1757
  key,
1687
1758
  remoteHistory,
1688
- summaryOptions.remoteInstructions ?? SUMMARIZATION_SYSTEM_PROMPT,
1759
+ summaryOptions.remoteSystemPrompt?.join("\n\n") ?? SUMMARIZATION_SYSTEM_PROMPT,
1689
1760
  signal,
1690
1761
  {
1691
1762
  fetch: summaryOptions.fetch,
@@ -48,6 +48,7 @@ import { captureOpenAIHttpError } from "@linxiraos/pi-ai/utils/openai-http";
48
48
  import {
49
49
  applyCodexResidencyHeader,
50
50
  CODEX_BASE_URL,
51
+ codexRoutingHint,
51
52
  getCodexAccountId,
52
53
  OPENAI_HEADER_VALUES,
53
54
  OPENAI_HEADERS,
@@ -824,6 +825,8 @@ export async function requestOpenAiRemoteCompaction(
824
825
  }
825
826
  headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
826
827
  headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
828
+ // This compaction request sends no `service_tier`, so the hint is model-only.
829
+ headers[OPENAI_HEADERS.ROUTING_HINT] = codexRoutingHint(request.model, undefined);
827
830
  Object.assign(
828
831
  headers,
829
832
  createOpenAICodexCompatibilityMetadata({
package/src/telemetry.ts CHANGED
@@ -1638,7 +1638,7 @@ export async function recordManualChatTelemetry(
1638
1638
  /**
1639
1639
  * Options accepted by {@link instrumentedCompleteSimple}. Mirrors the
1640
1640
  * `streamAssistantResponse` chat-span lifecycle for oneshot LLM calls
1641
- * (compaction summaries, handoff document, branch summary, inspect_image).
1641
+ * (compaction summaries, handoff document, branch summary, image_question).
1642
1642
  */
1643
1643
  export interface InstrumentedChatSpanOptions {
1644
1644
  readonly telemetry: AgentTelemetry | undefined;
@@ -1649,7 +1649,7 @@ export interface InstrumentedChatSpanOptions {
1649
1649
  /**
1650
1650
  * Tag stamped onto `pi.gen_ai.oneshot.kind`. Values used by the agent:
1651
1651
  * `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
1652
- * `handoff`, `branch_summary`, `inspect_image`. Free-form to allow callers
1652
+ * `handoff`, `branch_summary`, `image_question`. Free-form to allow callers
1653
1653
  * outside this package to add new kinds without bumping the helper.
1654
1654
  */
1655
1655
  readonly oneshotKind?: string;