@oh-my-pi/pi-agent-core 18.0.3 → 18.0.4
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/package.json +8 -8
- package/src/agent-loop.ts +17 -2
- package/src/append-only-context.ts +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [18.0.4] - 2026-08-24
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Improved performance in append-only context mode by memoizing message serialization, keeping per-call sync overhead flat as conversations grow.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed an issue where `onTurnEnd` was skipped for turns ended by a terminal tool result (such as a subagent's final `yield`).
|
|
14
|
+
|
|
3
15
|
## [18.0.0] - 2026-08-22
|
|
4
16
|
|
|
5
17
|
### 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.0.
|
|
4
|
+
"version": "18.0.4",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "18.0.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "18.0.
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.0.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.0.
|
|
42
|
-
"@oh-my-pi/pi-wire": "18.0.
|
|
43
|
-
"@oh-my-pi/snapcompact": "18.0.
|
|
38
|
+
"@oh-my-pi/pi-ai": "18.0.4",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "18.0.4",
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.0.4",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.0.4",
|
|
42
|
+
"@oh-my-pi/pi-wire": "18.0.4",
|
|
43
|
+
"@oh-my-pi/snapcompact": "18.0.4",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@oh-my-pi/omptype": "18.0.
|
|
47
|
+
"@oh-my-pi/omptype": "18.0.4",
|
|
48
48
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
49
49
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
50
50
|
"@types/bun": "^1.3.14"
|
package/src/agent-loop.ts
CHANGED
|
@@ -623,6 +623,15 @@ function buildAgentEndEvent(
|
|
|
623
623
|
* Push a `turn_end` event and run the awaited per-turn hook when the run is
|
|
624
624
|
* still healthy. The hook is skipped for externally aborted or errored turns so
|
|
625
625
|
* a user interrupt does not hang on a background backlog wait.
|
|
626
|
+
*
|
|
627
|
+
* A {@link TERMINAL_TOOL_RESULT_ABORT_REASON} abort is the exception: it is a
|
|
628
|
+
* graceful yield (e.g. a subagent's final `yield` tool), not a user interrupt.
|
|
629
|
+
* The completed tool batch is persisted and the turn must still reach
|
|
630
|
+
* `onTurnEnd` so per-turn bookkeeping — notably advisor review of the yield
|
|
631
|
+
* delta (#9505) — runs exactly as it does for a plain end-of-turn message. The
|
|
632
|
+
* hook receives no signal in that case so downstream waits (advisor catch-up)
|
|
633
|
+
* behave identically to a normal final turn instead of short-circuiting on the
|
|
634
|
+
* spent abort.
|
|
626
635
|
*/
|
|
627
636
|
async function emitTurnEnd(
|
|
628
637
|
stream: EventStream<AgentEvent, AgentMessage[]>,
|
|
@@ -635,10 +644,16 @@ async function emitTurnEnd(
|
|
|
635
644
|
runHookOnAbortedMessage = false,
|
|
636
645
|
): Promise<void> {
|
|
637
646
|
stream.push({ type: "turn_end", message, toolResults });
|
|
647
|
+
const terminalYield = signal?.reason === TERMINAL_TOOL_RESULT_ABORT_REASON;
|
|
638
648
|
const isAbortedOrError =
|
|
639
649
|
message.role === "assistant" && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
640
|
-
if (signal?.aborted || (isAbortedOrError && !runHookOnAbortedMessage)) return;
|
|
641
|
-
await config.onTurnEnd?.(currentContext.messages,
|
|
650
|
+
if ((signal?.aborted && !terminalYield) || (isAbortedOrError && !runHookOnAbortedMessage)) return;
|
|
651
|
+
await config.onTurnEnd?.(currentContext.messages, terminalYield ? undefined : signal, {
|
|
652
|
+
message,
|
|
653
|
+
toolResults,
|
|
654
|
+
willContinue: false,
|
|
655
|
+
...context,
|
|
656
|
+
});
|
|
642
657
|
}
|
|
643
658
|
|
|
644
659
|
function createGateStopMessage(model: Model, reason: string | undefined): AssistantMessage {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
|
|
18
18
|
import { normalizeTools } from "./agent-loop";
|
|
19
|
-
import
|
|
19
|
+
import { messageEstimateVersion } from "./compaction/message-cache";
|
|
20
|
+
import type { AgentContext, AgentMessage } from "./types";
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// StablePrefix (formerly ImmutablePrefix)
|
|
@@ -177,6 +178,26 @@ export class AppendOnlyContextManager {
|
|
|
177
178
|
* point on every subsequent turn.
|
|
178
179
|
*/
|
|
179
180
|
#messageDigests: number[] = [];
|
|
181
|
+
/**
|
|
182
|
+
* Digests memoized by message object identity, validated by the message's
|
|
183
|
+
* estimate version ({@link messageEstimateVersion}). Synced message objects
|
|
184
|
+
* are stable between calls: converted fragments are cached per session
|
|
185
|
+
* message identity and handed back unchanged on every call, so an unchanged
|
|
186
|
+
* history re-hits this memo instead of re-serializing every previously-
|
|
187
|
+
* synced message on each LLM call.
|
|
188
|
+
*
|
|
189
|
+
* Owner-side rewrites (prune/shake/strip-images) mutate messages IN PLACE
|
|
190
|
+
* under stable identity — for assistant pass-through fragments the log
|
|
191
|
+
* aliases the very object being mutated — so a bare identity memo would
|
|
192
|
+
* serve pre-mutation bytes forever. Those owners MUST call
|
|
193
|
+
* `invalidateMessageCache`, which bumps the symbol-keyed version tag
|
|
194
|
+
* this memo validates before every hit; a version mismatch recomputes from
|
|
195
|
+
* actual bytes and the sync diverges exactly as if a fresh object had
|
|
196
|
+
* arrived. Mutating a synced message without that bump violates the
|
|
197
|
+
* cache-coherence contract shared with the tokenizer and convert caches
|
|
198
|
+
* (see `compaction/message-cache.ts`) and is unsupported.
|
|
199
|
+
*/
|
|
200
|
+
#digestMemo = new WeakMap<object, { version: number; digest: number }>();
|
|
180
201
|
|
|
181
202
|
build(context: AgentContext, options: BuildOptions): Context {
|
|
182
203
|
this.prefix.build(context, options);
|
|
@@ -290,6 +311,9 @@ export class AppendOnlyContextManager {
|
|
|
290
311
|
#messageDigest(msg: unknown): number {
|
|
291
312
|
if (!msg || typeof msg !== "object") return 0;
|
|
292
313
|
const m = msg as Record<string, unknown>;
|
|
314
|
+
const version = messageEstimateVersion(msg as AgentMessage);
|
|
315
|
+
const cached = this.#digestMemo.get(m);
|
|
316
|
+
if (cached !== undefined && cached.version === version) return cached.digest;
|
|
293
317
|
const payload = JSON.stringify({
|
|
294
318
|
r: m.role ?? null,
|
|
295
319
|
c: m.content ?? null,
|
|
@@ -304,7 +328,9 @@ export class AppendOnlyContextManager {
|
|
|
304
328
|
for (let j = 0; j < payload.length; j++) {
|
|
305
329
|
hash = ((hash << 5) - hash + payload.charCodeAt(j)) | 0;
|
|
306
330
|
}
|
|
307
|
-
|
|
331
|
+
const hash32 = hash >>> 0;
|
|
332
|
+
this.#digestMemo.set(m, { version, digest: hash32 });
|
|
333
|
+
return hash32;
|
|
308
334
|
}
|
|
309
335
|
}
|
|
310
336
|
|