@oh-my-pi/pi-agent-core 18.9.0 → 18.9.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 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.9.0",
4
+ "version": "18.9.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.9.0",
42
- "@oh-my-pi/pi-catalog": "18.9.0",
43
- "@oh-my-pi/pi-natives": "18.9.0",
44
- "@oh-my-pi/pi-utils": "18.9.0",
45
- "@oh-my-pi/pi-wire": "18.9.0",
46
- "@oh-my-pi/snapcompact": "18.9.0",
41
+ "@oh-my-pi/pi-ai": "18.9.2",
42
+ "@oh-my-pi/pi-catalog": "18.9.2",
43
+ "@oh-my-pi/pi-natives": "18.9.2",
44
+ "@oh-my-pi/pi-utils": "18.9.2",
45
+ "@oh-my-pi/pi-wire": "18.9.2",
46
+ "@oh-my-pi/snapcompact": "18.9.2",
47
47
  "@opentelemetry/api": "^1.9.1"
48
48
  },
49
49
  "devDependencies": {
50
- "@oh-my-pi/omptype": "18.9.0",
50
+ "@oh-my-pi/omptype": "18.9.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"
@@ -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 reference is unchanged
116
- * AND every tool resolves to the same normalized parameters identity as
117
- * last build, the fingerprint cannot have changed, so the full snapshot
118
- * + stringify is skipped. Comparing resolved parameters (not the tool
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
- #lastPrompt: readonly string[] | undefined;
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. Compared by value on the fast path.
129
- #lastPromptText: string | undefined;
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
- if (prev !== null && this.#fastPathHit(context, options)) {
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, snapshot.tools);
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 resolved identity. */
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 by reference first (steady state), then by joined bytes so
166
- // an in-place push/splice of the same array still misses.
167
- if (this.#lastPrompt !== context.systemPrompt) return false;
168
- if (this.#lastPromptText !== undefined) {
169
- const text = context.systemPrompt.join("\u0000");
170
- if (text !== this.#lastPromptText) return false;
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 tools = context.tools ?? [];
173
- if (this.#lastToolKey === undefined || this.#lastToolKey.length !== tools.length) {
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 < tools.length; i++) {
177
- if (this.#lastToolKey[i] !== toolKeyForPrefix(tools[i]!)) {
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, normalized: Tool[]): void {
185
- this.#lastPrompt = context.systemPrompt;
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 = (context.tools ?? []).map(tool => toolKeyForPrefix(tool));
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.#lastPrompt = undefined;
197
- this.#lastPromptText = undefined;
206
+ this.#lastPromptSegments = undefined;
198
207
  this.#lastToolKey = undefined;
199
208
  this.#lastIntentTracing = undefined;
200
209
  this.#lastPruneToolDescriptions = undefined;