@oh-my-pi/pi-agent-core 18.4.4 → 18.4.6
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/agent.d.ts +7 -0
- package/dist/types/compaction/pruning.d.ts +6 -0
- package/dist/types/types.d.ts +7 -0
- package/package.json +8 -8
- package/src/agent-loop.ts +3 -0
- package/src/agent.ts +14 -0
- package/src/compaction/pruning.ts +39 -18
- package/src/types.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.4.6] - 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added APIs for improving agent follow-up and steering workflow management, including moving queued follow-ups into steering with a single queue-change notification.
|
|
10
|
+
- Added support for trusted post-tool guidance via `afterToolCall` results, allowing additional context to be included in the next provider request, including after tool failures.
|
|
11
|
+
|
|
5
12
|
## [18.4.4] - 2026-09-29
|
|
6
13
|
|
|
7
14
|
### Added
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -477,6 +477,13 @@ export declare class Agent {
|
|
|
477
477
|
* delivery are dropped here exactly as {@link replaceQueues} does.
|
|
478
478
|
*/
|
|
479
479
|
replaceQueue(queue: "steering" | "followUp", messages: readonly AgentMessage[]): void;
|
|
480
|
+
/**
|
|
481
|
+
* Install `followUp` as the pending follow-up queue (same claim handling as
|
|
482
|
+
* {@link replaceQueue}) and append `promoted` to the steering queue, then notify
|
|
483
|
+
* queue listeners once. A live steering claim is left alone. Listeners never
|
|
484
|
+
* observe the moved messages in both queues or in neither.
|
|
485
|
+
*/
|
|
486
|
+
moveFollowUpsToSteering(followUp: readonly AgentMessage[], promoted: readonly AgentMessage[]): void;
|
|
480
487
|
appendMessage(m: AgentMessage): void;
|
|
481
488
|
popMessage(): AgentMessage | undefined;
|
|
482
489
|
/**
|
|
@@ -43,6 +43,12 @@ export declare const DEFAULT_PRUNE_CONFIG: PruneConfig;
|
|
|
43
43
|
export interface PruneResult {
|
|
44
44
|
prunedCount: number;
|
|
45
45
|
tokensSaved: number;
|
|
46
|
+
/**
|
|
47
|
+
* Restore every result this pass blanked. Pruning mutates entries in place,
|
|
48
|
+
* so a caller whose persistence of the pruned history fails calls this to keep
|
|
49
|
+
* memory matching what is durable.
|
|
50
|
+
*/
|
|
51
|
+
undo(): void;
|
|
46
52
|
}
|
|
47
53
|
/** Exact placeholder written over a superseded tool result. */
|
|
48
54
|
export declare const SUPERSEDED_NOTICE = "[Superseded by a newer read of this file]";
|
package/dist/types/types.d.ts
CHANGED
|
@@ -808,6 +808,13 @@ export interface AfterToolCallResult {
|
|
|
808
808
|
isError?: boolean;
|
|
809
809
|
/** If provided, replaces the contextually-useless flag carried with the tool result. */
|
|
810
810
|
useless?: boolean;
|
|
811
|
+
/**
|
|
812
|
+
* Trusted post-tool instructions for the next provider request. Delivered
|
|
813
|
+
* outside the tool result, after all calls in the batch settle. Unlike
|
|
814
|
+
* `BeforeToolCallResult.additionalContext`, this is retained for error
|
|
815
|
+
* results because the callback receives the finalized outcome.
|
|
816
|
+
*/
|
|
817
|
+
additionalContext?: string;
|
|
811
818
|
}
|
|
812
819
|
/** Context passed to `beforeToolCall`. */
|
|
813
820
|
export interface BeforeToolCallContext {
|
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.
|
|
4
|
+
"version": "18.4.6",
|
|
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.4.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.4.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.4.6",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.4.6",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.4.6",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.4.6",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.4.6",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.4.6",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.4.
|
|
50
|
+
"@oh-my-pi/omptype": "18.4.6",
|
|
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
|
@@ -3470,6 +3470,9 @@ async function executeToolCalls(
|
|
|
3470
3470
|
});
|
|
3471
3471
|
result = coerced.result;
|
|
3472
3472
|
isError = coerced.malformed || (after.isError ?? isError);
|
|
3473
|
+
if (isNonBlankContext(after.additionalContext)) {
|
|
3474
|
+
record.reportedContext.push(after.additionalContext);
|
|
3475
|
+
}
|
|
3473
3476
|
}
|
|
3474
3477
|
} catch (e) {
|
|
3475
3478
|
caughtError = e;
|
package/src/agent.ts
CHANGED
|
@@ -1252,6 +1252,20 @@ export class Agent {
|
|
|
1252
1252
|
this.#emitQueueChanged();
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* Install `followUp` as the pending follow-up queue (same claim handling as
|
|
1257
|
+
* {@link replaceQueue}) and append `promoted` to the steering queue, then notify
|
|
1258
|
+
* queue listeners once. A live steering claim is left alone. Listeners never
|
|
1259
|
+
* observe the moved messages in both queues or in neither.
|
|
1260
|
+
*/
|
|
1261
|
+
moveFollowUpsToSteering(followUp: readonly AgentMessage[], promoted: readonly AgentMessage[]): void {
|
|
1262
|
+
this.#followUpQueue = followUp.slice();
|
|
1263
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
1264
|
+
this.#steeringQueue.push(...promoted);
|
|
1265
|
+
this.#notifySteeringWaiters();
|
|
1266
|
+
this.#emitQueueChanged();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1255
1269
|
appendMessage(m: AgentMessage) {
|
|
1256
1270
|
this.#state.messages.push(m);
|
|
1257
1271
|
const live = this.#liveSteered.findIndex(entry => entry.message === m);
|
|
@@ -62,6 +62,34 @@ export const DEFAULT_PRUNE_CONFIG: PruneConfig = {
|
|
|
62
62
|
export interface PruneResult {
|
|
63
63
|
prunedCount: number;
|
|
64
64
|
tokensSaved: number;
|
|
65
|
+
/**
|
|
66
|
+
* Restore every result this pass blanked. Pruning mutates entries in place,
|
|
67
|
+
* so a caller whose persistence of the pruned history fails calls this to keep
|
|
68
|
+
* memory matching what is durable.
|
|
69
|
+
*/
|
|
70
|
+
undo(): void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const NOTHING_PRUNED: PruneResult = { prunedCount: 0, tokensSaved: 0, undo: () => {} };
|
|
74
|
+
|
|
75
|
+
/** Blank `message` to `notice`, returning the step that restores it. */
|
|
76
|
+
function blankToolResult(message: ToolResultMessage, notice: string, prunedAt: number): () => void {
|
|
77
|
+
const { content, prunedAt: previousPrunedAt } = message;
|
|
78
|
+
message.content = [{ type: "text", text: notice }];
|
|
79
|
+
message.prunedAt = prunedAt;
|
|
80
|
+
invalidateMessageCache(message as AgentMessage);
|
|
81
|
+
return () => {
|
|
82
|
+
message.content = content;
|
|
83
|
+
message.prunedAt = previousPrunedAt;
|
|
84
|
+
invalidateMessageCache(message as AgentMessage);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Combine per-message restore steps into one {@link PruneResult.undo}. */
|
|
89
|
+
function undoAll(steps: Array<() => void>): () => void {
|
|
90
|
+
return () => {
|
|
91
|
+
for (const step of steps) step();
|
|
92
|
+
};
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
/** Exact placeholder written over a superseded tool result. */
|
|
@@ -257,7 +285,7 @@ export function pruneSupersededToolResults(
|
|
|
257
285
|
candidates.push(...collectUselessResults(entries, tokenizer, toolCallsById, config.protectedTools, exclude));
|
|
258
286
|
candidates.sort((a, b) => a.index - b.index);
|
|
259
287
|
}
|
|
260
|
-
if (candidates.length === 0) return
|
|
288
|
+
if (candidates.length === 0) return NOTHING_PRUNED;
|
|
261
289
|
|
|
262
290
|
const now = config.now ?? Date.now();
|
|
263
291
|
let lastMessageTimestamp: number | undefined;
|
|
@@ -290,17 +318,15 @@ export function pruneSupersededToolResults(
|
|
|
290
318
|
candidate => candidate.index >= boundaryIndex && suffixTokens[candidate.index] <= suffixTokenLimit,
|
|
291
319
|
);
|
|
292
320
|
}
|
|
293
|
-
if (toPrune.length === 0) return
|
|
321
|
+
if (toPrune.length === 0) return NOTHING_PRUNED;
|
|
294
322
|
|
|
295
323
|
const prunedAt = Date.now();
|
|
296
324
|
let tokensSaved = 0;
|
|
297
|
-
|
|
298
|
-
candidate.message.content = [{ type: "text", text: candidate.notice }];
|
|
299
|
-
candidate.message.prunedAt = prunedAt;
|
|
300
|
-
invalidateMessageCache(candidate.message as AgentMessage);
|
|
325
|
+
const steps = toPrune.map(candidate => {
|
|
301
326
|
tokensSaved += estimatePrunedSavings(candidate.tokens, candidate.notice);
|
|
302
|
-
|
|
303
|
-
|
|
327
|
+
return blankToolResult(candidate.message, candidate.notice, prunedAt);
|
|
328
|
+
});
|
|
329
|
+
return { prunedCount: toPrune.length, tokensSaved, undo: undoAll(steps) };
|
|
304
330
|
}
|
|
305
331
|
|
|
306
332
|
export function pruneToolOutputs(
|
|
@@ -310,7 +336,6 @@ export function pruneToolOutputs(
|
|
|
310
336
|
): PruneResult {
|
|
311
337
|
let accumulatedTokens = 0;
|
|
312
338
|
let tokensSaved = 0;
|
|
313
|
-
let prunedCount = 0;
|
|
314
339
|
|
|
315
340
|
const candidates: Array<{ entry: SessionMessageEntry; tokens: number; superseded: boolean; useless: boolean }> = [];
|
|
316
341
|
const toolCallsById = collectToolCallsById(entries);
|
|
@@ -394,24 +419,20 @@ export function pruneToolOutputs(
|
|
|
394
419
|
}
|
|
395
420
|
|
|
396
421
|
if (tokensSaved < config.minimumSavings || candidates.length === 0) {
|
|
397
|
-
return
|
|
422
|
+
return NOTHING_PRUNED;
|
|
398
423
|
}
|
|
399
424
|
|
|
400
425
|
const prunedAt = Date.now();
|
|
401
|
-
|
|
402
|
-
const message = candidate.entry.message as ToolResultMessage;
|
|
426
|
+
const steps = candidates.map(candidate => {
|
|
403
427
|
const notice = candidate.superseded
|
|
404
428
|
? SUPERSEDED_NOTICE
|
|
405
429
|
: candidate.useless
|
|
406
430
|
? USELESS_NOTICE
|
|
407
431
|
: createPrunedNotice(candidate.tokens);
|
|
408
|
-
message
|
|
409
|
-
|
|
410
|
-
invalidateMessageCache(message as AgentMessage);
|
|
411
|
-
prunedCount++;
|
|
412
|
-
}
|
|
432
|
+
return blankToolResult(candidate.entry.message as ToolResultMessage, notice, prunedAt);
|
|
433
|
+
});
|
|
413
434
|
|
|
414
|
-
return { prunedCount, tokensSaved };
|
|
435
|
+
return { prunedCount: candidates.length, tokensSaved, undo: undoAll(steps) };
|
|
415
436
|
}
|
|
416
437
|
|
|
417
438
|
/**
|
package/src/types.ts
CHANGED
|
@@ -906,6 +906,13 @@ export interface AfterToolCallResult {
|
|
|
906
906
|
isError?: boolean;
|
|
907
907
|
/** If provided, replaces the contextually-useless flag carried with the tool result. */
|
|
908
908
|
useless?: boolean;
|
|
909
|
+
/**
|
|
910
|
+
* Trusted post-tool instructions for the next provider request. Delivered
|
|
911
|
+
* outside the tool result, after all calls in the batch settle. Unlike
|
|
912
|
+
* `BeforeToolCallResult.additionalContext`, this is retained for error
|
|
913
|
+
* results because the callback receives the finalized outcome.
|
|
914
|
+
*/
|
|
915
|
+
additionalContext?: string;
|
|
909
916
|
}
|
|
910
917
|
|
|
911
918
|
/** Context passed to `beforeToolCall`. */
|