* feat(steer): /steer <prompt> injects a mid-run note after the next tool call Adds a new slash command that sits between /queue (turn boundary) and interrupt. /steer <text> stashes the message on the running agent and the agent loop appends it to the LAST tool result's content once the current tool batch finishes. The model sees it as part of the tool output on its next iteration. No interrupt is fired, no new user turn is inserted, and no prompt cache invalidation happens beyond the normal per-turn tool-result churn. Message-role alternation is preserved — we only modify an existing role:"tool" message's content. Wiring ------ - hermes_cli/commands.py: register /steer + add to ACTIVE_SESSION_BYPASS_COMMANDS. - run_agent.py: add _pending_steer state, AIAgent.steer(), _drain_pending_steer(), _apply_pending_steer_to_tool_results(); drain at end of both parallel and sequential tool executors; clear on interrupt; return leftover as result['pending_steer'] if the agent exits before another tool batch. - cli.py: /steer handler — route to agent.steer() when running, fall back to the regular queue otherwise; deliver result['pending_steer'] as next turn. - gateway/run.py: running-agent intercept calls running_agent.steer(); idle-agent path strips the prefix and forwards as a regular user message. - tui_gateway/server.py: new session.steer JSON-RPC method. - ui-tui: SessionSteerResponse type + local /steer slash command that calls session.steer when ui.busy, otherwise enqueues for the next turn. Fallbacks --------- - Agent exits mid-steer → surfaces in run_conversation result as pending_steer so CLI/gateway deliver it as the next user turn instead of silently dropping it. - All tools skipped after interrupt → re-stashes pending_steer for the caller. - No active agent → /steer reduces to sending the text as a normal message. Tests ----- - tests/run_agent/test_steer.py — accept/reject, concatenation, drain, last-tool-result injection, multimodal list content, thread safety, cleared-on-interrupt, registry membership, bypass-set membership. - tests/gateway/test_steer_command.py — running agent, pending sentinel, missing steer() method, rejected payload, empty payload. - tests/gateway/test_command_bypass_active_session.py — /steer bypasses the Level-1 base adapter guard. - tests/test_tui_gateway_server.py — session.steer RPC paths. 72/72 targeted tests pass under scripts/run_tests.sh. * feat(steer): register /steer in Discord's native slash tree Discord's app_commands tree is a curated subset of slash commands (not derived from COMMAND_REGISTRY like Telegram/Slack). /steer already works there as plain text (routes through handle_message → base adapter bypass → runner), but registering it here adds Discord's native autocomplete + argument hint UI so users can discover and type it like any other first-class command.
330 lines
9.4 KiB
TypeScript
330 lines
9.4 KiB
TypeScript
import type { SessionInfo, SlashCategory, Usage } from './types.js'
|
|
|
|
export interface GatewaySkin {
|
|
banner_hero?: string
|
|
banner_logo?: string
|
|
branding?: Record<string, string>
|
|
colors?: Record<string, string>
|
|
help_header?: string
|
|
tool_prefix?: string
|
|
}
|
|
|
|
export interface GatewayCompletionItem {
|
|
display: string
|
|
meta?: string
|
|
text: string
|
|
}
|
|
|
|
export interface GatewayTranscriptMessage {
|
|
context?: string
|
|
name?: string
|
|
role: 'assistant' | 'system' | 'tool' | 'user'
|
|
text?: string
|
|
}
|
|
|
|
// ── Commands / completion ────────────────────────────────────────────
|
|
|
|
export interface CommandsCatalogResponse {
|
|
canon?: Record<string, string>
|
|
categories?: SlashCategory[]
|
|
pairs?: [string, string][]
|
|
skill_count?: number
|
|
sub?: Record<string, string[]>
|
|
warning?: string
|
|
}
|
|
|
|
export interface CompletionResponse {
|
|
items?: GatewayCompletionItem[]
|
|
replace_from?: number
|
|
}
|
|
|
|
export interface SlashExecResponse {
|
|
output?: string
|
|
warning?: string
|
|
}
|
|
|
|
export type CommandDispatchResponse =
|
|
| { output?: string; type: 'exec' | 'plugin' }
|
|
| { target: string; type: 'alias' }
|
|
| { message?: string; name: string; type: 'skill' }
|
|
|
|
// ── Config ───────────────────────────────────────────────────────────
|
|
|
|
export interface ConfigDisplayConfig {
|
|
bell_on_complete?: boolean
|
|
details_mode?: string
|
|
thinking_mode?: string
|
|
tui_compact?: boolean
|
|
tui_statusbar?: boolean
|
|
}
|
|
|
|
export interface ConfigFullResponse {
|
|
config?: { display?: ConfigDisplayConfig }
|
|
}
|
|
|
|
export interface ConfigMtimeResponse {
|
|
mtime?: number
|
|
}
|
|
|
|
export interface ConfigGetValueResponse {
|
|
display?: string
|
|
home?: string
|
|
value?: string
|
|
}
|
|
|
|
export interface ConfigSetResponse {
|
|
credential_warning?: string
|
|
history_reset?: boolean
|
|
info?: SessionInfo
|
|
value?: string
|
|
warning?: string
|
|
}
|
|
|
|
export interface SetupStatusResponse {
|
|
provider_configured?: boolean
|
|
}
|
|
|
|
// ── Session lifecycle ────────────────────────────────────────────────
|
|
|
|
export interface SessionCreateResponse {
|
|
info?: SessionInfo & { credential_warning?: string }
|
|
session_id: string
|
|
}
|
|
|
|
export interface SessionResumeResponse {
|
|
info?: SessionInfo
|
|
message_count?: number
|
|
messages: GatewayTranscriptMessage[]
|
|
resumed?: string
|
|
session_id: string
|
|
}
|
|
|
|
export interface SessionListItem {
|
|
id: string
|
|
message_count: number
|
|
preview: string
|
|
source?: string
|
|
started_at: number
|
|
title: string
|
|
}
|
|
|
|
export interface SessionListResponse {
|
|
sessions?: SessionListItem[]
|
|
}
|
|
|
|
export interface SessionUndoResponse {
|
|
removed?: number
|
|
}
|
|
|
|
export interface SessionUsageResponse {
|
|
cache_read?: number
|
|
cache_write?: number
|
|
calls?: number
|
|
compressions?: number
|
|
context_max?: number
|
|
context_percent?: number
|
|
context_used?: number
|
|
cost_status?: 'estimated' | 'exact'
|
|
cost_usd?: number
|
|
input?: number
|
|
model?: string
|
|
output?: number
|
|
total?: number
|
|
}
|
|
|
|
export interface SessionCompressResponse {
|
|
info?: SessionInfo
|
|
messages?: GatewayTranscriptMessage[]
|
|
removed?: number
|
|
usage?: Usage
|
|
}
|
|
|
|
export interface SessionBranchResponse {
|
|
session_id?: string
|
|
title?: string
|
|
}
|
|
|
|
export interface SessionCloseResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface SessionInterruptResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface SessionSteerResponse {
|
|
status?: 'queued' | 'rejected'
|
|
text?: string
|
|
}
|
|
|
|
// ── Prompt / submission ──────────────────────────────────────────────
|
|
|
|
export interface PromptSubmitResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface BackgroundStartResponse {
|
|
task_id?: string
|
|
}
|
|
|
|
export interface BtwStartResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface ClarifyRespondResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface ApprovalRespondResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface SudoRespondResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
export interface SecretRespondResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
// ── Shell / clipboard / input ────────────────────────────────────────
|
|
|
|
export interface ShellExecResponse {
|
|
code: number
|
|
stderr?: string
|
|
stdout?: string
|
|
}
|
|
|
|
export interface ClipboardPasteResponse {
|
|
attached?: boolean
|
|
count?: number
|
|
height?: number
|
|
message?: string
|
|
token_estimate?: number
|
|
width?: number
|
|
}
|
|
|
|
export interface InputDetectDropResponse {
|
|
height?: number
|
|
is_image?: boolean
|
|
matched?: boolean
|
|
name?: string
|
|
text?: string
|
|
token_estimate?: number
|
|
width?: number
|
|
}
|
|
|
|
export interface TerminalResizeResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
// ── Image attach ─────────────────────────────────────────────────────
|
|
|
|
export interface ImageAttachResponse {
|
|
height?: number
|
|
name?: string
|
|
remainder?: string
|
|
token_estimate?: number
|
|
width?: number
|
|
}
|
|
|
|
// ── Voice ────────────────────────────────────────────────────────────
|
|
|
|
export interface VoiceToggleResponse {
|
|
enabled?: boolean
|
|
}
|
|
|
|
export interface VoiceRecordResponse {
|
|
text?: string
|
|
}
|
|
|
|
// ── Tools (TS keeps configure since it resets local history) ─────────
|
|
|
|
export interface ToolsConfigureResponse {
|
|
changed?: string[]
|
|
enabled_toolsets?: string[]
|
|
info?: SessionInfo
|
|
missing_servers?: string[]
|
|
reset?: boolean
|
|
unknown?: string[]
|
|
}
|
|
|
|
// ── Model picker ─────────────────────────────────────────────────────
|
|
|
|
export interface ModelOptionProvider {
|
|
is_current?: boolean
|
|
models?: string[]
|
|
name: string
|
|
slug: string
|
|
total_models?: number
|
|
warning?: string
|
|
}
|
|
|
|
export interface ModelOptionsResponse {
|
|
model?: string
|
|
provider?: string
|
|
providers?: ModelOptionProvider[]
|
|
}
|
|
|
|
// ── MCP ──────────────────────────────────────────────────────────────
|
|
|
|
export interface ReloadMcpResponse {
|
|
ok?: boolean
|
|
}
|
|
|
|
// ── Subagent events ──────────────────────────────────────────────────
|
|
|
|
export interface SubagentEventPayload {
|
|
duration_seconds?: number
|
|
goal: string
|
|
status?: 'completed' | 'failed' | 'interrupted' | 'running'
|
|
summary?: string
|
|
task_count?: number
|
|
task_index: number
|
|
text?: string
|
|
tool_name?: string
|
|
tool_preview?: string
|
|
}
|
|
|
|
export type GatewayEvent =
|
|
| { payload?: { skin?: GatewaySkin }; session_id?: string; type: 'gateway.ready' }
|
|
| { payload?: GatewaySkin; session_id?: string; type: 'skin.changed' }
|
|
| { payload: SessionInfo; session_id?: string; type: 'session.info' }
|
|
| { payload?: { text?: string }; session_id?: string; type: 'thinking.delta' }
|
|
| { payload?: undefined; session_id?: string; type: 'message.start' }
|
|
| { payload?: { kind?: string; text?: string }; session_id?: string; type: 'status.update' }
|
|
| { payload: { line: string }; session_id?: string; type: 'gateway.stderr' }
|
|
| { payload?: { cwd?: string; python?: string }; session_id?: string; type: 'gateway.start_timeout' }
|
|
| { payload?: { preview?: string }; session_id?: string; type: 'gateway.protocol_error' }
|
|
| { payload?: { text?: string }; session_id?: string; type: 'reasoning.delta' | 'reasoning.available' }
|
|
| { payload: { name?: string; preview?: string }; session_id?: string; type: 'tool.progress' }
|
|
| { payload: { name?: string }; session_id?: string; type: 'tool.generating' }
|
|
| { payload: { context?: string; name?: string; tool_id: string }; session_id?: string; type: 'tool.start' }
|
|
| {
|
|
payload: { error?: string; inline_diff?: string; name?: string; summary?: string; tool_id: string }
|
|
session_id?: string
|
|
type: 'tool.complete'
|
|
}
|
|
| {
|
|
payload: { choices: string[] | null; question: string; request_id: string }
|
|
session_id?: string
|
|
type: 'clarify.request'
|
|
}
|
|
| { payload: { command: string; description: string }; session_id?: string; type: 'approval.request' }
|
|
| { payload: { request_id: string }; session_id?: string; type: 'sudo.request' }
|
|
| { payload: { env_var: string; prompt: string; request_id: string }; session_id?: string; type: 'secret.request' }
|
|
| { payload: { task_id: string; text: string }; session_id?: string; type: 'background.complete' }
|
|
| { payload: { text: string }; session_id?: string; type: 'btw.complete' }
|
|
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.start' }
|
|
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.thinking' }
|
|
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.tool' }
|
|
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.progress' }
|
|
| { payload: SubagentEventPayload; session_id?: string; type: 'subagent.complete' }
|
|
| { payload: { rendered?: string; text?: string }; session_id?: string; type: 'message.delta' }
|
|
| {
|
|
payload?: { reasoning?: string; rendered?: string; text?: string; usage?: Usage }
|
|
session_id?: string
|
|
type: 'message.complete'
|
|
}
|
|
| { payload?: { message?: string }; session_id?: string; type: 'error' }
|