docs(xai): clarify x_search vs xurl routing without schema cross-refs

Make the x_search / xurl boundary explicit in the skill, feature docs,
toolset metadata, setup note, and reference pages, while keeping the
model-facing x_search schema generic (no static xurl name).

Regression tests assert behavioral routing invariants rather than frozen
prose snapshots. Drop the stale CI-only plugin/hangup hunks already on
main so this rebases cleanly.
This commit is contained in:
Julien Talbot 2026-07-15 00:38:57 +04:00 committed by Teknium
parent b718725121
commit b9b100da11
10 changed files with 174 additions and 15 deletions

View file

@ -475,7 +475,9 @@ TOOL_CATEGORIES = {
"setup_title": "Select xAI Credential Source",
"setup_note": (
"Hermes routes X searches through xAI's built-in x_search "
"Responses tool. Both credential sources hit the same "
"Responses tool for read-only public X discovery. Use the xurl "
"skill for authenticated X API reads and account actions. Both "
"credential sources hit the same "
"https://api.x.ai/v1/responses endpoint — pick whichever you "
"already have. SuperGrok OAuth is preferred when both are set "
"(uses your subscription quota instead of API spend)."

View file

@ -28,6 +28,14 @@ Use this skill for:
- raw access to any X API v2 endpoint
- multi-app / multi-account workflows
If Hermes also exposes the `x_search` tool, route by intent:
- Use `x_search` for read-only public X discovery: "what are people saying", current reactions, public claims, broad semantic search, and synthesized answers with citations.
- Use `xurl` for exact or authenticated X API work: post, reply, quote, delete, like, repost, bookmark, follow, block, mute, DM, media upload, timeline, mentions, account-specific reads, or raw v2 endpoints.
- For mixed workflows, use `x_search` to discover candidate public posts, then use `xurl read` or another exact `xurl` command only after the target post/user/action is clear.
- Never treat an `x_search` answer as evidence that an X write happened. For state-changing X actions, only the `xurl` command output or the X API response proves the action.
- Prefer `x_search` over `xurl search` when the user asks for broad public discussion and does not need X API-exact results or authenticated account context.
This skill replaces the older `xitter` skill (which wrapped a third-party Python CLI). `xurl` is maintained by the X developer platform team, supports OAuth 2.0 PKCE with auto-refresh, and covers a substantially larger API surface.
---
@ -391,12 +399,13 @@ xurl --app staging /2/users/me # one-off against staging
## Agent Workflow
1. Verify prerequisites: `xurl --help` and `xurl auth status`.
2. **Check default app has credentials.** Parse the `auth status` output. The default app is marked with `▸`. If the default app shows `oauth2: (none)` but another app has a valid oauth2 user, tell the user to run `xurl auth default <that-app>` to fix it. This is the most common setup mistake — the user added an app with a custom name but never set it as default, so xurl keeps trying the empty `default` profile.
3. If auth is missing entirely, stop and direct the user to the "One-Time User Setup" section — do NOT attempt to register apps or pass secrets yourself.
4. Start with a cheap read (`xurl whoami`, `xurl user @handle`, `xurl search ... -n 3`) to confirm reachability.
5. Confirm the target post/user and the user's intent before any write action (post, reply, like, repost, DM, follow, block, delete).
6. Use JSON output directly — every response is already structured.
7. Never paste `~/.xurl` contents back into the conversation.
2. Before using `xurl search`, check intent. If the user needs broad public X discovery and the `x_search` tool is available, use `x_search` instead. Continue with `xurl` when the task needs an exact API read, authenticated account context, or any X write action.
3. **Check default app has credentials.** Parse the `auth status` output. The default app is marked with `▸`. If the default app shows `oauth2: (none)` but another app has a valid oauth2 user, tell the user to run `xurl auth default <that-app>` to fix it. This is the most common setup mistake — the user added an app with a custom name but never set it as default, so xurl keeps trying the empty `default` profile.
4. If auth is missing entirely, stop and direct the user to the "One-Time User Setup" section — do NOT attempt to register apps or pass secrets yourself.
5. Start with a cheap read (`xurl whoami`, `xurl user @handle`, `xurl search ... -n 3`) to confirm reachability.
6. Confirm the target post/user and the user's intent before any write action (post, reply, like, repost, DM, follow, block, delete).
7. Use JSON output directly — every response is already structured.
8. Never paste `~/.xurl` contents back into the conversation.
---

View file

@ -0,0 +1,98 @@
"""Behavioral contract for xurl / x_search routing guidance.
These tests assert structural invariants (required topics + mutual exclusivity
of responsibility), not frozen prose snapshots.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
XURL_SKILL = REPO_ROOT / "skills" / "social-media" / "xurl" / "SKILL.md"
X_SEARCH_DOC = REPO_ROOT / "website" / "docs" / "user-guide" / "features" / "x-search.md"
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _contains_any(text: str, *needles: str) -> bool:
lowered = text.lower()
return any(n.lower() in lowered for n in needles)
def test_xurl_skill_routes_by_intent_not_interchangeably():
text = _read(XURL_SKILL)
lowered = text.lower()
# Both surfaces named so agents can choose by capability.
assert "x_search" in lowered
assert "xurl" in lowered
# x_search is discovery / read-only public research.
assert _contains_any(text, "read-only public", "public x discovery", "broad public")
# xurl owns authenticated / write / exact API work.
assert _contains_any(
text,
"authenticated",
"exact or authenticated",
"exact api",
"account actions",
"write action",
)
# Writes must not be evidenced by x_search answers.
assert _contains_any(
text,
"never treat an `x_search` answer",
"never evidence",
"proves the action",
"x api response",
)
# Prefer x_search over xurl search for broad public discovery when available.
assert "x_search" in lowered and "xurl search" in lowered
assert _contains_any(text, "prefer `x_search`", "use `x_search` instead", "route by intent")
def test_xurl_agent_workflow_prefers_x_search_for_broad_discovery():
text = _read(XURL_SKILL)
# Workflow must preflight intent before xurl search.
assert "xurl search" in text.lower()
assert _contains_any(text, "check intent", "before using `xurl search`")
assert _contains_any(text, "broad public", "public x discovery")
assert _contains_any(text, "write action", "authenticated account", "exact api")
def test_x_search_doc_separates_discovery_from_account_actions():
text = _read(X_SEARCH_DOC)
lowered = text.lower()
assert "x_search" in lowered
assert "xurl" in lowered
# Explicit comparison section or equivalent boundary language.
assert _contains_any(text, "vs `xurl`", "vs xurl", "two different x surfaces")
assert _contains_any(text, "read-only public", "public x discovery")
assert _contains_any(
text,
"posting",
"replying",
"liking",
"dm",
"media upload",
"deleting",
)
assert _contains_any(
text,
"authenticated",
"exact or authenticated",
"account actions",
"state-changing",
)
# Write confirmation must come from xurl / X API, not x_search.
assert _contains_any(
text,
"confirmed by `xurl`",
"xurl` output",
"x api response",
"never evidence",
)
assert _contains_any(text, "switch to the `xurl`", "switch to `xurl`", "xurl skill")

View file

@ -31,6 +31,15 @@ class TestGetToolset:
assert ts is not None
assert "web_search" in ts["tools"]
def test_x_search_toolset_marks_read_only_and_points_to_xurl(self):
ts = get_toolset("x_search")
assert ts is not None
assert ts["tools"] == ["x_search"]
description = ts["description"].lower()
assert "read-only" in description
assert "xurl" in description
assert "authenticated" in description
def test_merges_registry_tools_into_builtin_toolset(self, monkeypatch):
reg = ToolRegistry()
reg.register(

View file

@ -98,6 +98,28 @@ def test_x_search_rejects_conflicting_handle_filters(monkeypatch):
assert result["error"] == "allowed_x_handles and excluded_x_handles cannot be used together"
def test_x_search_schema_is_read_only_without_cross_tool_names():
"""Static schema must state read-only scope without naming other surfaces.
AGENTS.md forbids hardcoding cross-tool/skill names in tool schemas because
those surfaces may be unavailable. Keep out-of-scope guidance generic here;
xurl routing lives in the skill and feature docs.
"""
from tools.x_search_tool import X_SEARCH_SCHEMA
description = X_SEARCH_SCHEMA["description"]
lowered = description.lower()
assert "read-only" in lowered
assert "public x" in lowered
for action in ("post", "reply", "like", "dm", "upload media", "delete"):
assert action in lowered
assert "authenticated" in lowered
# No static cross-surface names in the model-facing schema.
assert "xurl" not in lowered
assert "web_search" not in lowered
def test_x_search_extracts_inline_url_citations(monkeypatch):
from tools.x_search_tool import x_search_tool

View file

@ -480,9 +480,12 @@ X_SEARCH_SCHEMA = {
"name": "x_search",
"description": (
"Search X (Twitter) posts, profiles, and threads using xAI's built-in "
"X Search tool. Use this for current discussion, reactions, or claims "
"on X rather than general web pages. Available when xAI credentials "
"are configured (SuperGrok OAuth or XAI_API_KEY)."
"X Search tool. Read-only discovery only: use this for current "
"discussion, reactions, or claims on public X rather than general web "
"pages. Do not use it to post, reply, like, DM, upload media, delete, "
"or inspect the user's authenticated X account — those require a "
"separate authenticated X API surface outside this tool. Available "
"when xAI credentials are configured (SuperGrok OAuth or XAI_API_KEY)."
),
"parameters": {
"type": "object",

View file

@ -110,9 +110,11 @@ TOOLSETS = {
"x_search": {
"description": (
"Search X (Twitter) posts and threads via xAI's built-in "
"x_search Responses tool. Available when xAI credentials are "
"configured (SuperGrok OAuth or XAI_API_KEY). Off by default; "
"enable in `hermes tools` → X (Twitter) Search."
"x_search Responses tool. Read-only public X discovery; use the "
"xurl skill for authenticated X API reads and account actions. "
"Available when xAI credentials are configured (SuperGrok OAuth "
"or XAI_API_KEY). Off by default; enable in `hermes tools` → "
"X (Twitter) Search."
),
"tools": ["x_search"],
"includes": []

View file

@ -216,7 +216,7 @@ The single `video_generate` tool covers both modalities — pass `image_url` to
| Tool | Description | Requires environment |
|------|-------------|----------------------|
| `x_search` | Search X (Twitter) posts, profiles, and threads using xAI's built-in `x_search` Responses tool. Use this for current discussion, reactions, or claims on X rather than general web pages. Off by default — opt in via `hermes tools` → 🐦 X (Twitter) Search. Schema is only registered when xAI credentials are configured (check_fn-gated). | XAI_API_KEY **or** xAI Grok OAuth (SuperGrok / Premium+) login |
| `x_search` | Search X (Twitter) posts, profiles, and threads using xAI's built-in `x_search` Responses tool. Read-only public X discovery for current discussion, reactions, or claims on public X (not general web pages). Does not post, reply, like, DM, upload media, delete, or inspect the authenticated X account — those need a separate authenticated X API surface (e.g. the `xurl` skill). Off by default — opt in via `hermes tools` → 🐦 X (Twitter) Search. Schema is only registered when xAI credentials are configured (check_fn-gated). | XAI_API_KEY **or** xAI Grok OAuth (SuperGrok / Premium+) login |
## `tts` toolset

View file

@ -83,7 +83,7 @@ Or in-session:
| `vision` | `vision_analyze` | Image analysis via vision-capable models. |
| `video` | `video_analyze` | Video analysis and understanding tools (opt-in, not in the default toolset — add explicitly via `--toolsets`). |
| `web` | `web_extract`, `web_search` | Web search and page content extraction. |
| `x_search` | `x_search` | Search X (Twitter) posts and threads via xAI's built-in `x_search` Responses tool. Off by default; opt in via `hermes tools`. Schema only registered when xAI credentials (SuperGrok OAuth or `XAI_API_KEY`) are configured. |
| `x_search` | `x_search` | Read-only public X discovery via xAI's built-in `x_search` Responses tool. Use the `xurl` skill for authenticated X API reads and account actions. Off by default; opt in via `hermes tools`. Schema only registered when xAI credentials (SuperGrok OAuth or `XAI_API_KEY`) are configured. |
| `yuanbao` | `yb_query_group_info`, `yb_query_group_members`, `yb_search_sticker`, `yb_send_dm`, `yb_send_sticker` | Yuanbao DM/group actions and sticker search. Registered only on `hermes-yuanbao`. |
## Platform Toolsets

View file

@ -11,6 +11,17 @@ The `x_search` tool lets the agent search X (Twitter) posts, profiles, and threa
**Use this instead of `web_search`** when you specifically want current discussion, reactions, or claims **on X**. For general web pages, keep using `web_search` / `web_extract`.
## `x_search` vs `xurl`
Hermes can expose two different X surfaces:
| Surface | Use it for | Do not use it for |
|---------|------------|-------------------|
| `x_search` | Read-only public X discovery: current discussion, reactions, claims, profiles, threads, and synthesized answers with citations. | Posting, replying, liking, DMs, media upload, deleting, or proving that an authenticated X account changed state. |
| `xurl` skill | Exact or authenticated X API work: `post`, `reply`, `read`, `like`, `dm`, timelines, mentions, media upload, account-specific reads, and raw v2 endpoints. | Broad Grok-synthesized public X research when `x_search` is available and no authenticated account context is needed. |
For mixed workflows, use `x_search` to discover candidate public posts, then switch to `xurl read` or another exact `xurl` command after the target post/user/action is clear. Any state-changing X action must be confirmed by `xurl` output or the X API response; an `x_search` answer is never evidence that a write happened.
:::tip
If you're paying Portal for an xAI model anyway, Live Search calls bill against the same xAI key configured for chat. See [Nous Portal](/integrations/nous-portal).
:::
@ -119,6 +130,8 @@ The agent will:
2. Get back a synthesized answer plus a list of citations linking to specific posts
3. Reply with the answer and references
If the next user request is "reply to the best one" or "like that post", the agent should switch to the `xurl` skill, confirm the exact target post, and use the X API action. `x_search` remains a discovery tool.
## Troubleshooting
### "No xAI credentials available"
@ -149,5 +162,6 @@ Causes worth checking:
## See Also
- [xAI Grok OAuth (SuperGrok / Premium+)](../../guides/xai-grok-oauth.md) — the OAuth setup guide
- [xurl skill](../skills/bundled/social-media/social-media-xurl.md) — official X API CLI for authenticated account actions
- [Web Search & Extract](web-search.md) — for general (non-X) web search
- [Tools Reference](../../reference/tools-reference.md) — full tool catalog