Skip to content

Replay serverside file and web search#46

Merged
ScriptSmith merged 4 commits into
mainfrom
replay-serverside-tools
Jun 4, 2026
Merged

Replay serverside file and web search#46
ScriptSmith merged 4 commits into
mainfrom
replay-serverside-tools

Conversation

@ScriptSmith
Copy link
Copy Markdown
Owner

No description provided.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 4, 2026

Greptile Summary

This PR adds server-side replay for web_search_call and file_search_call history items. When a follow-up turn echoes these hosted items back, Hadrian now rewrites them into the function_call + function_call_output pairs every provider accepts, replaying the retained replay_content text so the model keeps earlier results in context. A new shared driver (server_tool_history::rewrite_hosted_calls_to_function_pairs) unifies the rewrite logic previously duplicated across MCP, web search, and file search.

  • New WebSearchAction struct adds the spec-required action (query + optional source URLs) and a Hadrian-extension replay_content field to WebSearchCallOutput; both fields are populated on every search and used to reconstruct the function pair on replay.
  • FileSearchResultItem schema change replaces the content: Vec<FileSearchResultContent> array with a flat text: String to match OpenAI's Responses API schema; FileSearchCallOutput gains the same replay_content extension field.
  • Shared rewrite driver (server_tool_history.rs) avoids allocation on the no-match fast path via a position scan, uses exact worst-case capacity, and delegates per-item conversion to each tool's closure.

Confidence Score: 4/5

Safe to merge after addressing the FileSearchResultItem.text backward-compat deserialization failure for stored results.

The FileSearchResultItem.text field has no #[serde(default)], so any stored FileSearchCallOutput persisted with include=["file_search_call.results"] (using the old content array format) will fail to deserialize after this deploy. The failure surfaces at the multi-turn boundary — exactly when the replay machinery this PR introduces is invoked — and produces a hard error rather than degraded output. Everything else in the PR is well-guarded: WebSearchCallOutput.action has #[serde(default)], replay_content on both types has #[serde(default)], and the shared driver correctly avoids allocation on the no-match path.

src/api_types/responses.rs — FileSearchResultItem.text needs #[serde(default)] for backward compatibility with stored results items.

Important Files Changed

Filename Overview
src/api_types/responses.rs Adds WebSearchAction/Source types, action+replay_content to WebSearchCallOutput, and migrates FileSearchResultItem from a content array to a flat text string — the text field lacks #[serde(default)], breaking deserialization of previously stored results.
src/services/server_tool_history.rs New shared driver for 1→2 hosted-call rewrites; scans for the first match before allocating, uses exact worst-case capacity, and passes prefix through untouched — implementation is correct.
src/services/web_search_tool.rs Adds rewrite_web_search_history before the tools early-return, enriches the web_search_call output item with action and replay_content, adds optional source URL population, and refactors format_web_search_call_output_event to take the full item.
src/services/file_search_tool.rs Adds rewrite_file_search_history before the tools early-return and threads replay_content through build_file_search_call_output; schema change to FileSearchResultItem.text inherits the backward-compat risk from responses.rs.
src/services/mcp/preprocess.rs Refactors rewrite_mcp_history to delegate to the shared driver, changing mcp_call_to_function_pair to take &McpCallItem — clean reduction of duplicated rewrite logic.
src/services/responses_chain.rs Doc-comment update and a new round-trip test for web_search_call deserialization with action and replay_content fields — no logic changes.
src/services/mod.rs Adds pub mod server_tool_history to expose the new shared driver.
scripts/openapi-conformance.py Adds two DOCUMENTED_MISSING_FIELDS entries for the moderation field on /chat/completions and /responses — straightforward allowlist update.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Hadrian
    participant Provider
    participant SearchBackend

    Note over Client,SearchBackend: Turn 1 — search executes
    Client->>Hadrian: POST /responses (web_search tool declared)
    Hadrian->>Provider: function tool "web_search" (rewritten)
    Provider-->>Hadrian: "function_call { query }"
    Hadrian->>SearchBackend: execute_search(query)
    SearchBackend-->>Hadrian: results
    Hadrian-->>Client: SSE: web_search_call item (action + replay_content stored)
    Hadrian->>Hadrian: persist response with replay_content

    Note over Client,SearchBackend: Turn 2 — history replay
    Client->>Hadrian: POST /responses (previous_response_id or manual history)
    Note right of Hadrian: input contains WebSearchCall item
    Hadrian->>Hadrian: "rewrite_web_search_history()<br/>WebSearchCall → function_call + function_call_output<br/>(replay_content as output)"
    Hadrian->>Provider: history with function pairs (provider-compatible)
    Provider-->>Hadrian: next response
    Hadrian-->>Client: SSE response
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/api_types/responses.rs:767-769
The schema migration from `content: Vec<FileSearchResultContent>` to `text: String` is not backward-compatible for stored `FileSearchCallOutput` items that previously had `include=["file_search_call.results"]`. Any such item in the DB has `"content": [{"type":"text","text":"..."}]` and no `"text"` key. Because `text` carries no `#[serde(default)]`, serde will fail with "missing field `text`" when deserializing those items, causing the entire `FileSearchCallOutput` — and therefore the multi-turn input payload — to fail. The struct doc-comment also explicitly says "each field is optional in the spec", so the spec allows omitting `text`, yet the Rust type treats it as required. Adding `#[serde(default)]` yields an empty string for old items rather than a panic, matching the same degradation strategy used for `replay_content`.

