Skip to content

Omnio: honor /skill-name slash commands on the OpenAI chat path - #17

Merged
ppazosp merged 5 commits into
mainfrom
ppp/omnio-slash-commands-main
Jun 25, 2026
Merged

Omnio: honor /skill-name slash commands on the OpenAI chat path#17
ppazosp merged 5 commits into
mainfrom
ppp/omnio-slash-commands-main

Conversation

@ppazosp

@ppazosp ppazosp commented Jun 25, 2026

Copy link
Copy Markdown

Honor /skill-name slash commands on POST /v1/chat/completions (the only path Omnia's chat calls), and surface the brand's invocable commands — including the built-in /learn — on GET /v1/skills so the client can list them.

This re-lands the patch that PR #16 introduced: #16 was stacked on ppp/sync-upstream-2026-06 and its base was never retargeted, so merging it landed the patch in that (now-deleted) sync branch instead of main. The upstream sync (#15) is in main; this brings the slash patch on top of it. Same 5 commits, cherry-picked cleanly onto post-sync main.

What's in it

  • Slash expansion on the chat path_maybe_expand_slash_command runs in _handle_chat_completions after the session is resolved: a leading /command resolves to a bundle then a skill and is rewritten to the canonical invocation message (unmodified, so the sync's memory-extraction scaffolding stays intact); /learn expands via build_learn_prompt; unknown /foo passes straight through to the LLM as plain text. A resolved-but-build-failed command is logged rather than silently dropped.
  • GET /v1/skills carries the command — each skill gets a command field (validated against the live get_skill_commands() registry via slugify_skill_name), and the listing appends the /learn built-in (category: "command") so it's endpoint-driven, not hardcoded client-side.

Tests

tests/gateway/test_api_server_slash_commands.py, tests/agent/test_slugify_skill_name.py, extended tests/gateway/test_api_server.py — 201 passing, ruff clean on the post-sync base.

Pairs with Omnia PR NousResearch#3315 (merged), which drives the / palette UI.

🤖 Generated with Claude Code

https://claude.ai/code/session_0184TxzJd1SYeBedsTLEfEwM

ppazosp and others added 5 commits June 25, 2026 12:35
The OpenAI chat path is the only Hermes surface that did not honor slash
commands — CLI, TUI and the messaging gateway all expand a leading
/skill-name into its skill-invocation payload, but a chat-completions
client (Omnia's Omnio chat) got the raw "/foo" forwarded to the model as
plain text.

Add _maybe_expand_skill_command and call it in _handle_chat_completions
once the session id is resolved: a recognized /command is replaced with
the unmodified build_skill_invocation_message / build_bundle_invocation_message
output (bundles take precedence, mirroring gateway/run.py dispatch order).
Unlike the messaging gateway, the chat API carries general prose, so a
message that merely starts with "/" and matches no skill (a path, a
question about /etc) passes through untouched rather than being rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bit68U5GXoHUds92TE5XVp
A "/" skill palette needs the exact command string to send for each skill;
deriving the slug client-side would risk drifting from Hermes' own
normalization and silently producing a "/foo" that never resolves.

Extract slugify_skill_name as the single source of truth (lower, spaces/
underscores to hyphens, drop invalid chars, collapse + trim hyphens), reuse
it in scan_skill_commands, and add a `command` field to each /v1/skills
entry (null when the name reduces to an empty slug). Clients render and send
"/<command>" and it is guaranteed to resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bit68U5GXoHUds92TE5XVp
- A "/command" that RESOLVES to a real skill/bundle but then builds no
  payload (e.g. a SKILL.md removed/unreadable) was silently forwarded to the
  model as raw text. Log it at error level (and use logger.exception for the
  unexpected-exception branches) so the failure is debuggable instead of
  looking like the skill "did nothing".
- /v1/skills now validates each derived command against the live command
  registry: a non-null `command` is guaranteed to resolve on the chat path.
  This rejects slugs that don't round-trip (display-truncated names) and
  per-platform-disabled/incompatible skills (absent from the registry), which
  report command=null rather than a command that silently fails to invoke.
- Document that multimodal (text+image) turns don't expand (content is a list,
  not a str).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bit68U5GXoHUds92TE5XVp
/learn is the one built-in command worth honoring on the OpenAI chat path
(no Omnia-UI equivalent): like the gateway, it rewrites the turn to the
standards-guided build_learn_prompt that drives the agent to author a skill
via skill_manage. Renamed _maybe_expand_skill_command -> _maybe_expand_slash_command
and dispatch /learn before skill/bundle resolution. Side-effecting built-ins
(/new, /yolo, …) are still deliberately not handled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bit68U5GXoHUds92TE5XVp
…arn")

The palette was hardcoding the /learn command client-side, so it showed even
when the sprite was cold. Return it from /v1/skills instead (category
"command", name "learn") so the whole palette is endpoint-driven — empty until
the gateway responds — and the displayed name is the command itself, not prose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bit68U5GXoHUds92TE5XVp
@github-actions

Copy link
Copy Markdown

🔎 Lint report: ppp/omnio-slash-commands-main vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11251 on HEAD, 11252 on base (✅ -1)

🆕 New issues (2):

Rule Count
invalid-assignment 1
unresolved-import 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
tests/agent/test_slugify_skill_name.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues (2):

Rule Count
unresolved-attribute 2
First entries
run_agent.py:2984: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`

Unchanged: 5933 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@ppazosp
ppazosp merged commit b65931a into main Jun 25, 2026
25 checks passed
ppazosp pushed a commit that referenced this pull request Jul 30, 2026
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