@oh-my-pi/pi-agent-core 18.3.1 → 18.3.2
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 +7 -0
- package/dist/types/compaction/pruning.d.ts +9 -0
- package/dist/types/compaction/transcript-tokens.d.ts +11 -1
- package/package.json +8 -8
- package/src/agent-loop.ts +21 -0
- package/src/compaction/anthropic.ts +1 -1
- package/src/compaction/pruning.ts +1 -1
- package/src/compaction/transcript-tokens.ts +33 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.3.2] - 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed tool calls that put their payload in the intent field `i` (for example a file body in `write`) silently running with the leftover arguments; they now fail with an error telling the model to retry ([#13140](https://github.com/can1357/oh-my-pi/issues/13140), [#13141](https://github.com/can1357/oh-my-pi/pull/13141) by [@radkawar](https://github.com/radkawar))
|
|
10
|
+
- Fixed the Anthropic compaction failure log omitting why no compaction block came back; it now names the stop reason ([#13300](https://github.com/can1357/oh-my-pi/pull/13300) by [@alphastorm](https://github.com/alphastorm))
|
|
11
|
+
|
|
5
12
|
## [18.3.1] - 2026-09-25
|
|
6
13
|
|
|
7
14
|
### Added
|
|
@@ -82,6 +82,15 @@ export interface SupersedePruneConfig {
|
|
|
82
82
|
/** Tool-result protection matchers (same contract as {@link PruneConfig.protectedTools}). */
|
|
83
83
|
protectedTools: ProtectedToolMatcher[];
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Generic age-based pruning floor. Below this, blanking a result to
|
|
87
|
+
* `[Output truncated - N tokens]` recovers nothing — the placeholder itself
|
|
88
|
+
* costs ~8 tokens, so a sub-floor result grows the context (and churns the
|
|
89
|
+
* prompt cache) instead of shrinking it. Superseded/useless results keep their
|
|
90
|
+
* own rules: useless already drops no-savings candidates, superseded prunes for
|
|
91
|
+
* correctness regardless of size.
|
|
92
|
+
*/
|
|
93
|
+
export declare const MIN_PRUNE_TOKENS = 50;
|
|
85
94
|
/**
|
|
86
95
|
* Prune superseded tool results (e.g. stale `read` outputs replaced by a newer
|
|
87
96
|
* read of the same file) and, when `pruneUseless` is set, results their tool
|
|
@@ -37,6 +37,14 @@ export interface TranscriptUsageAnchor {
|
|
|
37
37
|
* the session-entry walkers.
|
|
38
38
|
*/
|
|
39
39
|
export declare function isTranscriptUsageAnchor(message: AgentMessage): message is AssistantMessage;
|
|
40
|
+
/** Options for {@link findTranscriptUsageAnchor}. */
|
|
41
|
+
export interface TranscriptUsageAnchorOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Also treat usage reported at or before the newest `prunedAt` as stale,
|
|
44
|
+
* the same rule {@link findRequestUsageAnchor} applies to request contexts.
|
|
45
|
+
*/
|
|
46
|
+
skipPrunedAnchors?: boolean;
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* Newest assistant turn in `messages[fromIndex..]` whose usage can anchor the
|
|
42
50
|
* transcript, or `undefined` when none qualifies (fresh context, or every
|
|
@@ -45,7 +53,7 @@ export declare function isTranscriptUsageAnchor(message: AgentMessage): message
|
|
|
45
53
|
* `fromIndex` excludes turns whose usage is stale — anything a compaction
|
|
46
54
|
* summarized away describes a prompt that is no longer sent.
|
|
47
55
|
*/
|
|
48
|
-
export declare function findTranscriptUsageAnchor(messages: readonly AgentMessage[], fromIndex?: number): TranscriptUsageAnchor | undefined;
|
|
56
|
+
export declare function findTranscriptUsageAnchor(messages: readonly AgentMessage[], fromIndex?: number, options?: TranscriptUsageAnchorOptions): TranscriptUsageAnchor | undefined;
|
|
49
57
|
/**
|
|
50
58
|
* Newest assistant turn in a provider request's `messages` whose usage still
|
|
51
59
|
* describes the prefix it sits on, or `undefined` when none does.
|
|
@@ -67,6 +75,8 @@ export interface TranscriptTokenOptions {
|
|
|
67
75
|
* accounting is governed separately by {@link countFromIndex}.
|
|
68
76
|
*/
|
|
69
77
|
anchorFromIndex?: number;
|
|
78
|
+
/** Forwarded to {@link findTranscriptUsageAnchor}. */
|
|
79
|
+
skipPrunedAnchors?: boolean;
|
|
70
80
|
/**
|
|
71
81
|
* First message whose content is counted locally when no anchor is found.
|
|
72
82
|
* Defaults to 0 (count the whole transcript), which is what a floor
|
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.3.
|
|
4
|
+
"version": "18.3.2",
|
|
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.3.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.3.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.3.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.3.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.3.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.3.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.3.2",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.3.2",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.3.2",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.3.2",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.3.2",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.3.2",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.3.
|
|
50
|
+
"@oh-my-pi/omptype": "18.3.2",
|
|
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-loop.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
getStreamingPartialJson,
|
|
40
40
|
kCursorExecResolved,
|
|
41
41
|
} from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
42
|
+
import { schemaDefinesProperty } from "@oh-my-pi/pi-ai/utils/schema/json-schema-validator";
|
|
42
43
|
import { stamp } from "@oh-my-pi/pi-ai/utils/schema/stamps";
|
|
43
44
|
import {
|
|
44
45
|
createHarmonyAuditEvent,
|
|
@@ -1022,6 +1023,13 @@ function resolveIntentMode(intent: AgentTool["intent"]): "require" | "optional"
|
|
|
1022
1023
|
return "require";
|
|
1023
1024
|
}
|
|
1024
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Longest `i` value accepted as an intent. The injected field is described as
|
|
1028
|
+
* a "concise intent" (INTENT_FIELD_DESCRIPTION); anything past this is a tool
|
|
1029
|
+
* payload the model put in the wrong field, not a label.
|
|
1030
|
+
*/
|
|
1031
|
+
const MAX_INTENT_LENGTH = 200;
|
|
1032
|
+
|
|
1025
1033
|
function extractIntent(args: Record<string, unknown>): { intent?: string; strippedArgs: Record<string, unknown> } {
|
|
1026
1034
|
const { [INTENT_FIELD]: intent, ...strippedArgs } = args;
|
|
1027
1035
|
if (typeof intent !== "string") {
|
|
@@ -2830,6 +2838,19 @@ async function prepareToolCallDispatch(
|
|
|
2830
2838
|
if (intentTracing) {
|
|
2831
2839
|
const { intent, strippedArgs } = extractIntent(toolCall.arguments);
|
|
2832
2840
|
argsForExecution = strippedArgs;
|
|
2841
|
+
// A payload in `i` would be stripped and the tool run with the leftover
|
|
2842
|
+
// args. Unknown tools fall through to the not-found error; a tool that
|
|
2843
|
+
// owns `i` as a real parameter has nowhere else to put the value.
|
|
2844
|
+
if (
|
|
2845
|
+
intent !== undefined &&
|
|
2846
|
+
intent.length > MAX_INTENT_LENGTH &&
|
|
2847
|
+
tool &&
|
|
2848
|
+
!schemaDefinesProperty(toolWireSchema(tool), INTENT_FIELD)
|
|
2849
|
+
) {
|
|
2850
|
+
entry.args = strippedArgs;
|
|
2851
|
+
entry.validationErrorMessage = `\`${INTENT_FIELD}\` is a short intent label (at most ${MAX_INTENT_LENGTH} chars); the value you sent is ${intent.length} chars. The tool was not run. Put that content in the tool's own parameters and retry with a brief \`${INTENT_FIELD}\`.`;
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2833
2854
|
if (intent) {
|
|
2834
2855
|
toolCall.intent = intent;
|
|
2835
2856
|
} else if (typeof tool?.intent === "function") {
|
|
@@ -255,7 +255,7 @@ export async function requestAnthropicNativeCompaction(
|
|
|
255
255
|
throw new Error(
|
|
256
256
|
response.stopDetails?.type === "compaction"
|
|
257
257
|
? "Anthropic compaction returned no signed summary"
|
|
258
|
-
:
|
|
258
|
+
: `Anthropic compaction response carried no compaction block (stop reason: ${response.stopDetails?.type ?? response.stopReason})`,
|
|
259
259
|
);
|
|
260
260
|
}
|
|
261
261
|
return {
|
|
@@ -120,7 +120,7 @@ function createPrunedNotice(tokens: number): string {
|
|
|
120
120
|
* own rules: useless already drops no-savings candidates, superseded prunes for
|
|
121
121
|
* correctness regardless of size.
|
|
122
122
|
*/
|
|
123
|
-
const MIN_PRUNE_TOKENS = 50;
|
|
123
|
+
export const MIN_PRUNE_TOKENS = 50;
|
|
124
124
|
|
|
125
125
|
function getToolResultMessage(entry: SessionEntry): ToolResultMessage | undefined {
|
|
126
126
|
if (entry.type !== "message") return undefined;
|
|
@@ -47,6 +47,31 @@ export function isTranscriptUsageAnchor(message: AgentMessage): message is Assis
|
|
|
47
47
|
return assistant.usage !== undefined && hasContextTokenUsage(assistant.usage);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Newest `prunedAt` in `messages`, or `-Infinity` when nothing was pruned.
|
|
52
|
+
*
|
|
53
|
+
* A pruned tool result was rewritten in place, so every usage report made at
|
|
54
|
+
* or before this time still counts the removed bytes.
|
|
55
|
+
*/
|
|
56
|
+
function newestPrunedAt(messages: readonly AgentMessage[]): number {
|
|
57
|
+
let rewriteAt = Number.NEGATIVE_INFINITY;
|
|
58
|
+
for (const message of messages) {
|
|
59
|
+
if (message.role === "toolResult" && message.prunedAt !== undefined) {
|
|
60
|
+
rewriteAt = Math.max(rewriteAt, message.prunedAt);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return rewriteAt;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Options for {@link findTranscriptUsageAnchor}. */
|
|
67
|
+
export interface TranscriptUsageAnchorOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Also treat usage reported at or before the newest `prunedAt` as stale,
|
|
70
|
+
* the same rule {@link findRequestUsageAnchor} applies to request contexts.
|
|
71
|
+
*/
|
|
72
|
+
skipPrunedAnchors?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
/**
|
|
51
76
|
* Newest assistant turn in `messages[fromIndex..]` whose usage can anchor the
|
|
52
77
|
* transcript, or `undefined` when none qualifies (fresh context, or every
|
|
@@ -58,10 +83,12 @@ export function isTranscriptUsageAnchor(message: AgentMessage): message is Assis
|
|
|
58
83
|
export function findTranscriptUsageAnchor(
|
|
59
84
|
messages: readonly AgentMessage[],
|
|
60
85
|
fromIndex = 0,
|
|
86
|
+
options?: TranscriptUsageAnchorOptions,
|
|
61
87
|
): TranscriptUsageAnchor | undefined {
|
|
88
|
+
const rewriteAt = options?.skipPrunedAnchors === true ? newestPrunedAt(messages) : Number.NEGATIVE_INFINITY;
|
|
62
89
|
for (let index = messages.length - 1; index >= fromIndex; index--) {
|
|
63
90
|
const message = messages[index];
|
|
64
|
-
if (!isTranscriptUsageAnchor(message)) continue;
|
|
91
|
+
if (!isTranscriptUsageAnchor(message) || message.timestamp <= rewriteAt) continue;
|
|
65
92
|
return { index, message, tokens: calculateContextTokens(message.usage) };
|
|
66
93
|
}
|
|
67
94
|
return undefined;
|
|
@@ -105,6 +132,8 @@ export interface TranscriptTokenOptions {
|
|
|
105
132
|
* accounting is governed separately by {@link countFromIndex}.
|
|
106
133
|
*/
|
|
107
134
|
anchorFromIndex?: number;
|
|
135
|
+
/** Forwarded to {@link findTranscriptUsageAnchor}. */
|
|
136
|
+
skipPrunedAnchors?: boolean;
|
|
108
137
|
/**
|
|
109
138
|
* First message whose content is counted locally when no anchor is found.
|
|
110
139
|
* Defaults to 0 (count the whole transcript), which is what a floor
|
|
@@ -132,7 +161,9 @@ export function estimateTranscriptTokens(
|
|
|
132
161
|
): number {
|
|
133
162
|
const estimateOptions: MessageCountOptions | undefined =
|
|
134
163
|
options?.excludeEncryptedReasoning === true ? { excludeEncryptedReasoning: true } : undefined;
|
|
135
|
-
const anchor = findTranscriptUsageAnchor(messages, options?.anchorFromIndex ?? 0
|
|
164
|
+
const anchor = findTranscriptUsageAnchor(messages, options?.anchorFromIndex ?? 0, {
|
|
165
|
+
skipPrunedAnchors: options?.skipPrunedAnchors,
|
|
166
|
+
});
|
|
136
167
|
let total = anchor?.tokens ?? 0;
|
|
137
168
|
for (let index = anchor ? anchor.index + 1 : (options?.countFromIndex ?? 0); index < messages.length; index++) {
|
|
138
169
|
total += tokenizer.countMessage(messages[index], estimateOptions);
|