```suggestion
    /// The text retrieved from the file. OpenAI's Responses API uses a flat
    /// string here (unlike the Assistants API's typed `content` array).
    #[serde(default)]
    pub text: String,
```

Reviews (5): Last reviewed commit: "Review fixes" | Re-trigger Greptile

Comment thread src/services/server_tool_history.rs Outdated
@ScriptSmith
Copy link
Copy Markdown
Owner Author

@greptile-apps

@ScriptSmith
Copy link
Copy Markdown
Owner Author

@greptile-apps

@ScriptSmith ScriptSmith merged commit 5f32880 into main Jun 4, 2026
19 of 20 checks passed
@ScriptSmith ScriptSmith deleted the replay-serverside-tools branch June 4, 2026 12:44
Comment on lines +767 to +769
/// The text retrieved from the file. OpenAI's Responses API uses a flat
/// string here (unlike the Assistants API's typed `content` array).
pub text: String,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The schema migration from content: Vec<FileSearchResultContent> to text: String is not backward-compatible for stored FileSearchCallOutput items that previously had include=["file_search_call.results"]. Any such item in the DB has "content": [{"type":"text","text":"..."}] and no "text" key. Because text carries no #[serde(default)], serde will fail with "missing field text" when deserializing those items, causing the entire FileSearchCallOutput — and therefore the multi-turn input payload — to fail. The struct doc-comment also explicitly says "each field is optional in the spec", so the spec allows omitting text, yet the Rust type treats it as required. Adding #[serde(default)] yields an empty string for old items rather than a panic, matching the same degradation strategy used for replay_content.

Suggested change
/// The text retrieved from the file. OpenAI's Responses API uses a flat
/// string here (unlike the Assistants API's typed `content` array).
pub text: String,
/// The text retrieved from the file. OpenAI's Responses API uses a flat
/// string here (unlike the Assistants API's typed `content` array).
#[serde(default)]
pub text: String,
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/api_types/responses.rs
Line: 767-769

Comment:
The schema migration from `content: Vec<FileSearchResultContent>` to `text: String` is not backward-compatible for stored `FileSearchCallOutput` items that previously had `include=["file_search_call.results"]`. Any such item in the DB has `"content": [{"type":"text","text":"..."}]` and no `"text"` key. Because `text` carries no `#[serde(default)]`, serde will fail with "missing field `text`" when deserializing those items, causing the entire `FileSearchCallOutput` — and therefore the multi-turn input payload — to fail. The struct doc-comment also explicitly says "each field is optional in the spec", so the spec allows omitting `text`, yet the Rust type treats it as required. Adding `#[serde(default)]` yields an empty string for old items rather than a panic, matching the same degradation strategy used for `replay_content`.

```suggestion
    /// The text retrieved from the file. OpenAI's Responses API uses a flat
    /// string here (unlike the Assistants API's typed `content` array).
    #[serde(default)]
    pub text: String,
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant