mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
The subpath entry existed only so the TUI could import the client-side projection fallback. The TUI spawns its gateway from this same checkout and cannot version-skew with it, so that fallback was dead weight — it reads `display` directly now. With its last caller gone the dispatch helper folds back into the desktop, where an older backend is genuinely reachable. apps/shared/package.json is byte-identical to main again.
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
/**
|
|
* A `/skill` invocation expands into a model-facing message that embeds the
|
|
* whole skill body. That payload is for the agent — the UI shows the
|
|
* invocation the user typed (`/work fix the leak`) and nothing else.
|
|
*
|
|
* The gateway already projects this (see `_skill_scaffold_projection` in
|
|
* tui_gateway/server.py) and ships the result as `display` on a dispatch and
|
|
* as the `text` of a `skill_invocation` history row. This module is the
|
|
* client-side twin so a desktop/TUI talking to an older gateway — or any
|
|
* future path that hands raw scaffolding to a bubble — still renders the
|
|
* invocation instead of the body.
|
|
*
|
|
* The markers below mirror `agent/skill_commands.py` byte for byte.
|
|
*/
|
|
|
|
const INVOCATION_PREFIX = '[IMPORTANT: The user has invoked the '
|
|
const SINGLE_MARKER = 'The full skill content is loaded below.]'
|
|
const SINGLE_INSTRUCTION = 'The user has provided the following instruction alongside the skill invocation: '
|
|
const RUNTIME_NOTE = '\n\n[Runtime note:'
|
|
const BUNDLE_MARKER = ' skill bundle,'
|
|
const BUNDLE_INSTRUCTION = '\nUser instruction: '
|
|
const BUNDLE_SKILL_BLOCK = '\n\n[Loaded as part of the '
|
|
|
|
// The skill name is the first quoted span of the activation note, for both the
|
|
// single-skill (`work`) and the bundle (`/clean /work`) header.
|
|
const NAME_RE = new RegExp(`^${INVOCATION_PREFIX.replace(/[[\]]/g, '\\$&')}"([^"]*)"`)
|
|
|
|
/** Text between `marker` and `end`, or '' when the marker is absent. */
|
|
function between(text: string, marker: string, end: string, fromEnd = false): string {
|
|
const index = fromEnd ? text.lastIndexOf(marker) : text.indexOf(marker)
|
|
|
|
if (index < 0) {
|
|
return ''
|
|
}
|
|
|
|
const tail = text.slice(index + marker.length)
|
|
const stop = tail.indexOf(end)
|
|
|
|
return (stop >= 0 ? tail.slice(0, stop) : tail).trim()
|
|
}
|
|
|
|
/**
|
|
* The invocation a scaffolded turn came from (`/work fix the leak`), or null
|
|
* when `text` is ordinary user prose that should render as written.
|
|
*/
|
|
export function skillInvocationText(text: string): null | string {
|
|
if (!text.startsWith(INVOCATION_PREFIX)) {
|
|
return null
|
|
}
|
|
|
|
const name = (NAME_RE.exec(text)?.[1] ?? '').trim()
|
|
|
|
if (!name) {
|
|
return null
|
|
}
|
|
|
|
// Bundle headers already carry their typed "/a /b" keys; a single skill is
|
|
// a bare name. The single-skill instruction trails the body (which may quote
|
|
// the marker), so match it from the end.
|
|
const label = name.startsWith('/') ? name : `/${name}`
|
|
|
|
const instruction = text.includes(BUNDLE_MARKER)
|
|
? between(text, BUNDLE_INSTRUCTION, BUNDLE_SKILL_BLOCK)
|
|
: text.includes(SINGLE_MARKER)
|
|
? between(text, SINGLE_INSTRUCTION, RUNTIME_NOTE, true)
|
|
: ''
|
|
|
|
return instruction ? `${label} ${instruction.replace(/\s+/g, ' ')}` : label
|
|
}
|