Skip to content

feat(x_search): add structured output and response chaining#27416

Open
valda wants to merge 2 commits into
NousResearch:mainfrom
valda:feat/x-search-structured-output
Open

feat(x_search): add structured output and response chaining#27416
valda wants to merge 2 commits into
NousResearch:mainfrom
valda:feat/x-search-structured-output

Conversation

@valda

@valda valda commented May 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Extend the built-in x_search tool with four parameters that expose additional xAI Responses API capabilities:

  • output_schema — strict JSON Schema output, expanded into text.format.json_schema.
  • instructions — top-level system-style instructions controlling response style.
  • previous_response_id — chain a follow-up call from a stored prior response.
  • store — whether xAI stores the response so its id can be reused later. Defaults to false.

The success payload now includes:

  • response_id — the xAI response id, for follow-up chaining.
  • structured_output — parsed JSON when output_schema is set and parsing succeeds, otherwise null.

Existing fields, error shape, credential resolution, retry behavior, and citation extraction are unchanged.

Why this matters for agent loops

x_search already returns an LLM-summarized answer with citations. But agent workflows often need typed fields — extracted items, classification labels, confidence annotations, or alert decisions — that downstream steps can consume without sending the prose through another LLM pass to recover the shape.

With output_schema, a single x_search call can return a strictly-shaped JSON object directly. The schema becomes an explicit tool contract rather than an implicit prompt convention, which matters most for skills and cron jobs that run the same query repeatedly: they can declare the expected shape once and rely on it across invocations, instead of re-coaxing it out of free-form text each time.

Combined with previous_response_id, follow-up calls can refine the same search while keeping the structured shape stable across turns.

This is additive: callers that do not pass output_schema keep the existing conversational response shape.

Scope

This PR intentionally bundles the four new parameters together because they share the same tool schema, request payload construction, response parsing, and tests. Splitting them would create overlapping changes in tools/x_search_tool.py without making review materially easier.

Out of scope:

  • Per-call model override.
  • Adding a JSON Schema validator dependency.
  • Adding a warnings field to tool responses.

Behavior

  • store=false remains the default to preserve current privacy semantics.
  • Callers should set store=true on the initial/seed call when they plan to reuse its response_id with previous_response_id.
  • When previous_response_id is provided, the request is stored automatically so chained follow-up responses can themselves be reused.
  • instructions and previous_response_id are mutually exclusive per the xAI Responses API. If both are provided, instructions is ignored and a warning is logged. previous_response_id wins because it is required to preserve the requested response chain.
  • response_id is only included on the success path. Failure responses keep the existing error shape.
  • structured_output is always present on the success path:
    • null when output_schema is not provided.
    • Parsed JSON when output_schema is provided and parsing succeeds.
    • null when parsing fails, while preserving the raw answer and keeping the tool call successful.

Testing

  • Added tests for request payload construction:
    • output_schema expands into text.format.json_schema.
    • instructions is added as a top-level field.
    • previous_response_id is added as a top-level field.
    • instructions is dropped when previous_response_id is also provided.
    • store defaults to false.
    • store=true is honored.
    • previous_response_id automatically enables store.
  • Added tests for response handling:
    • data.id is returned as response_id on success.
    • Valid JSON text is parsed into structured_output.
    • Invalid JSON text leaves structured_output as null without failing the tool call.
  • Added validation coverage:
    • output_schema must be a JSON object.
    • Blank instructions and previous_response_id values are omitted from the request body.
$ pytest tests/tools/test_x_search_tool.py
............................                                             [100%]
24 passed in 1.52s

Dogfood

Verified against the live xAI Responses API:

import json
from tools.x_search_tool import _handle_x_search

# Seed call: structured output + store=true so we can chain a follow-up.
r1 = json.loads(_handle_x_search({
    "query": "Pick 3 recent popular posts mentioned Hermes Agent within the last 24 hours.",
    "output_schema": {
        "type": "object",
        "properties": {
            "tweets": {"type": "array", "items": {
                "type": "object",
                "properties": {
                    "handle": {"type": "string"},
                    "summary": {"type": "string"},
                    "sentiment": {
                        "type": "string",
                        "enum": ["positive", "neutral", "negative"],
                        "description": "Overall sentiment of the post",
                    },
                },
                "required": ["handle", "summary", "sentiment"],
            }},
        },
        "required": ["tweets"],
    },
    "instructions": "Respond in Japanese. Be concise.",
    "store": True,
}))
print("*** First call with structured output and store=true ***")
print("response_id      :", r1["response_id"])
print("structured type  :", type(r1["structured_output"]).__name__)
print("structured keys  :", list(r1["structured_output"].keys()))
print("inline citations :", len(r1["inline_citations"]))
for i, tweet in enumerate(r1["structured_output"]["tweets"]):
    print(f"post {i} handle  :", tweet["handle"])
    print(f"post {i} summary :", tweet["summary"][:300])
    print(f"post {i} sentiment:", tweet["sentiment"])

