mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
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:
parent
b718725121
commit
b9b100da11
10 changed files with 174 additions and 15 deletions
|
|
@ -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)."
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
98
tests/skills/test_xurl_x_search_routing.py
Normal file
98
tests/skills/test_xurl_x_search_routing.py
Normal 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")
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue