@linxiraos/pi-agent-core 1.0.4 → 1.0.6

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 (33) hide show
  1. package/CHANGELOG.md +21 -5
  2. package/dist/types/agent-loop.d.ts +137 -0
  3. package/dist/types/agent.d.ts +465 -0
  4. package/dist/types/append-only-context.d.ts +131 -0
  5. package/dist/types/compaction/branch-summarization.d.ts +101 -0
  6. package/dist/types/compaction/compaction-v2-streaming.d.ts +85 -0
  7. package/dist/types/compaction/compaction.d.ts +321 -0
  8. package/dist/types/compaction/entries.d.ts +111 -0
  9. package/dist/types/compaction/errors.d.ts +38 -0
  10. package/dist/types/compaction/index.d.ts +13 -0
  11. package/dist/types/compaction/message-cache.d.ts +23 -0
  12. package/dist/types/compaction/messages.d.ts +79 -0
  13. package/dist/types/compaction/openai.d.ts +117 -0
  14. package/dist/types/compaction/pruning.d.ts +105 -0
  15. package/dist/types/compaction/shake.d.ts +99 -0
  16. package/dist/types/compaction/tool-protection.d.ts +19 -0
  17. package/dist/types/compaction/utils.d.ts +61 -0
  18. package/dist/types/compaction.d.ts +1 -0
  19. package/dist/types/index.d.ts +13 -0
  20. package/dist/types/pause.d.ts +46 -0
  21. package/dist/types/proxy.d.ts +91 -0
  22. package/dist/types/replay-policy.d.ts +5 -0
  23. package/dist/types/run-collector.d.ts +196 -0
  24. package/dist/types/telemetry.d.ts +625 -0
  25. package/dist/types/thinking.d.ts +18 -0
  26. package/dist/types/tokenizer.d.ts +2 -0
  27. package/dist/types/types.d.ts +786 -0
  28. package/dist/types/utils/yield.d.ts +71 -0
  29. package/package.json +79 -78
  30. package/src/compaction/branch-summarization.ts +12 -6
  31. package/src/compaction/compaction-v2-streaming.ts +1 -0
  32. package/src/compaction/compaction.ts +48 -6
  33. package/src/telemetry.ts +39 -4
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Cooperative yield utility for preventing Bun event-loop busy-wait.
3
+ *
4
+ * ## Root Cause
5
+ *
6
+ * Bun 1.3.x (JavaScriptCore) event loop busy-waits (spins in userspace)
7
+ * when the only pending work is an unresolved Promise — even if there are
8
+ * active I/O watchers (stdin, child process pipes, etc.). The event loop
9
+ * continuously polls for microtask resolution instead of blocking in
10
+ * `epoll_wait`, consuming ~100% of a CPU core.
11
+ *
12
+ * This affects any `await` on a never-resolved Promise, including:
13
+ * - `Promise.withResolvers()` used for user input callbacks
14
+ * - `await proc.exited` for long-running child processes
15
+ * - Agent loop iterations waiting for the next tool call
16
+ *
17
+ * ## Fix
18
+ *
19
+ * A recurring `setInterval` keeps the event loop sleeping in `epoll_wait`.
20
+ * The `EventLoopKeepalive` class and `keepaliveWhile()` wrapper provide a
21
+ * clean way to install and clean up this keepalive timer.
22
+ *
23
+ * The older `yieldIfDue()` and `ExponentialYield` approaches (compensated
24
+ * sleep loops) are retained for the agent-loop hot-path where Promises
25
+ * resolve frequently and the keepalive alone is insufficient.
26
+ */
27
+ export declare class EventLoopKeepalive {
28
+ #private;
29
+ [Symbol.dispose](): void;
30
+ }
31
+ /**
32
+ * Cooperative yield gate. Sleeps for at least {@link YieldGateOptions.sleepMs}
33
+ * but at most once every {@link YieldGateOptions.intervalMs}; hot-path callers
34
+ * invoke it freely and only the slow path actually sleeps.
35
+ *
36
+ * The clock and sleep are injectable so tests drive the gate logic without
37
+ * touching process-global `Date.now`/`scheduler.wait` — globals a concurrent
38
+ * test file can restore mid-run, which previously made the shared gate flake.
39
+ */
40
+ export interface YieldGateOptions {
41
+ now?: () => number;
42
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
43
+ intervalMs?: number;
44
+ sleepMs?: number;
45
+ }
46
+ export declare class YieldGate {
47
+ #private;
48
+ constructor(opts?: YieldGateOptions);
49
+ yieldIfDue(signal?: AbortSignal): Promise<void>;
50
+ }
51
+ /**
52
+ * Yield to the Bun event loop, sleeping for at least 20 ms — but at most once
53
+ * every {@link YIELD_INTERVAL_MS} across all callers.
54
+ */
55
+ export declare function yieldIfDue(): Promise<void>;
56
+ export declare class ExponentialYield {
57
+ #private;
58
+ constructor(opts?: {
59
+ minMs?: number;
60
+ maxMs?: number;
61
+ multiplier?: number;
62
+ });
63
+ notifyActivity(): void;
64
+ sleep(signal?: AbortSignal): Promise<number>;
65
+ /**
66
+ * Race `racers` against an exponentially-backed-off cooperative yield.
67
+ * The losing sleep is cancelled as soon as a racer settles, so no stray
68
+ * timers keep the event loop alive past the racer's resolution.
69
+ */
70
+ race<T>(racers: Array<Promise<T>>): Promise<T>;
71
+ }
package/package.json CHANGED
@@ -1,78 +1,79 @@
1
- {
2
- "type": "module",
3
- "name": "@linxiraos/pi-agent-core",
4
- "version": "1.0.4",
5
- "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
- "homepage": "https://linxira-os.github.io",
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/can1357/oh-my-pi/issues"
19
- },
20
- "keywords": [
21
- "ai",
22
- "agent",
23
- "llm",
24
- "transport",
25
- "state-management"
26
- ],
27
- "main": "./src/index.ts",
28
- "types": "./src/index.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.0.4",
39
- "@linxiraos/pi-catalog": "1.0.4",
40
- "@linxiraos/pi-natives": "1.0.2",
41
- "@linxiraos/pi-utils": "1.0.4",
42
- "@linxiraos/pi-wire": "1.0.2",
43
- "@linxiraos/pi-snapcompact": "1.0.4",
44
- "@opentelemetry/api": "^1.9.1"
45
- },
46
- "devDependencies": {
47
- "@linxiraos/pi-omptype": "1.0.2",
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
- ],
60
- "exports": {
61
- ".": {
62
- "types": "./src/index.ts",
63
- "import": "./src/index.ts"
64
- },
65
- "./compaction": {
66
- "types": "./src/compaction.ts",
67
- "import": "./src/compaction.ts"
68
- },
69
- "./compaction/*": {
70
- "types": "./src/compaction/*.ts",
71
- "import": "./src/compaction/*.ts"
72
- },
73
- "./*": {
74
- "types": "./src/*.ts",
75
- "import": "./src/*.ts"
76
- }
77
- }
78
- }
1
+ {
2
+ "type": "module",
3
+ "name": "@linxiraos/pi-agent-core",
4
+ "version": "1.0.6",
5
+ "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
+ "homepage": "https://linxira-os.github.io",
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/can1357/oh-my-pi/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.0.6",
39
+ "@linxiraos/pi-catalog": "1.0.6",
40
+ "@linxiraos/pi-natives": "1.0.2",
41
+ "@linxiraos/pi-utils": "1.0.6",
42
+ "@linxiraos/pi-wire": "1.0.2",
43
+ "@opentelemetry/api": "^1.9.1",
44
+ "@linxiraos/pi-snapcompact": "1.0.6"
45
+ },
46
+ "devDependencies": {
47
+ "@linxiraos/pi-omptype": "1.0.2",
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
+ "dist/types"
60
+ ],
61
+ "exports": {
62
+ ".": {
63
+ "types": "./dist/types/index.d.ts",
64
+ "import": "./src/index.ts"
65
+ },
66
+ "./compaction": {
67
+ "types": "./dist/types/compaction.d.ts",
68
+ "import": "./src/compaction.ts"
69
+ },
70
+ "./compaction/*": {
71
+ "types": "./dist/types/compaction/*.d.ts",
72
+ "import": "./src/compaction/*.ts"
73
+ },
74
+ "./*": {
75
+ "types": "./dist/types/*.d.ts",
76
+ "import": "./src/*.ts"
77
+ }
78
+ }
79
+ }
@@ -334,12 +334,18 @@ export async function generateBranchSummary(
334
334
  ];
