Repository navigation
ai-providers: one shared formatter for command results and rejected actions (single fencing call site) #845
Description
Goal
The text that ai-providers builds from a command result, a rejected action and a history entry exists exactly once. Remote output is wrapped by fence_untrusted at a single place, so a future hardening of that wrapping cannot be applied to one provider and forgotten in another.
Context
crates/ai-providers/src/anthropic.rs and crates/ai-providers/src/openai_compatible.rs each carry their own copy of the history formatting:
| Function | anthropic.rs |
openai_compatible.rs |
|---|---|---|
role_str |
:342 | :387 (pub(crate)) |
message_content_text |
:354 | :400 (pub(crate)) |
format_action_rejected |
:384 | :432 |
format_command_result (calls fence_untrusted for stdout and stderr) |
:408–468 | :455–518 |
A token-level comparison shows format_command_result is 98 % identical (42 of 44 code lines); the other three are identical apart from comments, which already say "see identical comment in …". openai_responses.rs:36–39 imports role_str and message_content_text from openai_compatible. fence_untrusted is called at anthropic.rs:453,458 and openai_compatible.rs:506,511.
The fenced format of remote output (<stdout>, <stderr>, <security_notice>, <cancelled_by_user>, <waiting_for_input>, <output_truncated>) is specified in Specs 0013, 0027, 0039 and 0043, and is security invariant 8 in .claude/skills/smart-ssh-coder/references/security-invariants.md.
Requirements
- New module
crates/ai-providers/src/common/prompt_format.rs(withcommon/mod.rs) that holdsrole_str,message_content_text,format_action_rejectedandformat_command_resultonce,pub(crate). anthropic.rs,openai_compatible.rsandopenai_responses.rscall the shared functions; their local copies are deleted.- The produced strings stay byte-identical for every
MessageContentvariant and every combination ofcancelled,waiting_for_inputandoutput.truncated. If the two copies differ in any produced byte today, stop and describe the difference in the PR instead of choosing one silently. - The explanatory comments of both copies are merged into one doc comment on the shared function (no "see identical comment in …" left).
openai_responses.rsno longer importsrole_strormessage_content_textfromopenai_compatible.
Acceptance Criteria
-
grep -rn "fence_untrusted(" crates/ai-providers/src(production code, tests excluded) finds call sites only incommon/prompt_format.rs; no provider file callsfence_untrusteditself. -
grep -rn "fn format_command_result\|fn format_action_rejected\|fn message_content_text\|fn role_str" crates/ai-providers/srcfinds each function once. - New unit tests in
common/prompt_format.rspin the full output offormat_command_result(normal, cancelled, cancelled + waiting for input, truncated, non-UTF-8 output, output that contains a closing</stdout>tag) and offormat_action_rejected(all threeRejectionReasonvariants). - A test asserts that the Anthropic and the OpenAI-compatible request bodies render the same command-result text for the same history entry.
- The existing tests that call
format_command_result(anthropic.rs:~3403,openai_compatible.rs:~2751) still pass, moved or unchanged. - The number of tests in
cargo test -p ai-providers -- --listdoes not decrease. - Gate green:
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace,cargo build --workspace.
Technical Notes
- Security-relevant (fencing of untrusted remote output, review priority elevated): read
security-invariants.md(section "Nicht vertrauenswürdiger Inhalt") and Spec 0039 before changing anything. The wrapping must never get weaker: no tag, notice or escape may be dropped or reordered. - This is the first, low-risk step of consolidating the provider crate. The HTTP retry loop, the frame loop and the max-tokens retry follow in separate issues; do not touch them here.
- Pure move plus deduplication; no change to request bodies, prompts or logging.
- New code comments in English.
Spec impact: none
Open questions
- Module name:
common/prompt_format.rs(recommended, matches the follow-up modulescommon/http_retry.rs,common/frame_loop.rs) orhistory.rsat crate root. Recommendation:common/.
Related Work
- Follow-ups in this series: ai-providers: one shared HTTP send loop with 429/5xx retry for all providers #851, ai-providers: one shared SSE frame loop (FrameHandler) and response-to-event-stream helper #855, ai-providers: shared max-tokens retry, openai_responses without openai_compatible internals, dead budget field removed #860
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
21 remaining items