generated from coulomb/repo-seed
Pass --output-format json with --json-schema and unwrap CLI envelope
The Claude Code adapter previously passed --json-schema alone. On Claude
CLI 2.1.160 that combination still emits the model's conversational
preamble on stdout while the schema-enforced structured payload ships on
a sidecar channel the adapter cannot read. Result: callers requesting
structured output got prose that fails JSON parsing downstream — exactly
the failure mode the activity-core CUST-WP-0045 daily triage canary hit
on 2026-06-01 ("Triage report generated and returned via structured
output. Key signals:..." → json.loads error at column 1).
Fix: when --json-schema is set, also pass --output-format json. The CLI
then writes a JSON envelope on stdout. The adapter unwraps it by
probing a small allowlist of known text-bearing fields (result,
result_text, content, text, output). Unknown envelope shapes fall
through to raw stdout so the operator can introspect the structure and
extend the allowlist.
The unwrap path is only triggered when --json-schema was set, so non-
schema callers keep the existing raw-stdout behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -39,15 +39,65 @@ def test_execute_prompt_passes_json_schema_to_claude_cli(monkeypatch):
|
||||
"--print",
|
||||
"--json-schema",
|
||||
'{"type":"object"}',
|
||||
"--output-format",
|
||||
"json",
|
||||
],
|
||||
"input": "Produce a report.",
|
||||
"capture_output": True,
|
||||
"text": True,
|
||||
"timeout": 42,
|
||||
}
|
||||
# Stdout shape that does not match any known envelope field is returned
|
||||
# verbatim so the caller can introspect and we can extend the field list.
|
||||
assert response.content == '{"summary":"ok","recommendations":[]}'
|
||||
|
||||
|
||||
def test_execute_prompt_unwraps_cli_json_envelope_result_field(monkeypatch):
|
||||
"""With --output-format json the CLI wraps the model payload in an
|
||||
envelope. The adapter unwraps the textual result so the caller still
|
||||
sees the model's structured-output JSON, not the envelope."""
|
||||
def fake_run(cmd, input, capture_output, text, timeout): # noqa: ANN001
|
||||
envelope = {
|
||||
"type": "result",
|
||||
"result": '{"summary":"ok","recommendations":[]}',
|
||||
"total_cost_usd": 0.001,
|
||||
}
|
||||
import json as _json
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout=_json.dumps(envelope),
|
||||
stderr="",
|
||||
)
|
||||
|
||||
monkeypatch.setattr("llm_connect.claude_code.subprocess.run", fake_run)
|
||||
adapter = ClaudeCodeAdapter(cli_path="/custom/claude")
|
||||
|
||||
response = adapter.execute_prompt(
|
||||
"Produce a report.",
|
||||
RunConfig(model_params={"json_schema": {"type": "object"}}),
|
||||
)
|
||||
|
||||
assert response.content == '{"summary":"ok","recommendations":[]}'
|
||||
|
||||
|
||||
def test_execute_prompt_no_unwrap_without_json_schema(monkeypatch):
|
||||
"""Without --json-schema we do not pass --output-format json, so the
|
||||
envelope unwrap path stays inert and raw stdout passes through."""
|
||||
def fake_run(cmd, input, capture_output, text, timeout): # noqa: ANN001
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout='{"result":"this is just stdout, not an envelope"}',
|
||||
stderr="",
|
||||
)
|
||||
|
||||
monkeypatch.setattr("llm_connect.claude_code.subprocess.run", fake_run)
|
||||
adapter = ClaudeCodeAdapter(cli_path="/custom/claude")
|
||||
|
||||
response = adapter.execute_prompt("Plain prompt.", RunConfig())
|
||||
|
||||
assert response.content == '{"result":"this is just stdout, not an envelope"}'
|
||||
|
||||
|
||||
def test_claude_code_adapter_prefers_env_cli_path(monkeypatch):
|
||||
monkeypatch.setenv("LLM_CONNECT_CLAUDE_CLI_PATH", "/home/me/bin/claude")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user