@oh-my-pi/pi-agent-core 18.5.1 → 18.6.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 +6 -0
- package/package.json +8 -8
- package/src/agent-loop.ts +47 -0
- package/src/prompts/dsml-tool-call-leak.md +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.6.0] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- When a DeepSeek turn ends with raw `<|DSML|…>` tool-call text that could not be parsed into a real call, and no tool call was made, the broken markup is now removed from the message before it is saved to history. The agent then tells the model the tool call failed, shows it the correct format, and asks it again, at most twice in a row. Previously the markup stayed in history, and the agent stopped as if the model had finished ([#14202](https://github.com/can1357/oh-my-pi/pull/14202) by [@H4vC](https://github.com/H4vC)).
|
|
10
|
+
|
|
5
11
|
## [18.5.1] - 2026-10-03
|
|
6
12
|
|
|
7
13
|
### Added
|
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.6.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.6.0",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.6.0",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.6.0",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.6.0",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.6.0",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.6.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.6.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
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
recoverHarmonyToolCall,
|
|
54
54
|
signalListLabel,
|
|
55
55
|
} from "@oh-my-pi/pi-ai/utils/harmony-leak";
|
|
56
|
+
import { isDsmlLeakRecoveryTarget, removeDsmlToolMarkupLeak } from "@oh-my-pi/pi-ai/utils/dsml-leak";
|
|
56
57
|
import { cloneJsonTree, logger, sanitizeText, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
57
58
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
58
59
|
import { LiveSteeringChannel } from "./live-steering";
|
|
@@ -76,6 +77,7 @@ import {
|
|
|
76
77
|
startInvokeAgentSpan,
|
|
77
78
|
} from "./telemetry";
|
|
78
79
|
import { createAdditionalContextMessage, isNonBlankContext, joinAdditionalContext } from "./tool-context";
|
|
80
|
+
import dsmlToolCallLeakPrompt from "./prompts/dsml-tool-call-leak.md" with { type: "text" };
|
|
79
81
|
import type {
|
|
80
82
|
AgentContext,
|
|
81
83
|
AgentEvent,
|
|
@@ -116,6 +118,14 @@ const ABORTED: unique symbol = Symbol("agent-loop-aborted");
|
|
|
116
118
|
*/
|
|
117
119
|
const MAX_PAUSED_TURN_CONTINUATIONS = 8;
|
|
118
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Cap on consecutive "tool call failed" nudges for a turn that ended with raw
|
|
123
|
+
* DeepSeek DSML tool-call markup in its text and no structured call. A model
|
|
124
|
+
* that keeps leaking after this many corrections is left to yield. Resets
|
|
125
|
+
* whenever a turn carries tool calls.
|
|
126
|
+
*/
|
|
127
|
+
const MAX_DSML_LEAK_NUDGES = 2;
|
|
128
|
+
|
|
119
129
|
/**
|
|
120
130
|
* Cap on consecutive forced escalations for a single soft tool requirement.
|
|
121
131
|
* A forced `toolChoice` guarantees the call, so this is purely defensive: if a
|
|
@@ -1222,6 +1232,7 @@ async function runLoopBody(
|
|
|
1222
1232
|
let harmonyRetryAttempt = 0;
|
|
1223
1233
|
let harmonyTruncateResumeCount = 0;
|
|
1224
1234
|
let pausedTurnContinuations = 0;
|
|
1235
|
+
let dsmlLeakNudges = 0;
|
|
1225
1236
|
|
|
1226
1237
|
// Soft tool requirement lifecycle (reminder then escalation; see SoftToolRequirement).
|
|
1227
1238
|
// The host-owned state survives only a gate stop between Agent.prompt calls.
|
|
@@ -1685,6 +1696,7 @@ async function runLoopBody(
|
|
|
1685
1696
|
|
|
1686
1697
|
if (toolCalls.length > 0) {
|
|
1687
1698
|
pausedTurnContinuations = 0;
|
|
1699
|
+
dsmlLeakNudges = 0;
|
|
1688
1700
|
} else if (
|
|
1689
1701
|
!hasMoreToolCalls &&
|
|
1690
1702
|
message.stopReason === "stop" &&
|
|
@@ -1698,6 +1710,25 @@ async function runLoopBody(
|
|
|
1698
1710
|
// mid-work turn.
|
|
1699
1711
|
pausedTurnContinuations++;
|
|
1700
1712
|
hasMoreToolCalls = true;
|
|
1713
|
+
} else if (
|
|
1714
|
+
!hasMoreToolCalls &&
|
|
1715
|
+
message.stopReason === "stop" &&
|
|
1716
|
+
dsmlLeakMessages.has(message) &&
|
|
1717
|
+
dsmlLeakNudges < MAX_DSML_LEAK_NUDGES
|
|
1718
|
+
) {
|
|
1719
|
+
// DeepSeek wrote its tool call as raw DSML text the healer could not
|
|
1720
|
+
// recover (often missing the opening envelope). The markup was
|
|
1721
|
+
// stripped before commit; without a nudge the loop would yield as if
|
|
1722
|
+
// the model were done, so tell it the call failed and re-sample.
|
|
1723
|
+
const nudge = injectExecutionAdditionalContext(
|
|
1724
|
+
currentContext,
|
|
1725
|
+
newMessages,
|
|
1726
|
+
stream,
|
|
1727
|
+
dsmlToolCallLeakPrompt,
|
|
1728
|
+
);
|
|
1729
|
+
if (nudge) additionalMessages.push(nudge);
|
|
1730
|
+
dsmlLeakNudges++;
|
|
1731
|
+
hasMoreToolCalls = true;
|
|
1701
1732
|
}
|
|
1702
1733
|
|
|
1703
1734
|
await emitTurnEnd(stream, currentContext, message, toolResults, config, signal, {
|
|
@@ -1967,6 +1998,7 @@ async function streamAssistantResponse(
|
|
|
1967
1998
|
// per model without touching the shared session `serviceTier`.
|
|
1968
1999
|
const effectiveServiceTier = config.getServiceTier ? config.getServiceTier(model) : config.serviceTier;
|
|
1969
2000
|
const harmonyMitigationEnabled = isHarmonyLeakMitigationTarget(model);
|
|
2001
|
+
const dsmlLeakRecoveryEnabled = isDsmlLeakRecoveryTarget(model);
|
|
1970
2002
|
const harmonyAbortController = harmonyMitigationEnabled ? new AbortController() : undefined;
|
|
1971
2003
|
const requestSignal = harmonyAbortController
|
|
1972
2004
|
? signal
|
|
@@ -2196,6 +2228,18 @@ async function streamAssistantResponse(
|
|
|
2196
2228
|
}
|
|
2197
2229
|
}
|
|
2198
2230
|
finalMessage = snapshotAssistantMessage(finalMessage);
|
|
2231
|
+
// Unhealed DSML tool-call markup must never reach history: replaying
|
|
2232
|
+
// it teaches the model to keep writing calls as text (#10556). Strip
|
|
2233
|
+
// it before the context, the UI, and the session see the message.
|
|
2234
|
+
// Only DSML-speaking models qualify; from others it is prose.
|
|
2235
|
+
if (
|
|
2236
|
+
dsmlLeakRecoveryEnabled &&
|
|
2237
|
+
finalMessage.stopReason === "stop" &&
|
|
2238
|
+
!finalMessage.content.some(block => block.type === "toolCall") &&
|
|
2239
|
+
removeDsmlToolMarkupLeak(finalMessage)
|
|
2240
|
+
) {
|
|
2241
|
+
dsmlLeakMessages.add(finalMessage);
|
|
2242
|
+
}
|
|
2199
2243
|
// Expand inline macros (and any other registered rewrite) on the
|
|
2200
2244
|
// finalized message before it reaches the context, the UI, or tool
|
|
2201
2245
|
// dispatch — so a single mutation is the source of truth for all three.
|
|
@@ -2752,6 +2796,9 @@ interface PreparedToolCall {
|
|
|
2752
2796
|
prepareError?: unknown;
|
|
2753
2797
|
}
|
|
2754
2798
|
|
|
2799
|
+
/** Committed assistant messages whose unhealed DSML tool-call markup was stripped; the loop answers them with a failure nudge. */
|
|
2800
|
+
const dsmlLeakMessages = new WeakSet<AssistantMessage>();
|
|
2801
|
+
|
|
2755
2802
|
/**
|
|
2756
2803
|
* Prepare results computed in the stream-done branch (before `message_start`/
|
|
2757
2804
|
* `message_end`) so a `beforeToolCall` args revision is baked into the message
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Tool call failed: your previous response tried to invoke a tool, but the DSML markup was malformed, so it arrived as plain text, no tool was executed, and the broken markup was removed from your response. Usually the opening `<|DSML|tool_calls>` and/or `<|DSML|invoke name="…">` tags are missing, or the tool name was written as a plain line.
|
|
2
|
+
|
|
3
|
+
A tool call must be one complete envelope:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
<|DSML|tool_calls>
|
|
7
|
+
<|DSML|invoke name="TOOL_NAME">
|
|
8
|
+
<|DSML|parameter name="ARG_NAME" string="true">text value</|DSML|parameter>
|
|
9
|
+
<|DSML|parameter name="OTHER_ARG" string="false">{"json": "value"}</|DSML|parameter>
|
|
10
|
+
</|DSML|invoke>
|
|
11
|
+
</|DSML|tool_calls>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `<|DSML|tool_calls>` wraps every call; put one `<|DSML|invoke>` per call inside it.
|
|
15
|
+
- The tool name goes in the `name` attribute of `<|DSML|invoke>`, never on its own line.
|
|
16
|
+
- Use `string="true"` for raw text values and `string="false"` for numbers, booleans, arrays, and objects (JSON).
|
|
17
|
+
- Close every tag.
|
|
18
|
+
|
|
19
|
+
Re-issue the intended call now in this shape, or reply in plain prose if no tool is needed.
|