mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-10 02:17:22 +08:00
Desktop ships upstream rc packages through Yarn patch resolutions, so the empty-name guard belongs in the patched agent loop rather than in a desktop-only wrapper. The new regression loads the installed loop implementation, exercises an empty-name tool call directly, and proves the turn now ends after one synthetic error with new-session guidance instead of entering normal unknown-tool dispatch. Constraint: Desktop consumes upstream Harness rc packages through local Yarn patches Rejected: Patch dsh-llm-deepseek again | adapter already preserves prior non-empty tool-call deltas, but finalized empty names still need loop-level termination Rejected: Treat all unknown tools as terminal | would broaden behavior beyond the proxy-corruption case from issue #157 Confidence: high Scope-risk: narrow Directive: Keep the empty-name guard in the agent loop until upstream ships an rc that closes the loop at source Tested: yarn workspace dsh-plugin-desktop vitest run tests/empty-tool-call-loop.spec.ts Tested: yarn workspace dsh-plugin-desktop vitest run tests/deepseek-streaming-tool-call.spec.ts tests/empty-tool-call-loop.spec.ts Tested: yarn workspace dsh-plugin-desktop vitest run tests/package.spec.ts -t 'patches the agent loop to stop after one empty-name tool call' Tested: yarn workspace dsh-plugin-desktop typecheck Tested: yarn workspace dsh-plugin-desktop build
50 lines
2.2 KiB
Diff
50 lines
2.2 KiB
Diff
diff --git a/lib/index.js b/lib/index.js
|
|
index 17e11c9df8..597c77594b 100644
|
|
--- a/lib/index.js
|
|
+++ b/lib/index.js
|
|
@@ -114,6 +114,24 @@ var RuntimeContextProjection = class {
|
|
* @param signal - abort signal shared by the step.
|
|
* @param acceptContext - accepts committed result context for the next step boundary.
|
|
*/
|
|
+const EMPTY_TOOL_NAME_ERROR = "assistant emitted a tool call with an empty tool name";
|
|
+const EMPTY_TOOL_NAME_RESULT = "Error: assistant emitted a tool call with an empty tool name. This usually means an upstream proxy or cached long session dropped tool_call.name/id. Start a new session or use the official DeepSeek API directly.";
|
|
+function hasEmptyToolCallName(block) {
|
|
+ return typeof block.name === "string" && block.name.trim() === "";
|
|
+}
|
|
+function appendEmptyToolCallFailure(session, turn, step, block) {
|
|
+ const callSeq = appendToolCall(session, turn, step, block);
|
|
+ appendToolResult(session, turn, step, block, {
|
|
+ content: [{
|
|
+ type: "text",
|
|
+ text: EMPTY_TOOL_NAME_RESULT
|
|
+ }],
|
|
+ isError: true,
|
|
+ error: {
|
|
+ message: EMPTY_TOOL_NAME_ERROR
|
|
+ }
|
|
+ }, callSeq);
|
|
+}
|
|
async function executeToolCalls(ctx, turn, step, toolCalls, signal, acceptContext) {
|
|
const agent = ctx.agents.requireInitiator();
|
|
const { session } = agent;
|
|
@@ -131,8 +149,18 @@ async function executeToolCalls(ctx, turn, step, toolCalls, signal, acceptContex
|
|
let concluded = false;
|
|
while (next < planned.length) {
|
|
const first = planned[next];
|
|
+ if (hasEmptyToolCallName(first.block)) {
|
|
+ appendEmptyToolCallFailure(session, turn, step, first.block);
|
|
+ return { concluded: true };
|
|
+ }
|
|
const mode = ctx.tools.executionMode(first.exec).kind;
|
|
- const outcome = await runGroup(ctx, turn, step, mode === "parallel" ? planned.slice(next) : [first], mode, signal, acceptContext);
|
|
+ const group = [first];
|
|
+ if (mode === "parallel") while (next + group.length < planned.length) {
|
|
+ const candidate = planned[next + group.length];
|
|
+ if (hasEmptyToolCallName(candidate.block)) break;
|
|
+ group.push(candidate);
|
|
+ }
|
|
+ const outcome = await runGroup(ctx, turn, step, group, mode, signal, acceptContext);
|
|
next += outcome.consumed;
|
|
concluded ||= outcome.concluded;
|
|
if (outcome.aborted) {
|