@oh-my-pi/pi-agent-core 18.11.0 → 18.11.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/CHANGELOG.md +12 -0
- package/dist/types/agent.d.ts +7 -0
- package/dist/types/compaction/compaction.d.ts +8 -0
- package/dist/types/tokenizer.d.ts +10 -0
- package/package.json +8 -8
- package/src/agent.ts +23 -0
- package/src/compaction/compaction.ts +16 -3
- package/src/tokenizer.ts +27 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.11.1] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Tokenizer.countLatestTurn`: counts a message as the transcript's latest assistant turn, which an Anthropic Messages replay keeps byte-for-byte ([#15791](https://github.com/can1357/oh-my-pi/pull/15791) by [@will-bogusz](https://github.com/will-bogusz))
|
|
10
|
+
- `Agent.prioritizeSteering`: moves given pending steering messages ahead of older ones so one-at-a-time delivery takes them first ([#15642](https://github.com/can1357/oh-my-pi/pull/15642))
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed a Cursor tool call re-sent by the server persisting two results under one id; the re-run's result replaces the first ([#15749](https://github.com/can1357/oh-my-pi/issues/15749))
|
|
15
|
+
- Fixed compaction keeping more recent history than `keepRecentTokens` when the latest Claude turn on the Anthropic Messages API ended its tool calls on `stop` or `length`: the replay keeps that turn's signed reasoning, but the cut point counted only its visible blocks ([#15791](https://github.com/can1357/oh-my-pi/pull/15791) by [@will-bogusz](https://github.com/will-bogusz))
|
|
16
|
+
|
|
5
17
|
## [18.11.0] - 2026-10-14
|
|
6
18
|
|
|
7
19
|
### Breaking Changes
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -514,6 +514,13 @@ export declare class Agent {
|
|
|
514
514
|
* Delivered after current tool execution, skips remaining tools.
|
|
515
515
|
*/
|
|
516
516
|
steer(m: AgentMessage): void;
|
|
517
|
+
/**
|
|
518
|
+
* Move the given pending steering messages (by identity) ahead of the rest,
|
|
519
|
+
* keeping both groups' relative order, so one-at-a-time delivery takes them
|
|
520
|
+
* first. Messages no longer pending, including a claimed batch already
|
|
521
|
+
* being delivered, are ignored.
|
|
522
|
+
*/
|
|
523
|
+
prioritizeSteering(messages: readonly AgentMessage[]): void;
|
|
517
524
|
/**
|
|
518
525
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
519
526
|
* Delivered only when agent has no more tool calls or steering messages.
|
|
@@ -137,6 +137,14 @@ export declare function shouldCompact(contextTokens: number, contextWindow: numb
|
|
|
137
137
|
* takes the floor.)
|
|
138
138
|
*/
|
|
139
139
|
export declare function compactionContextTokens(providerContextTokens: number, storedConversationEstimate: number): number;
|
|
140
|
+
/**
|
|
141
|
+
* The prompt budget below `contextWindow`: the window less
|
|
142
|
+
* {@link resolveBudgetReserveTokens}, capped at `contextWindow - 1` so it never
|
|
143
|
+
* reaches the whole window even when the reserve resolves to 0. Independent of
|
|
144
|
+
* any configured trigger; the default {@link resolveThresholdTokens} and
|
|
145
|
+
* length-stop recovery's "did the window run out" check both use it.
|
|
146
|
+
*/
|
|
147
|
+
export declare function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number;
|
|
140
148
|
export declare function resolveThresholdTokens(contextWindow: number, settings: CompactionSettings): number;
|
|
141
149
|
/**
|
|
142
150
|
* True for entries that begin a conversational turn (a user request, a
|
|
@@ -111,6 +111,16 @@ export declare class Tokenizer {
|
|
|
111
111
|
* the native remote-compaction probe applies.
|
|
112
112
|
*/
|
|
113
113
|
countMessage(message: AgentMessage, options?: MessageCountOptions): number;
|
|
114
|
+
/**
|
|
115
|
+
* {@link countMessage} for the transcript's latest surviving assistant turn
|
|
116
|
+
* (`isSurvivingAssistant`), which an Anthropic Messages replay keeps
|
|
117
|
+
* byte-for-byte (`transformMessages`): a turn whose tool calls ended without
|
|
118
|
+
* a tool-use stop keeps its signatures then, so its reasoning is charged. A
|
|
119
|
+
* count of one message cannot see its position; a caller walking back from
|
|
120
|
+
* the end of the transcript counts that turn here. Not memoized: it differs
|
|
121
|
+
* from {@link countMessage} only for that one turn.
|
|
122
|
+
*/
|
|
123
|
+
countLatestTurn(message: AgentMessage, options?: MessageCountOptions): number;
|
|
114
124
|
/** Sum of {@link countMessage} over `messages`. */
|
|
115
125
|
countMessages(messages: readonly AgentMessage[], options?: MessageCountOptions): number;
|
|
116
126
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "18.11.
|
|
4
|
+
"version": "18.11.1",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@oh-my-pi/pi-ai": "18.11.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.11.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.11.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.11.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.11.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.11.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.11.1",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.11.1",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.11.1",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.11.1",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.11.1",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.11.1",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.11.
|
|
50
|
+
"@oh-my-pi/omptype": "18.11.1",
|
|
51
51
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
52
52
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
53
53
|
"@types/bun": "^1.3.14"
|
package/src/agent.ts
CHANGED
|
@@ -1390,6 +1390,20 @@ export class Agent {
|
|
|
1390
1390
|
this.#emitQueueChanged();
|
|
1391
1391
|
}
|
|
1392
1392
|
|
|
1393
|
+
/**
|
|
1394
|
+
* Move the given pending steering messages (by identity) ahead of the rest,
|
|
1395
|
+
* keeping both groups' relative order, so one-at-a-time delivery takes them
|
|
1396
|
+
* first. Messages no longer pending, including a claimed batch already
|
|
1397
|
+
* being delivered, are ignored.
|
|
1398
|
+
*/
|
|
1399
|
+
prioritizeSteering(messages: readonly AgentMessage[]): void {
|
|
1400
|
+
const wanted = new Set(messages);
|
|
1401
|
+
const front = this.#steeringQueue.filter(message => wanted.has(message));
|
|
1402
|
+
if (front.every((message, index) => this.#steeringQueue[index] === message)) return;
|
|
1403
|
+
this.#steeringQueue = [...front, ...this.#steeringQueue.filter(message => !wanted.has(message))];
|
|
1404
|
+
this.#emitQueueChanged();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1393
1407
|
/**
|
|
1394
1408
|
* Queue a follow-up message to be processed after the agent finishes.
|
|
1395
1409
|
* Delivered only when agent has no more tool calls or steering messages.
|
|
@@ -1808,10 +1822,19 @@ export class Agent {
|
|
|
1808
1822
|
// that, a transformer resolving after the swap would patch a detached
|
|
1809
1823
|
// object while the persisted result kept the original payload — the
|
|
1810
1824
|
// rewrite silently lost.
|
|
1825
|
+
//
|
|
1826
|
+
// A result for a call already buffered is the server re-dispatching
|
|
1827
|
+
// that call; the provider keeps one block for it, so the re-run's
|
|
1828
|
+
// result replaces the first rather than pairing a second result with
|
|
1829
|
+
// the same id (issue #15749).
|
|
1811
1830
|
const entry: CursorToolResultEntry = {
|
|
1812
1831
|
toolResult: message,
|
|
1813
1832
|
additionalContext: (message as ToolResultWithAdditionalContext)[TOOL_RESULT_ADDITIONAL_CONTEXT],
|
|
1814
1833
|
};
|
|
1834
|
+
const superseded = this.#cursorToolResultBuffer.findIndex(
|
|
1835
|
+
({ toolResult }) => toolResult.toolCallId === message.toolCallId,
|
|
1836
|
+
);
|
|
1837
|
+
if (superseded >= 0) this.#cursorToolResultBuffer.splice(superseded, 1);
|
|
1815
1838
|
this.#cursorToolResultBuffer.push(entry);
|
|
1816
1839
|
const transform = this.#cursorOnToolResult;
|
|
1817
1840
|
if (transform) {
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import type { InputItem as CodexInputItem } from "@oh-my-pi/pi-ai/providers/openai-codex/request-transformer";
|
|
35
35
|
import { convertTools } from "@oh-my-pi/pi-ai/providers/openai-responses";
|
|
36
36
|
import { buildResponsesInput, resolveOpenAICompatPolicy } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
37
|
+
import { isSurvivingAssistant } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
37
38
|
import { stripOpenAIResponsesOutputOnlyStatusesForReplay } from "@oh-my-pi/pi-ai/utils";
|
|
38
39
|
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
39
40
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
@@ -375,9 +376,11 @@ export function compactionContextTokens(providerContextTokens: number, storedCon
|
|
|
375
376
|
/**
|
|
376
377
|
* The prompt budget below `contextWindow`: the window less
|
|
377
378
|
* {@link resolveBudgetReserveTokens}, capped at `contextWindow - 1` so it never
|
|
378
|
-
* reaches the whole window even when the reserve resolves to 0.
|
|
379
|
+
* reaches the whole window even when the reserve resolves to 0. Independent of
|
|
380
|
+
* any configured trigger; the default {@link resolveThresholdTokens} and
|
|
381
|
+
* length-stop recovery's "did the window run out" check both use it.
|
|
379
382
|
*/
|
|
380
|
-
function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
383
|
+
export function promptBudgetTokens(contextWindow: number, settings: CompactionSettings): number {
|
|
381
384
|
return Math.max(0, Math.min(contextWindow - 1, contextWindow - resolveBudgetReserveTokens(contextWindow, settings)));
|
|
382
385
|
}
|
|
383
386
|
|
|
@@ -535,13 +538,23 @@ export function findCutPoint(
|
|
|
535
538
|
// belonging to an assistant. Checking individual messages can either retain
|
|
536
539
|
// the oversized older assistant or miss its boundary and retain all history.
|
|
537
540
|
let accumulatedTokens = 0;
|
|
541
|
+
// The latest surviving assistant turn replays byte-for-byte, signatures of
|
|
542
|
+
// abandoned tool calls included, so it is counted as the latest.
|
|
543
|
+
let latestTurnIndex = -1;
|
|
544
|
+
for (let i = entries.length - 1; i >= 0 && latestTurnIndex < 0; i--) {
|
|
545
|
+
const message = getMessageFromEntry(entries[i]);
|
|
546
|
+
if (message?.role === "assistant" && isSurvivingAssistant(message)) latestTurnIndex = i;
|
|
547
|
+
}
|
|
538
548
|
let cutPointIndex = cutPoints.length - 1;
|
|
539
549
|
let cutIndex = cutPoints[cutPointIndex];
|
|
540
550
|
|
|
541
551
|
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
542
552
|
const entry = entries[i];
|
|
543
553
|
const message = getMessageFromEntry(entry);
|
|
544
|
-
if (message)
|
|
554
|
+
if (message) {
|
|
555
|
+
accumulatedTokens +=
|
|
556
|
+
i === latestTurnIndex ? tokenizer.countLatestTurn(message) : tokenizer.countMessage(message);
|
|
557
|
+
}
|
|
545
558
|
if (i !== cutPoints[cutPointIndex]) continue;
|
|
546
559
|
if (accumulatedTokens > keepRecentTokens) break;
|
|
547
560
|
cutIndex = i;
|
package/src/tokenizer.ts
CHANGED
|
@@ -295,7 +295,7 @@ export class Tokenizer {
|
|
|
295
295
|
*/
|
|
296
296
|
countMessage(message: AgentMessage, options?: MessageCountOptions): number {
|
|
297
297
|
const floored = options?.excludeEncryptedReasoning === true;
|
|
298
|
-
if (!isEstimateCacheable(message)) return this.#measureMessage(message, floored);
|
|
298
|
+
if (!isEstimateCacheable(message)) return this.#measureMessage(message, floored, false);
|
|
299
299
|
const version = messageEstimateVersion(message);
|
|
300
300
|
let entry = this.#estimates.get(message);
|
|
301
301
|
if (entry === undefined || entry.version !== version) {
|
|
@@ -304,12 +304,25 @@ export class Tokenizer {
|
|
|
304
304
|
}
|
|
305
305
|
const cached = floored ? entry.floored : entry.default;
|
|
306
306
|
if (cached !== undefined) return cached;
|
|
307
|
-
const result = this.#measureMessage(message, floored);
|
|
307
|
+
const result = this.#measureMessage(message, floored, false);
|
|
308
308
|
if (floored) entry.floored = result;
|
|
309
309
|
else entry.default = result;
|
|
310
310
|
return result;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* {@link countMessage} for the transcript's latest surviving assistant turn
|
|
315
|
+
* (`isSurvivingAssistant`), which an Anthropic Messages replay keeps
|
|
316
|
+
* byte-for-byte (`transformMessages`): a turn whose tool calls ended without
|
|
317
|
+
* a tool-use stop keeps its signatures then, so its reasoning is charged. A
|
|
318
|
+
* count of one message cannot see its position; a caller walking back from
|
|
319
|
+
* the end of the transcript counts that turn here. Not memoized: it differs
|
|
320
|
+
* from {@link countMessage} only for that one turn.
|
|
321
|
+
*/
|
|
322
|
+
countLatestTurn(message: AgentMessage, options?: MessageCountOptions): number {
|
|
323
|
+
return this.#measureMessage(message, options?.excludeEncryptedReasoning === true, true);
|
|
324
|
+
}
|
|
325
|
+
|
|
313
326
|
/** Sum of {@link countMessage} over `messages`. */
|
|
314
327
|
countMessages(messages: readonly AgentMessage[], options?: MessageCountOptions): number {
|
|
315
328
|
let total = 0;
|
|
@@ -361,7 +374,7 @@ export class Tokenizer {
|
|
|
361
374
|
return total;
|
|
362
375
|
}
|
|
363
376
|
|
|
364
|
-
#measureMessage(message: AgentMessage, excludeEncryptedReasoning: boolean): number {
|
|
377
|
+
#measureMessage(message: AgentMessage, excludeEncryptedReasoning: boolean, latestTurn: boolean): number {
|
|
365
378
|
const fragments: string[] = [];
|
|
366
379
|
let extra = 0;
|
|
367
380
|
// Declaration-merged app roles (the coding-agent's bashExecution) are
|
|
@@ -407,14 +420,20 @@ export class Tokenizer {
|
|
|
407
420
|
// The replay's signature rule (`transformMessages`): an aborted or
|
|
408
421
|
// errored turn's final block can hold a partial signature, and a turn
|
|
409
422
|
// whose tool calls ended without a tool-use stop is abandoned, so all
|
|
410
|
-
// its signatures are stripped. Every other block completed.
|
|
411
|
-
// assistant turn keeps an abandoned
|
|
412
|
-
//
|
|
413
|
-
//
|
|
423
|
+
// its signatures are stripped. Every other block completed. On the
|
|
424
|
+
// Anthropic Messages API the latest assistant turn keeps an abandoned
|
|
425
|
+
// turn's signatures byte-for-byte (Bedrock's Converse replay strips
|
|
426
|
+
// them too); a per-message count cannot see position, so it follows
|
|
427
|
+
// the rule for older turns unless the caller says it is the latest
|
|
428
|
+
// (`countLatestTurn`).
|
|
414
429
|
const content = message.content;
|
|
415
430
|
const invalidStop = message.stopReason === "aborted" || message.stopReason === "error";
|
|
431
|
+
const keepsLatestSignatures = latestTurn && message.api === "anthropic-messages";
|
|
416
432
|
const abandonedToolUse =
|
|
417
|
-
!
|
|
433
|
+
!keepsLatestSignatures &&
|
|
434
|
+
!invalidStop &&
|
|
435
|
+
message.stopReason !== "toolUse" &&
|
|
436
|
+
content.some(block => block.type === "toolCall");
|
|
418
437
|
const untrustedIndex = invalidStop ? content.length - 1 : -1;
|
|
419
438
|
let signedReasoning = false;
|
|
420
439
|
for (let index = 0; index < content.length; index++) {
|