@oh-my-pi/pi-agent-core 18.9.1 → 18.10.0
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 +6 -0
- package/package.json +8 -8
- package/src/append-only-context.ts +38 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.9.2] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Improved context caching for repeat turns, reducing redundant prompt processing and making subsequent turns faster when the system prompt is unchanged.
|
|
10
|
+
|
|
5
11
|
## [18.9.0] - 2026-10-11
|
|
6
12
|
|
|
7
13
|
### Fixed
|
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.
|
|
4
|
+
"version": "18.10.0",
|
|
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.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.10.0",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.10.0",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.10.0",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.10.0",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.10.0",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.10.0",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.
|
|
50
|
+
"@oh-my-pi/omptype": "18.10.0",
|
|
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"
|
|
@@ -112,21 +112,23 @@ export class StablePrefix {
|
|
|
112
112
|
* Build or rebuild from live context.
|
|
113
113
|
* Returns `true` if the prefix actually changed (cache miss imminent).
|
|
114
114
|
*
|
|
115
|
-
* Steady-state fast path: when the live prompt
|
|
116
|
-
* AND every tool resolves to the same normalized parameters
|
|
117
|
-
*
|
|
118
|
-
*
|
|
115
|
+
* Steady-state fast path: when the live prompt has the same segments as
|
|
116
|
+
* last build AND every tool resolves to the same normalized parameters
|
|
117
|
+
* identity, the fingerprint cannot have changed, so the full snapshot +
|
|
118
|
+
* stringify is skipped. Comparing resolved parameters (not the tool
|
|
119
119
|
* container) is load-bearing: tools like ReadTool expose `parameters` as
|
|
120
120
|
* a getter over live settings (`skillful`, `memory.backend`), so the
|
|
121
121
|
* schema can swap under stable tool references when a setting toggles.
|
|
122
122
|
* Any other in-place mutation must go through `invalidate()`.
|
|
123
123
|
*/
|
|
124
|
-
|
|
125
|
-
// Joined prompt bytes snapshot: the prompt array is caller-owned and
|
|
124
|
+
// Segment-level prompt copy: the prompt array is caller-owned and
|
|
126
125
|
// mutable in place (Agent.setSystemPrompt stores the caller's array;
|
|
127
126
|
// anyone holding it can push/splice), so reference equality alone cannot
|
|
128
|
-
// prove the bytes are unchanged.
|
|
129
|
-
|
|
127
|
+
// prove the bytes are unchanged. Comparing the recorded segments against
|
|
128
|
+
// the live ones roots out that mutation without concatenating the whole
|
|
129
|
+
// prompt just to compare it — segments are immutable strings, so `!==`
|
|
130
|
+
// on two strings is a byte comparison.
|
|
131
|
+
#lastPromptSegments: readonly string[] | undefined;
|
|
130
132
|
// Per-tool wire-identity snapshot: every field normalizeTools and
|
|
131
133
|
// computeFingerprint read (name, description, resolved parameters,
|
|
132
134
|
// strict, customFormat, customWireName, intent mode, examples
|
|
@@ -138,11 +140,17 @@ export class StablePrefix {
|
|
|
138
140
|
|
|
139
141
|
build(context: AgentContext, options: BuildOptions): boolean {
|
|
140
142
|
const prev = this.#snapshot;
|
|
141
|
-
|
|
143
|
+
// The per-tool keys are rendered once per build and thread from the
|
|
144
|
+
// fast-path check into the recorded key, so a miss does not re-render
|
|
145
|
+
// every live tool description a second time (`normalizeTools` reads
|
|
146
|
+
// the same getters inside takeSnapshot). `prev === null` skips the
|
|
147
|
+
// check entirely — there is nothing to compare against.
|
|
148
|
+
const toolKeys = (context.tools ?? []).map(tool => toolKeyForPrefix(tool));
|
|
149
|
+
if (prev !== null && this.#fastPathHit(context, options, toolKeys)) {
|
|
142
150
|
return false;
|
|
143
151
|
}
|
|
144
152
|
const snapshot = takeSnapshot(context, options);
|
|
145
|
-
this.#recordFastPathKey(context, options,
|
|
153
|
+
this.#recordFastPathKey(context, options, toolKeys);
|
|
146
154
|
if (prev && prev.fingerprint === snapshot.fingerprint) {
|
|
147
155
|
// Identity changed but bytes did not (e.g. equivalent rebuild):
|
|
148
156
|
// keep serving the cached snapshot so downstream memo identity
|
|
@@ -154,47 +162,48 @@ export class StablePrefix {
|
|
|
154
162
|
return true;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
/** True when the cheap key matches: prompt + per-tool
|
|
158
|
-
#fastPathHit(context: AgentContext, options: BuildOptions): boolean {
|
|
165
|
+
/** True when the cheap key matches: prompt segments + per-tool identity. */
|
|
166
|
+
#fastPathHit(context: AgentContext, options: BuildOptions, toolKeys: readonly string[]): boolean {
|
|
159
167
|
if (
|
|
160
168
|
this.#lastIntentTracing !== options.intentTracing ||
|
|
161
169
|
this.#lastPruneToolDescriptions !== options.pruneToolDescriptions
|
|
162
170
|
) {
|
|
163
171
|
return false;
|
|
164
172
|
}
|
|
165
|
-
// Prompt
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
// Prompt segments: the caller may push/splice the live array in place
|
|
174
|
+
// without any reference change, so compare the recorded segments
|
|
175
|
+
// rather than concatenating the whole prompt just to compare it.
|
|
176
|
+
const liveSegments = context.systemPrompt;
|
|
177
|
+
const cachedSegments = this.#lastPromptSegments;
|
|
178
|
+
if (cachedSegments === undefined || cachedSegments.length !== liveSegments.length) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
for (let i = 0; i < cachedSegments.length; i++) {
|
|
182
|
+
if (cachedSegments[i] !== liveSegments[i]) return false;
|
|
171
183
|
}
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
184
|
+
const recorded = this.#lastToolKey;
|
|
185
|
+
if (recorded === undefined || recorded.length !== toolKeys.length) {
|
|
174
186
|
return false;
|
|
175
187
|
}
|
|
176
|
-
for (let i = 0; i <
|
|
177
|
-
if (
|
|
188
|
+
for (let i = 0; i < toolKeys.length; i++) {
|
|
189
|
+
if (recorded[i] !== toolKeys[i]) {
|
|
178
190
|
return false;
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
return true;
|
|
182
194
|
}
|
|
183
195
|
|
|
184
|
-
#recordFastPathKey(context: AgentContext, options: BuildOptions,
|
|
185
|
-
this.#
|
|
186
|
-
this.#lastPromptText = context.systemPrompt.join("\u0000");
|
|
196
|
+
#recordFastPathKey(context: AgentContext, options: BuildOptions, renderedToolKeys: readonly string[]): void {
|
|
197
|
+
this.#lastPromptSegments = [...context.systemPrompt];
|
|
187
198
|
this.#lastIntentTracing = options.intentTracing;
|
|
188
199
|
this.#lastPruneToolDescriptions = options.pruneToolDescriptions;
|
|
189
|
-
this.#lastToolKey =
|
|
190
|
-
void normalized;
|
|
200
|
+
this.#lastToolKey = renderedToolKeys;
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
/** Force rebuild on the next `build()` call. */
|
|
194
204
|
invalidate(): void {
|
|
195
205
|
this.#snapshot = null;
|
|
196
|
-
this.#
|
|
197
|
-
this.#lastPromptText = undefined;
|
|
206
|
+
this.#lastPromptSegments = undefined;
|
|
198
207
|
this.#lastToolKey = undefined;
|
|
199
208
|
this.#lastIntentTracing = undefined;
|
|
200
209
|
this.#lastPruneToolDescriptions = undefined;
|