# Chained follow-up: reuses the prior response via previous_response_id.
# instructions is intentionally omitted (would be dropped anyway).
r2 = json.loads(_handle_x_search({
    "query": "Which of those posts likely has the highest engagement?",
    "previous_response_id": r1["response_id"],
}))
print("*** Follow-up call reusing prior response ***")
print("success :", r2["success"])
print("answer  :", r2["answer"][:300])

Output (handles anonymized):

*** First call with structured output and store=true ***
response_id      : 8723063b-b760-9dcc-b094-9b0a65226888
structured type  : dict
structured keys  : ['tweets']
inline citations : 24
post 0 handle  : @user_a
post 0 summary : Hermes Agentは手間なく自己改善し、繰り返し作業をSkills化して進化。X Premiumだけで動かせる点が良い。
post 0 sentiment: positive
post 1 handle  : @user_a
post 1 summary : Claude CodeやCodexからHermes Agentを呼び、Xリサーチ→URL抽出→Excelまとめが可能。シンプルに価値大。
post 1 sentiment: positive
post 2 handle  : @user_b
post 2 summary : Hermes AgentがOpenAI Codex CLI経由で利用可能に。ChatGPTサブスクをエージェント実行枠に変える革新的機能。
post 2 sentiment: positive
*** Follow-up call reusing prior response ***
success : True
answer  : **@user_aのXリサーチ活用投稿(Views: 4186、Likes: 31、Bookmarks: 32)** が最もエンゲージメントが高いです。ViewsとBookmarksが他を上回っています。

This single round-trip confirms:

  • response_id is a non-null UUID — proves store=True took effect on the seed call.
  • structured_output is a parsed dict matching the declared schema.
  • The chained call returns success=true and references the prior turn's posts by handle — proves the response was actually re-used by the API, not just acknowledged.
  • inline_citations is populated even when top-level citations is empty.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets provider/xai xAI (Grok) labels May 17, 2026
@valda valda force-pushed the feat/x-search-structured-output branch from 67bcca7 to 3f72b51 Compare May 18, 2026 00:34
@valda valda force-pushed the feat/x-search-structured-output branch from 3f72b51 to a166a06 Compare May 21, 2026 01:07
valda added 2 commits May 24, 2026 13:04
Extend the built-in x_search tool with four parameters that expose
additional capabilities of the xAI Responses API:

- output_schema: expanded to text.format.json_schema (strict=true)
- instructions: top-level system instructions for response style
- previous_response_id: continue from a prior call's response
- store: whether xAI retains the response (default false)

Auto-enable store when previous_response_id is provided so chains
work without callers tracking both flags. instructions and
previous_response_id are mutually exclusive per the xAI Responses
API — when both are set, instructions is dropped with a warning log
rather than raising, to avoid raise-and-retry loops in upstream LLM
callers.

The success payload gains response_id (from data["id"]) and
structured_output (parsed JSON when output_schema is set, None
otherwise). Existing fields (answer, citations, inline_citations,
credential_source, etc.) are preserved unchanged, and failure
payloads keep their current shape.

Authentication, retry, structured errors, and citation extraction
are unchanged; only the request shape and response parsing are
extended.
Add 11 unit tests for the new parameters:

- output_schema expands to text.format.json_schema (strict=true)
- instructions lands at the top level of the request body
- previous_response_id lands at the top level and auto-enables store
- instructions is dropped when previous_response_id is also set
- store defaults to False; explicit store=True is honored
- response_id surfaces from data["id"] on success
- structured_output is None without a schema
- structured_output is None on JSON parse failure (success stays True)
- non-object output_schema returns a tool_error
- whitespace-only instructions / previous_response_id are omitted

The 13 existing tests are unchanged and still pass.
@valda valda force-pushed the feat/x-search-structured-output branch from a166a06 to 5103c42 Compare May 24, 2026 04:05
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused x_search enhancement. I reviewed the diff against the current checkout and did not find a concrete blocking issue from static inspection.

Current main still lacks the requested surface: tools/x_search_tool.py:274 defines x_search_tool without output_schema, instructions, previous_response_id, or store; tools/x_search_tool.py:325 hard-codes "store": False; and tools/x_search_tool.py:455 exposes none of those fields in X_SEARCH_SCHEMA. The PR diff adds request construction, success-payload parsing, and tests for those paths.

Staleness looks mechanical: gh pr diff 27416 --repo NousResearch/hermes-agent --patch | git apply --check --verbose - succeeds on the current checkout with only one-line offsets in tools/x_search_tool.py.

I did not run pytest or live xAI calls because this sweeper pass is read-only, so this is a static review only.

Automated hermes-sweeper review.

@valda

valda commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I use this feature daily in my own Hermes workflow, especially structured output and response chaining for x_search-based reporting, so it would be very helpful if this could be merged or salvaged into main.

I’m happy to rebase/update the PR and run the relevant tests if that would help.

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

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have provider/xai xAI (Grok) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants