@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 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
@@ -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]";
@@ -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",
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.4",
42
- "@oh-my-pi/pi-catalog": "18.4.4",
43
- "@oh-my-pi/pi-natives": "18.4.4",
44
- "@oh-my-pi/pi-utils": "18.4.4",
45
- "@oh-my-pi/pi-wire": "18.4.4",
46
- "@oh-my-pi/snapcompact": "18.4.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.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 { prunedCount: 0, tokensSaved: 0 };
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 { prunedCount: 0, tokensSaved: 0 };
321
+ if (toPrune.length === 0) return NOTHING_PRUNED;
294
322
 
295
323
  const prunedAt = Date.now();
296
324
  let tokensSaved = 0;
297
- for (const candidate of toPrune) {
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
- return { prunedCount: toPrune.length, tokensSaved };
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 { prunedCount: 0, tokensSaved: 0 };
422
+ return NOTHING_PRUNED;
398
423
  }
399
424
 
400
425
  const prunedAt = Date.now();
401
- for (const candidate of candidates) {
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.content = [{ type: "text", text: notice }];
409
- message.prunedAt = prunedAt;
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`. */