@oh-my-pi/pi-agent-core 18.14.1 → 18.15.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 +16 -0
- package/dist/types/agent.d.ts +11 -0
- package/dist/types/compaction/utils.d.ts +2 -0
- package/dist/types/sent-tool-definitions.d.ts +5 -0
- package/dist/types/types.d.ts +3 -2
- package/package.json +8 -8
- package/src/agent-loop.ts +2 -2
- package/src/agent.ts +14 -0
- package/src/compaction/branch-summarization.ts +4 -0
- package/src/compaction/compaction.ts +13 -1
- package/src/compaction/openai.ts +4 -0
- package/src/compaction/utils.ts +5 -0
- package/src/sent-tool-definitions.ts +10 -0
- package/src/types.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.15.0] - 2026-11-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed OpenAI remote compaction (`/responses/compact`) replaying backend-only `internal_chat_message_metadata_passthrough` metadata that Codex rejects as an unknown parameter ([#17590](https://github.com/can1357/oh-my-pi/pull/17590) by [@briankeefe](https://github.com/briankeefe))
|
|
10
|
+
|
|
11
|
+
## [18.14.2] - 2026-10-31
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The soft steering signal a tool sees as `toolCall.steeringSignal` now aborts with the source of the queued message (`"user"`, `"irc"`, …) as its `reason`, so a tool can yield only to the sources it should ([#17537](https://github.com/can1357/oh-my-pi/issues/17537))
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Reject incomplete local compaction, turn-prefix, handoff, and branch summaries ending with `stopReason: "length"` instead of using truncated text as complete context. Omit incomplete display-only short summaries without discarding a completed full summary ([#17499](https://github.com/can1357/oh-my-pi/pull/17499) by [@Xytronix](https://github.com/Xytronix)).
|
|
20
|
+
|
|
5
21
|
## [18.14.1] - 2026-10-31
|
|
6
22
|
|
|
7
23
|
### Breaking Changes
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ApiKey, type AssistantMessage, type AssistantMessageEvent, type Co
|
|
|
2
2
|
import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
|
|
3
3
|
import type { HarmonyAuditEvent } from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
4
4
|
import type { AppendOnlyContextManager } from "./append-only-context.js";
|
|
5
|
+
import { SentToolDefinitions } from "./sent-tool-definitions.js";
|
|
5
6
|
import { Tokenizer } from "./tokenizer.js";
|
|
6
7
|
import type { AgentBeforeModelCall, AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentState, AgentTool, AgentToolContext, AgentTurnEndContext, AsideBoundary, AsideMessage, PrepareQueuedMessages, SpeculativeToolExecutionConfig, StreamFn, ToolCallContext, ToolChoiceDirective } from "./types.js";
|
|
7
8
|
export declare class AgentBusyError extends Error {
|
|
@@ -64,6 +65,11 @@ export interface AgentOptions {
|
|
|
64
65
|
* Shared provider state map for session-scoped transport/session caches.
|
|
65
66
|
*/
|
|
66
67
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
68
|
+
/**
|
|
69
|
+
* Tool definitions already sent, for an Agent that replays another Agent's request history
|
|
70
|
+
* (see {@link Agent.copySentToolDefinitions}). Defaults to an empty store.
|
|
71
|
+
*/
|
|
72
|
+
sentToolDefinitions?: SentToolDefinitions;
|
|
67
73
|
/**
|
|
68
74
|
* Resolves an API key or resolver dynamically for each LLM call.
|
|
69
75
|
* Useful for expiring tokens and model-scoped credential routing.
|
|
@@ -295,6 +301,11 @@ export declare class Agent {
|
|
|
295
301
|
* Set the prompt cache key forwarded to providers.
|
|
296
302
|
*/
|
|
297
303
|
set promptCacheKey(value: string | undefined);
|
|
304
|
+
/**
|
|
305
|
+
* A detached copy of the tool definitions this agent has sent (`AgentOptions.sentToolDefinitions`), without the
|
|
306
|
+
* names `keep` rejects.
|
|
307
|
+
*/
|
|
308
|
+
copySentToolDefinitions(keep?: (name: string) => boolean): SentToolDefinitions;
|
|
298
309
|
/**
|
|
299
310
|
* Fields added to every log record written while this agent runs (the
|
|
300
311
|
* owning session's `sessionId`), so in-process agents' records stay apart.
|
|
@@ -10,6 +10,8 @@ export interface FileOperations {
|
|
|
10
10
|
edited: Set<string>;
|
|
11
11
|
}
|
|
12
12
|
export declare function createFileOps(): FileOperations;
|
|
13
|
+
/** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
|
|
14
|
+
export declare function incompleteOutputError(prefix: string): string;
|
|
13
15
|
/**
|
|
14
16
|
* Split a read-tool path into its base path and trailing selector, mirroring the
|
|
15
17
|
* read tool's own splitter. Single source of the grammar in this package: the
|
|
@@ -8,6 +8,11 @@ export declare class SentToolDefinitions {
|
|
|
8
8
|
#private;
|
|
9
9
|
/** Remember the definitions a request is about to send. */
|
|
10
10
|
record(tools: readonly Tool[]): void;
|
|
11
|
+
/**
|
|
12
|
+
* A detached copy: an Agent that replays another's request history declares the same withdrawn tools. `keep`
|
|
13
|
+
* leaves out a definition the copy must never declare.
|
|
14
|
+
*/
|
|
15
|
+
copy(keep?: (name: string) => boolean): SentToolDefinitions;
|
|
11
16
|
/**
|
|
12
17
|
* `context` with {@link Context.inactiveTools} set to the definitions for names the latest
|
|
13
18
|
* `requestControls.tools.declared` in its messages holds that are not in its `tools`; cleared when
|
package/dist/types/types.d.ts
CHANGED
|
@@ -588,7 +588,8 @@ export interface ToolCallContext {
|
|
|
588
588
|
providerMetadata?: ToolCallProviderMetadata;
|
|
589
589
|
/**
|
|
590
590
|
* Cooperative steering signal: aborted when a queued user/steering message
|
|
591
|
-
* (or an interrupting peer IRC) is detected while this tool batch runs.
|
|
591
|
+
* (or an interrupting peer IRC) is detected while this tool batch runs. Its
|
|
592
|
+
* `reason` names the source: a {@link SteeringInterruptSource} or `"irc"`.
|
|
592
593
|
* Unlike the hard abort signal it NEVER kills the tool — long-running
|
|
593
594
|
* tools MAY observe it (via `ctx.toolCall.steeringSignal`) to finish early
|
|
594
595
|
* or background themselves so the message injects promptly; ignoring it is
|
|
@@ -967,7 +968,7 @@ export type ToolApprovalDecision = ToolTier | {
|
|
|
967
968
|
* prompt policies to the tool it dispatches into. */
|
|
968
969
|
policyKey?: string;
|
|
969
970
|
};
|
|
970
|
-
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
971
|
+
export type ToolApproval = ToolApprovalDecision | ((args: unknown, context?: AgentToolContext) => ToolApprovalDecision);
|
|
971
972
|
/**
|
|
972
973
|
* Context passed to tool execution.
|
|
973
974
|
* Apps can extend via declaration merging.
|
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.15.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.15.0",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.15.0",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.15.0",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.15.0",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.15.0",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.15.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.15.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"
|
package/src/agent-loop.ts
CHANGED
|
@@ -3402,7 +3402,7 @@ async function executeToolCalls(
|
|
|
3402
3402
|
// also cascades: the freshly detached job's own completion re-triggers
|
|
3403
3403
|
// this check for the next command, so millisecond-long commands chain
|
|
3404
3404
|
// into separate background deliveries (#12869).
|
|
3405
|
-
if (source === "irc" && softInterrupts) steeringSoftController.abort();
|
|
3405
|
+
if (source === "irc" && softInterrupts) steeringSoftController.abort("irc");
|
|
3406
3406
|
};
|
|
3407
3407
|
|
|
3408
3408
|
const checkSteering = async (): Promise<void> => {
|
|
@@ -3441,7 +3441,7 @@ async function executeToolCalls(
|
|
|
3441
3441
|
interruptState.triggered = true;
|
|
3442
3442
|
interruptState.source = steeringSource ?? "unknown";
|
|
3443
3443
|
steeringAbortController.abort(TOOL_INTERRUPT_ABORT_REASON);
|
|
3444
|
-
if (softInterrupts) steeringSoftController.abort();
|
|
3444
|
+
if (softInterrupts) steeringSoftController.abort(interruptState.source);
|
|
3445
3445
|
}
|
|
3446
3446
|
return;
|
|
3447
3447
|
}
|
package/src/agent.ts
CHANGED
|
@@ -176,6 +176,11 @@ export interface AgentOptions {
|
|
|
176
176
|
* Shared provider state map for session-scoped transport/session caches.
|
|
177
177
|
*/
|
|
178
178
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
179
|
+
/**
|
|
180
|
+
* Tool definitions already sent, for an Agent that replays another Agent's request history
|
|
181
|
+
* (see {@link Agent.copySentToolDefinitions}). Defaults to an empty store.
|
|
182
|
+
*/
|
|
183
|
+
sentToolDefinitions?: SentToolDefinitions;
|
|
179
184
|
|
|
180
185
|
/**
|
|
181
186
|
* Resolves an API key or resolver dynamically for each LLM call.
|
|
@@ -568,6 +573,7 @@ export class Agent {
|
|
|
568
573
|
this.#deadline = opts.deadline;
|
|
569
574
|
this.#promptCacheKey = opts.promptCacheKey;
|
|
570
575
|
this.#providerSessionState = opts.providerSessionState;
|
|
576
|
+
if (opts.sentToolDefinitions) this.#sentToolDefinitions = opts.sentToolDefinitions;
|
|
571
577
|
this.#thinkingBudgets = opts.thinkingBudgets;
|
|
572
578
|
this.#temperature = opts.temperature;
|
|
573
579
|
this.#topP = opts.topP;
|
|
@@ -642,6 +648,14 @@ export class Agent {
|
|
|
642
648
|
this.#promptCacheKey = value;
|
|
643
649
|
}
|
|
644
650
|
|
|
651
|
+
/**
|
|
652
|
+
* A detached copy of the tool definitions this agent has sent (`AgentOptions.sentToolDefinitions`), without the
|
|
653
|
+
* names `keep` rejects.
|
|
654
|
+
*/
|
|
655
|
+
copySentToolDefinitions(keep?: (name: string) => boolean): SentToolDefinitions {
|
|
656
|
+
return this.#sentToolDefinitions.copy(keep);
|
|
657
|
+
}
|
|
658
|
+
|
|
645
659
|
/**
|
|
646
660
|
* Fields added to every log record written while this agent runs (the
|
|
647
661
|
* owning session's `sessionId`), so in-process agents' records stay apart.
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
createFileOps,
|
|
29
29
|
extractFileOpsFromMessage,
|
|
30
30
|
type FileOperations,
|
|
31
|
+
incompleteOutputError,
|
|
31
32
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
32
33
|
serializeConversationForSummary,
|
|
33
34
|
stripReadSelector,
|
|
@@ -386,6 +387,9 @@ export async function generateBranchSummary(
|
|
|
386
387
|
if (response.stopReason === "error") {
|
|
387
388
|
return { error: response.errorMessage || "Summarization failed" };
|
|
388
389
|
}
|
|
390
|
+
if (response.stopReason === "length") {
|
|
391
|
+
return { error: incompleteOutputError("Summarization failed") };
|
|
392
|
+
}
|
|
389
393
|
|
|
390
394
|
let summary = response.content
|
|
391
395
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
escapeSummaryBoundaryTags,
|
|
92
92
|
extractFileOpsFromMessage,
|
|
93
93
|
type FileOperations,
|
|
94
|
+
incompleteOutputError,
|
|
94
95
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
95
96
|
serializeConversationForSummary,
|
|
96
97
|
stripReadSelector,
|
|
@@ -1075,6 +1076,9 @@ async function summarizeConversationWindow(
|
|
|
1075
1076
|
if (response.stopReason === "error") {
|
|
1076
1077
|
throw createSummarizationError("Summarization failed", response);
|
|
1077
1078
|
}
|
|
1079
|
+
if (response.stopReason === "length") {
|
|
1080
|
+
throw new Error(incompleteOutputError("Summarization failed"));
|
|
1081
|
+
}
|
|
1078
1082
|
|
|
1079
1083
|
const textContent = response.content
|
|
1080
1084
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
@@ -1182,6 +1186,9 @@ export async function generateHandoffFromContext(
|
|
|
1182
1186
|
if (response.stopReason === "error") {
|
|
1183
1187
|
throw createSummarizationError("Handoff generation failed", response);
|
|
1184
1188
|
}
|
|
1189
|
+
if (response.stopReason === "length") {
|
|
1190
|
+
throw new Error(incompleteOutputError("Handoff generation failed"));
|
|
1191
|
+
}
|
|
1185
1192
|
|
|
1186
1193
|
return response.content
|
|
1187
1194
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
@@ -1305,8 +1312,10 @@ async function generateShortSummary(
|
|
|
1305
1312
|
case "error":
|
|
1306
1313
|
outcome = { kind: "failed", error: response.errorMessage || "Unknown error" };
|
|
1307
1314
|
break;
|
|
1308
|
-
case "stop":
|
|
1309
1315
|
case "length":
|
|
1316
|
+
outcome = { kind: "failed", error: incompleteOutputError("Short summary failed") };
|
|
1317
|
+
break;
|
|
1318
|
+
case "stop":
|
|
1310
1319
|
case "toolUse":
|
|
1311
1320
|
outcome = {
|
|
1312
1321
|
kind: "text",
|
|
@@ -2435,6 +2444,9 @@ async function generateTurnPrefixSummary(
|
|
|
2435
2444
|
if (response.stopReason === "error") {
|
|
2436
2445
|
throw createSummarizationError("Turn prefix summarization failed", response);
|
|
2437
2446
|
}
|
|
2447
|
+
if (response.stopReason === "length") {
|
|
2448
|
+
throw new Error(incompleteOutputError("Turn prefix summarization failed"));
|
|
2449
|
+
}
|
|
2438
2450
|
|
|
2439
2451
|
return response.content
|
|
2440
2452
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
package/src/compaction/openai.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
getOpenAIResponsesHistoryItems,
|
|
46
46
|
getOpenAIResponsesHistoryPayload,
|
|
47
47
|
normalizeResponsesToolCallId,
|
|
48
|
+
stripOpenAIResponsesInternalMetadataForReplay,
|
|
48
49
|
stripOpenAIResponsesOutputOnlyStatusesForReplay,
|
|
49
50
|
truncateResponseItemId,
|
|
50
51
|
} from "@oh-my-pi/pi-ai/utils";
|
|
@@ -846,6 +847,9 @@ export function buildOpenAiNativeHistory(
|
|
|
846
847
|
msgIndex++;
|
|
847
848
|
}
|
|
848
849
|
|
|
850
|
+
for (let index = 0; index < input.length; index++) {
|
|
851
|
+
input[index] = stripOpenAIResponsesInternalMetadataForReplay(input[index]!);
|
|
852
|
+
}
|
|
849
853
|
return stripOpenAIResponsesOutputOnlyStatusesForReplay(hoistInterleavedResponsesToolBatchMessages(input));
|
|
850
854
|
}
|
|
851
855
|
|
package/src/compaction/utils.ts
CHANGED
|
@@ -28,6 +28,11 @@ export function createFileOps(): FileOperations {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Error text for a summarization call that hit its output limit (`stopReason: "length"`): partial text is never used. */
|
|
32
|
+
export function incompleteOutputError(prefix: string): string {
|
|
33
|
+
return `${prefix}: model output ended incomplete (stopReason: length)`;
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
// Read-tool selector grammar, mirrored from the conservative filesystem splitter in
|
|
32
37
|
// packages/coding-agent/src/tools/path-utils.ts (splitPathAndSel). Keep in sync.
|
|
33
38
|
// A trailing `:chunk` is a selector only when it is a line-range list
|
|
@@ -13,6 +13,16 @@ export class SentToolDefinitions {
|
|
|
13
13
|
for (const tool of tools) this.#byName.set(tool.name, tool);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A detached copy: an Agent that replays another's request history declares the same withdrawn tools. `keep`
|
|
18
|
+
* leaves out a definition the copy must never declare.
|
|
19
|
+
*/
|
|
20
|
+
copy(keep: (name: string) => boolean = () => true): SentToolDefinitions {
|
|
21
|
+
const copy = new SentToolDefinitions();
|
|
22
|
+
copy.record([...this.#byName.values()].filter(tool => keep(tool.name)));
|
|
23
|
+
return copy;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
/**
|
|
17
27
|
* `context` with {@link Context.inactiveTools} set to the definitions for names the latest
|
|
18
28
|
* `requestControls.tools.declared` in its messages holds that are not in its `tools`; cleared when
|
package/src/types.ts
CHANGED
|
@@ -672,7 +672,8 @@ export interface ToolCallContext {
|
|
|
672
672
|
providerMetadata?: ToolCallProviderMetadata;
|
|
673
673
|
/**
|
|
674
674
|
* Cooperative steering signal: aborted when a queued user/steering message
|
|
675
|
-
* (or an interrupting peer IRC) is detected while this tool batch runs.
|
|
675
|
+
* (or an interrupting peer IRC) is detected while this tool batch runs. Its
|
|
676
|
+
* `reason` names the source: a {@link SteeringInterruptSource} or `"irc"`.
|
|
676
677
|
* Unlike the hard abort signal it NEVER kills the tool — long-running
|
|
677
678
|
* tools MAY observe it (via `ctx.toolCall.steeringSignal`) to finish early
|
|
678
679
|
* or background themselves so the message injects promptly; ignoring it is
|
|
@@ -1086,7 +1087,7 @@ export type ToolApprovalDecision =
|
|
|
1086
1087
|
* prompt policies to the tool it dispatches into. */
|
|
1087
1088
|
policyKey?: string;
|
|
1088
1089
|
};
|
|
1089
|
-
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
1090
|
+
export type ToolApproval = ToolApprovalDecision | ((args: unknown, context?: AgentToolContext) => ToolApprovalDecision);
|
|
1090
1091
|
|
|
1091
1092
|
/**
|
|
1092
1093
|
* Context passed to tool execution.
|