DeepSeek Harness Gemini 400: $defs $ref replay
The tool call itself succeeds; the bad part is the JSON it writes into history: cordis_inspect_query returns a config schema containing $defs, and on the next turn, when the result is replayed as a function_response, Gemini reads $ref as a reference to a multimodal attachment display_name, fails to find one, and returns 400. Every turn after that fails too. According to the discussion, the reporter reproduced this in the desktop app, and community members reviewed the tool-cordis source and return shape (discussion).
Which DeepSeek Harness turn starts returning the Gemini 400
The decisive pattern is "the tool succeeded, the next turn returns 400, a new session recovers"; when all three hold, history contamination is the likely cause. Reproduction and observation order:
- Switch the model to Gemini. Expected: requests use the native structured
function_responsepath rather than plain-text tool results. - Have the agent call
cordis_inspect_queryfor an item such asConfig.listConfigsthat returns a schema with aloaderExpressionreference. Expected: the tool result contains a$defsobject. - Observe that turn. Expected: the tool call succeeds with no error.
- Continue to the next turn. Expected:
400: {
"message": "The referenced name `#/$defs/loaderExpression` in function_response.response does not match to a display_name in the function_response.parts.",
"type": "upstream_error",
"code": 400
}
- Switch models or retry in the same session. Expected: still 400. Create a new session. Expected: normal behavior.
Why DeepSeek Harness hands Gemini a $ref named display_name
Gemini scans function_response.response for $ref values and looks them up as attachment names in parts; #/$defs/loaderExpression is a document-local pointer, not an attachment name, so the lookup cannot succeed. The source reviewed by the community is packages/extensions/tool-cordis/src/config.ts:117:
return toJson({
...listed,
acceptsMissing,
limitations,
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$defs: { ...definitions, loaderExpression: LOADER_EXPRESSION_SCHEMA },
...schema,
},
})
Three steps explain it:
- The tool returns the full projected schema as its result, with
$defscontaining both the config projector'sdefinitionsandloaderExpression. - The result is written into session history with no validation, because it is valid JSON.
- On the next turn the
$refis replayed verbatim insidefunction_response; Gemini fails to resolve it as an attachment name and returns 400. History only grows, so the error persists.
LOADER_EXPRESSION_SCHEMA comes from @deepseek-ai/dsh-app-boot, but inlining must not handle only that one: entries in definitions can also contain references. This is not the same trigger as Gemini thought signature 400; that one breaks on signature replay, while this one breaks on $ref inside a tool result.
DSH plugin fixes: inline, strip, or change the contract
All three paths stop the next turn from returning 400; they differ in whether you change the returned data, the tool contract, or just the history path. If you wrap this query in your own DSH plugin, the same fixes apply; prefer inlining for same-day relief and contract changes for team-level cleanup.
Path 1: inline $ref before toJson
This is the fastest stopgap with the least semantic change: replace each $ref with the object it points to. The output grows slightly, and Gemini still receives the full schema.
- Before the
toJson()call, walk theschemadeeply. Expected: every node in the schema is visited. - When a
$refstarts with#/$defs/, substitute the matching definition and enforce a recursion limit. Expected: references are replaced and recursion stops at the configured depth. - After inlining, delete the top-level
$defs. Expected: no$refbeginning with#/remains in the returned object. - Rerun the same query. Expected: the next turn no longer returns 400.
Illustrative code (not the repository's original):
const deref = (node: any, defs: any): any =>
Array.isArray(node) ? node.map((n) => deref(n, defs))
: node?.$ref?.startsWith('#/$defs/') ? deref(defs[node.$ref.slice(8)], defs)
: node && typeof node === 'object'
? Object.fromEntries(Object.entries(node).map(([k, v]) => [k, deref(v, defs)]))
: node
Path 2: strip the schema or serialize it as a string
If consumers do not need to read the schema as a structure, the simplest option is to keep it from entering function_response as an object.
- Confirm which callers actually read the
schemafield. Expected: you get a list of callers that depend on structural access. - Remove
schemafrom the returned object, or replace it withJSON.stringify(schema)while keeping the$schemaversion and summary fields. Expected: the result no longer carries a nested schema object. - Rerun the query. Expected: still valid JSON, but no nested
$refinsidefunction_response.response. - Check callers that rely on
response.schema.$defs. Expected: those callers parse the string and still work.
The cost is structural access: consumers that need a field must parse it again.
Path 3: stop returning $defs from the query
Moving the schema out of "data" and back into "tool description" is the more correct direction, but it changes the contract, not one line of code.
- Make
cordis_inspect_queryreturn only a summary of config items plus a handle for follow-up queries. Expected: the result contains no schema. - Put the schema in the tool definition or a separate type-query endpoint, fetched on demand. Expected: the schema has a dedicated lookup path.
- Update every caller. Expected: the code compiles without reading
schemafrom the result. - Rerun the full flow with Gemini. Expected: no tool result containing
$defsenters history, and other providers still read the summary and handle correctly.
This has the highest correctness and the highest rollout cost. Treat it as follow-up cleanup, not same-day mitigation.
What to do with a poisoned DeepSeek Harness session
There is no official recovery: the bad result is already in persistent history, and a retry will not remove it. Order of operations:
- Start a new session and continue there. Expected: no more 400.
- If the old session has content you must keep, export the text and move it by hand. Expected: the new session contains only the text you chose to carry over.
- Do not hand-edit
session.v4.jsonl.zstd. Expected: the compressed log stays valid and checksum-verifiable, which the edit would break. - If session rollback is available, roll back to before the
cordis_inspect_querycall. Expected: the session becomes usable again.
Retest and boundaries for the DeepSeek Harness fix
Testing "this turn does not error" is not enough; test that "no result containing $ref can enter history again". Steps:
- Call
cordis_inspect_querytwice in a row under Gemini. Expected: both turns succeed. - Inspect that tool result in the session log. Expected: no
$refbeginning with#/. - Use an ordinary query without
$defsas a control. Expected: behavior matches the pre-fix baseline; run the same query under a non-Gemini provider and the tool result is still readable.
There is one boundary: only the native structured function_response path is sensitive. If results were ever passed as plain text the problem would vanish, but so would structured access.
FAQ
DeepSeek Harness writes a JSON Schema containing $defs into session history as a tool result. On the next turn it is replayed as a function_response; Gemini reads $ref as a reference to a multimodal attachment display_name, cannot find it, and returns 400. History only grows, so every later turn fails.
Inlining $ref in DeepSeek Harness preserves the full structure and merely removes references, so consumers read the same schema. Deleting the schema turns the result into a summary or handle, reduces semantic information, and forces every caller that depends on the field to change.
DeepSeek Harness has no official recovery: the bad result is already in persistent history, and neither a retry nor a model switch removes it. Start a new session or roll back to history before the poisoning; hand-editing the session file is high risk.
Related Terms
- JSON Schema $defs
- `$defs` is the container for reusable subschemas in JSON Schema 2020-12, referenced elsewhere through `$ref`. Inside data it is ordinary JSON, but a specific API may attach extra semantics to it.— https://json-schema.org/draft/2020-12/json-schema-core
- function_response
- `function_response` is the structure that returns a tool result in Gemini's native function-calling protocol, paired with `function_call`. Its `response` field is a structured object parsed by the server according to protocol rules.— https://ai.google.dev/gemini-api/docs/function-calling
- display_name
- `display_name` names an attachment in Gemini multimodal parts so the model or server can refer to it. The protocol allows a `function_response` to reference an attachment by name, which explains why `$ref` is misread.— https://ai.google.dev/gemini-api/docs/function-calling
Sources
- deepseek-harness Discussion #10167: Gemini 3.x returns 400 permanently after calling cordis_inspect_query· deepseek-ai (GitHub Discussions)
- deepseek-harness repository: review of the return value in tool-cordis config.ts· deepseek-ai (GitHub)