335
335
 
336
336
  // Call LLM for summarization
337
- const response = await instrumentedCompleteSimple(
338
- model,
339
- { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
340
- { apiKey, signal, maxTokens: 2048, metadata },
341
- { telemetry: options.telemetry, oneshotKind: "branch_summary", completeImpl: options.completeImpl },
342
- );
337
+ let response: AssistantMessage;
338
+ try {
339
+ response = await instrumentedCompleteSimple(
340
+ model,
341
+ { systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
342
+ { apiKey, signal, maxTokens: 2048, metadata },
343
+ { telemetry: options.telemetry, oneshotKind: "branch_summary", completeImpl: options.completeImpl, retry: {} },
344
+ );
345
+ } catch (error) {
346
+ if (signal.aborted) return { aborted: true };
347
+ throw error;
348
+ }
343
349
 
344
350
  // Check if aborted or errored
345
351
  if (response.stopReason === "aborted") {
@@ -423,6 +423,7 @@ function buildCompactionV2Headers(
423
423
  }
424
424
  headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
425
425
  headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
426
+ headers[OPENAI_HEADERS.CODEX_BETA_FEATURES] = OPENAI_HEADER_VALUES.REMOTE_COMPACTION_V2;
426
427
  if (model.useResponsesLite) {
427
428
  headers[OPENAI_HEADERS.RESPONSES_LITE] = "true";
428
429
  }
@@ -16,6 +16,7 @@ import {
16
16
  type Message,
17
17
  type MessageAttribution,
18
18
  type Model,
19
+ type OneshotRetryOptions,
19
20
  type ProviderSessionState,
20
21
  type SimpleStreamOptions,
21
22
  type Tool,
@@ -470,8 +471,8 @@ function computeMessageTokens(message: AgentMessage, options?: { excludeEncrypte
470
471
  if (!options?.excludeEncryptedReasoning) fragments.push(block.data);
471
472
  } else if (block.type === "anthropicServerTool") {
472
473
  // Native Anthropic server-tool call/result replayed verbatim on the
473
- // wire (server_tool_use input, web_search_tool_result
474
- // encrypted_content). Opaque provider-replay state the provider still
474
+ // wire (server_tool_use input and opaque result content). This opaque
475
+ // provider-replay state the provider still
475
476
  // bills for on same-provider replay; excluded from the compaction
476
477
  // floor like other encrypted reasoning because its local byte size
477
478
  // diverges from provider billing.
@@ -831,6 +832,31 @@ export interface SummaryOptions {
831
832
  ctx: Context,
832
833
  options: SimpleStreamOptions,
833
834
  ) => Promise<AssistantMessage>;
835
+ /**
836
+ * Transient-failure retry for the summarization oneshots (`generateSummary`,
837
+ * `generateShortSummary`, `generateTurnPrefixSummary`).
838
+ *
839
+ * Defaults to enabled, which is what a one-shot caller such as manual
840
+ * `/compact` needs: a single Anthropic `overloaded_error` / 429 / 529 should
841
+ * not abort compaction and leave the context full.
842
+ *
843
+ * Pass `false` when the CALLER already owns a retry loop around the whole
844
+ * compaction attempt — auto-compaction does — otherwise the two budgets
845
+ * multiply (10 outer attempts x 3 inner = 30 requests) and each outer wait
846
+ * stacks on top of the inner backoff.
847
+ */
848
+ oneshotRetry?: OneshotRetryOptions | false;
849
+ }
850
+
851
+ /**
852
+ * Resolve the oneshot retry policy for a summarization call. Enabled by default
853
+ * so a lone transient blip cannot abort compaction; `false` opts out for callers
854
+ * that already retry the whole attempt (see `SummaryOptions.oneshotRetry`).
855
+ */
856
+ function summaryOneshotRetry(options: SummaryOptions | undefined): OneshotRetryOptions | undefined {
857
+ const configured = options?.oneshotRetry;
858
+ if (configured === false) return undefined;
859
+ return configured ?? {};
834
860
  }
835
861
 
836
862
  function localCodexCompaction(options: SummaryOptions | undefined) {
@@ -935,7 +961,12 @@ export async function generateSummary(
935
961
  providerSessionState: options?.providerSessionState,
936
962
  codexCompaction: localCodexCompaction(options),
937
963
  },
938
- { telemetry: options?.telemetry, oneshotKind: "compaction_summary", completeImpl: options?.completeImpl },
964
+ {
965
+ telemetry: options?.telemetry,
966
+ oneshotKind: "compaction_summary",
967
+ completeImpl: options?.completeImpl,
968
+ retry: summaryOneshotRetry(options),
969
+ },
939
970
  );
940
971
 
941
972
  if (response.stopReason === "error") {
@@ -1034,13 +1065,14 @@ export async function generateHandoffFromContext(
1034
1065
  telemetry: options.telemetry,
1035
1066
  oneshotKind: "handoff",
1036
1067
  completeImpl: options.completeImpl,
1068
+ retry: {},
1037
1069
  });
1038
1070
  if (response.stopReason === "error" && shouldRetryHandoffWithAutoToolChoice(response)) {
1039
1071
  response = await instrumentedCompleteSimple(
1040
1072
  model,
1041
1073
  context,
1042
1074
  { ...requestOptions, toolChoice: "auto" },
1043
- { telemetry: options.telemetry, oneshotKind: "handoff", completeImpl: options.completeImpl },
1075
+ { telemetry: options.telemetry, oneshotKind: "handoff", completeImpl: options.completeImpl, retry: {} },
1044
1076
  );
1045
1077
  }
1046
1078
 
@@ -1143,7 +1175,12 @@ async function generateShortSummary(
1143
1175
  providerSessionState: options?.providerSessionState,
1144
1176
  codexCompaction: localCodexCompaction(options),
1145
1177
  },
1146
- { telemetry: options?.telemetry, oneshotKind: "compaction_short_summary", completeImpl: options?.completeImpl },
1178
+ {
1179
+ telemetry: options?.telemetry,
1180
+ oneshotKind: "compaction_short_summary",
1181
+ completeImpl: options?.completeImpl,
1182
+ retry: summaryOneshotRetry(options),
1183
+ },
1147
1184
  );
1148
1185
 
1149
1186
  if (response.stopReason === "error") {
@@ -1719,7 +1756,12 @@ async function generateTurnPrefixSummary(
1719
1756
  providerSessionState: options?.providerSessionState,
1720
1757
  codexCompaction: localCodexCompaction(options),
1721
1758
  },
1722
- { telemetry: options?.telemetry, oneshotKind: "compaction_turn_prefix", completeImpl: options?.completeImpl },
1759
+ {
1760
+ telemetry: options?.telemetry,
1761
+ oneshotKind: "compaction_turn_prefix",
1762
+ completeImpl: options?.completeImpl,
1763
+ retry: summaryOneshotRetry(options),
1764
+ },
1723
1765
  );
1724
1766
 
1725
1767
  if (response.stopReason === "error") {
package/src/telemetry.ts CHANGED
@@ -30,6 +30,8 @@ import {
30
30
  completeSimple,
31
31
  type Message,
32
32
  type Model,
33
+ type OneshotRetryOptions,
34
+ retryTransientCompletion,
33
35
  type ServiceTier,
34
36
  type SimpleStreamOptions,
35
37
  type StopReason,
@@ -1663,6 +1665,23 @@ export interface InstrumentedChatSpanOptions {
1663
1665
  ctx: Context,
1664
1666
  options: SimpleStreamOptions,
1665
1667
  ) => Promise<AssistantMessage>;
1668
+ /**
1669
+ * Opt in to transient-failure retry for this oneshot (Anthropic
1670
+ * `overloaded_error`, `rate_limit_error`, 429/500/502/503/529). Omitted or
1671
+ * `undefined` means **no retry** — the failure is surfaced exactly as before.
1672
+ *
1673
+ * Deliberately opt-in rather than default-on: `oneshotKind` is free-form and
1674
+ * callers may pass arbitrary `ctx.tools` / `options.toolChoice`, so this
1675
+ * funnel cannot itself prove a given request is replay-safe. Re-issuing is
1676
+ * only safe when the call performs no side effect and nothing consumed
1677
+ * partial output — true for summaries, titles, handoffs and image
1678
+ * descriptions, which parse a complete response after it resolves. Enable it
1679
+ * per call site, as a reviewed decision.
1680
+ *
1681
+ * Pass `{}` to accept the {@link retryTransientCompletion} defaults
1682
+ * (3 attempts, 500ms base backoff, `retry-after` honored).
1683
+ */
1684
+ readonly retry?: OneshotRetryOptions;
1666
1685
  }
1667
1686
 
1668
1687
  /**
@@ -1723,10 +1742,26 @@ export async function instrumentedCompleteSimple<TApi extends Api>(
1723
1742
  try {
1724
1743
  return await runInActiveSpan(chatSpan, async () => {
1725
1744
  const complete = span.completeImpl ?? completeSimple;
1726
- const message = await complete(model, ctx, {
1727
- ...options,
1728
- onResponse: captureOnResponse,
1729
- });
1745
+ // Opt-in only (see `retry` on InstrumentedChatSpanOptions): each attempt
1746
+ // re-issues the whole request, which is safe only for replay-safe
1747
+ // oneshots. `getResponseHeaders` hands the failed attempt's headers to
1748
+ // the retry layer — an AssistantMessage carries none, so this is what
1749
+ // makes `retry-after` on a real 429/529 actually honored.
1750
+ const runOnce = () => {
1751
+ // Clear first so a previous attempt's `retry-after` can never be
1752
+ // reused for a later failure that arrived without headers.
1753
+ capturedHeaders = undefined;
1754
+ return complete(model, ctx, { ...options, onResponse: captureOnResponse });
1755
+ };
1756
+ const message = span.retry
1757
+ ? await retryTransientCompletion(runOnce, {
1758
+ ...span.retry,
1759
+ // Framework-owned: the caller must not be able to detach the
1760
+ // abort signal or the header source by passing them itself.
1761
+ signal: options.signal,
1762
+ getResponseHeaders: () => capturedHeaders,
1763
+ })
1764
+ : await runOnce();
1730
1765
  await finishChatSpan(telemetry, chatSpan, message, {
1731
1766
  stepNumber,
1732
1767
  serviceTier: options.serviceTier,