Phase 003: remove the coding-agent and ekko-agent runtimes
Every chat session now runs through the Hermes bridge, so the three chat chains become feature-equivalent instead of three partially-maintained dispatch paths. Keeping the coding-agent and in-house agent runtimes alive meant every bridge change had to be mirrored into two more runtimes that had already drifted. Historical rows are read, not migrated: sessions stored with the retired source normalize to cli on load, workflow graphs naming the retired agents keep loading and run on the bridge, and the visible-source check plus SOURCE_LABELS still recognize the old value so those sessions stay listed. The public surface follows the code: the OpenAPI generator and the MCP chat-run tool schema drop the six retired entry fields so external callers are not told about runtimes that no longer exist. harness-check gains a RETIRED_RUNTIME scan so the residual check is a mechanical gate instead of a manual grep, with the two surviving read-side compat sites recorded with reasons. Refs: docs/phases/phase_003.md
This commit is contained in:
@@ -173,13 +173,13 @@ describe('API Client', () => {
|
||||
status: 500,
|
||||
text: () => Promise.resolve(JSON.stringify({
|
||||
error: {
|
||||
message: 'spawn claude ENOENT',
|
||||
message: 'bridge connect ENOENT',
|
||||
code: 'ENOENT',
|
||||
},
|
||||
})),
|
||||
})
|
||||
|
||||
await expect(request('/api/coding-agents/runs/session-1/input')).rejects.toThrow('API Error 500: spawn claude ENOENT')
|
||||
await expect(request('/api/hermes/sessions/session-1/input')).rejects.toThrow('API Error 500: bridge connect ENOENT')
|
||||
})
|
||||
|
||||
it('returns parsed JSON on success', async () => {
|
||||
|
||||
@@ -154,11 +154,8 @@ describe('ChatInput draft persistence', () => {
|
||||
expect((wrapper.get('textarea').element as HTMLTextAreaElement).style.height).not.toBe('180px')
|
||||
})
|
||||
|
||||
it('shows context usage for coding-agent sessions', async () => {
|
||||
const wrapper = mountForSession('session-codex', {
|
||||
source: 'coding_agent',
|
||||
agent: 'codex',
|
||||
codingAgentId: 'codex',
|
||||
it('shows context usage for the active session', async () => {
|
||||
const wrapper = mountForSession('session-context', {
|
||||
inputTokens: 1200,
|
||||
outputTokens: 800,
|
||||
contextTokens: 2000,
|
||||
@@ -170,12 +167,8 @@ describe('ChatInput draft persistence', () => {
|
||||
expect(wrapper.find('.context-bar').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows reasoning effort selector for coding-agent sessions', async () => {
|
||||
const wrapper = mountForSession('session-codex', {
|
||||
source: 'coding_agent',
|
||||
agent: 'codex',
|
||||
codingAgentId: 'codex',
|
||||
})
|
||||
it('shows the reasoning effort selector for the active session', async () => {
|
||||
const wrapper = mountForSession('session-reasoning-effort')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.n-popselect-stub').exists()).toBe(true)
|
||||
|
||||
@@ -9,27 +9,20 @@ describe('ChatPanel session clicks', () => {
|
||||
expect(source).toContain('await chatStore.switchSession(sessionId)')
|
||||
})
|
||||
|
||||
it('allows session model switching for coding agent sessions', () => {
|
||||
it('switches the session model without a protocol mode modal', () => {
|
||||
const source = readFileSync('packages/client/src/components/hermes/chat/ChatPanel.vue', 'utf8')
|
||||
|
||||
expect(source).toContain('contextSession.value?.source === "coding_agent"')
|
||||
expect(source).toContain('isSessionModelScopedCodingAgent')
|
||||
expect(source).toContain('!isCodingAgentAuthProvider(group.provider)')
|
||||
expect(source).toContain('showSessionModelModeModal')
|
||||
expect(source).toContain('pendingSessionModelSwitch')
|
||||
expect(source).toContain('chatStore.switchSessionModel(model, provider, sessionModelSessionId.value, apiMode)')
|
||||
expect(source).toContain('chatStore.switchSessionModel(model, provider, sessionModelSessionId.value)')
|
||||
expect(source).not.toContain('showSessionModelModeModal')
|
||||
expect(source).not.toContain('pendingSessionModelSwitch')
|
||||
expect(source).not.toContain('header-model-button--readonly')
|
||||
expect(source).not.toContain('if (isActiveSessionCodingAgent.value) return')
|
||||
})
|
||||
|
||||
it('uses codingAgentId when deciding whether session model switches need an API mode', () => {
|
||||
it('does not gate session model switching on a coding agent id', () => {
|
||||
const source = readFileSync('packages/client/src/components/hermes/chat/ChatPanel.vue', 'utf8')
|
||||
|
||||
expect(source).toContain('const sessionModelCodingAgentId = computed<ChatCodingAgentId | undefined>')
|
||||
expect(source).toContain('sessionModelSession.value?.codingAgentId ||')
|
||||
expect(source).toContain('sessionModelSession.value?.agent === "claude"')
|
||||
expect(source).toContain('sessionModelCodingAgentId.value === "claude-code"')
|
||||
expect(source).not.toContain('sessionModelSession.value?.agent === "claude-code"')
|
||||
expect(source).not.toContain('sessionModelCodingAgentId')
|
||||
expect(source).not.toContain('isSessionModelScopedCodingAgent')
|
||||
})
|
||||
|
||||
it('uses the active sidebar model as the new chat default for the active profile', () => {
|
||||
|
||||
@@ -294,12 +294,9 @@ describe('chat store reasoning/tool boundaries', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('settles running coding-agent tools when the run completes without a tool.completed event', async () => {
|
||||
it('settles running tools when the run completes without a tool.completed event', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'claude'
|
||||
session.codingAgentId = 'claude-code'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
@@ -351,49 +348,6 @@ describe('chat store reasoning/tool boundaries', () => {
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('queues active coding agent follow-up messages without command styling', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'claude'
|
||||
session.codingAgentId = 'claude-code'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
await store.sendMessage('first input')
|
||||
const onEvent = chatApi.startRunViaSocket.mock.calls[0][1] as (event: RunEvent) => void
|
||||
onEvent({
|
||||
event: 'agent.event',
|
||||
session_id: 'session-1',
|
||||
source: 'coding_agent',
|
||||
kind: 'status',
|
||||
text: 'Input sent to coding agent.',
|
||||
})
|
||||
expect(store.messages).toHaveLength(1)
|
||||
|
||||
onEvent({ event: 'run.started', session_id: 'session-1' })
|
||||
|
||||
await store.sendMessage('/not-a-hermes-command')
|
||||
|
||||
expect(chatApi.startRunViaSocket).toHaveBeenCalledTimes(2)
|
||||
expect(store.queuedUserMessages.get('session-1')).toEqual([
|
||||
expect.objectContaining({
|
||||
role: 'user',
|
||||
content: '/not-a-hermes-command',
|
||||
queued: true,
|
||||
systemType: undefined,
|
||||
}),
|
||||
])
|
||||
expect(store.messages).toEqual([
|
||||
expect.objectContaining({
|
||||
role: 'user',
|
||||
content: 'first input',
|
||||
queued: false,
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('queues unknown slash commands in bridge sessions as normal user input', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
@@ -484,111 +438,14 @@ describe('chat store reasoning/tool boundaries', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('starts global coding-agent runs without provider credentials', async () => {
|
||||
it('sends the selected workspace and reasoning effort when starting a bridge run', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'codex'
|
||||
session.codingAgentId = 'codex'
|
||||
session.codingAgentMode = 'global'
|
||||
session.provider = 'should-not-send'
|
||||
session.model = 'should-not-send'
|
||||
session.baseUrl = 'http://example.invalid'
|
||||
session.apiKey = 'secret'
|
||||
session.apiMode = 'chat_completions'
|
||||
session.reasoningEffort = 'high'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
await store.sendMessage('use global codex auth')
|
||||
|
||||
const body = chatApi.startRunViaSocket.mock.calls[0][0]
|
||||
expect(body).toEqual(expect.objectContaining({
|
||||
source: 'coding_agent',
|
||||
coding_agent_id: 'codex',
|
||||
mode: 'global',
|
||||
}))
|
||||
expect(body.provider).toBeUndefined()
|
||||
expect(body.model).toBeUndefined()
|
||||
expect(body.baseUrl).toBeUndefined()
|
||||
expect(body.apiKey).toBeUndefined()
|
||||
expect(body.apiMode).toBeUndefined()
|
||||
expect(body.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
|
||||
it('clears stale coding-agent runtime credentials when switching providers', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'codex'
|
||||
session.codingAgentId = 'codex'
|
||||
session.codingAgentMode = 'scoped'
|
||||
session.provider = 'xiaomi'
|
||||
session.model = 'mimo-v2.5-pro'
|
||||
session.baseUrl = 'https://api.xiaomimimo.com/v1'
|
||||
session.apiKey = 'sk-xiaomi'
|
||||
session.apiMode = 'chat_completions'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
const ok = await store.switchSessionModel('deepseek-v4-pro', 'deepseek', 'session-1', 'chat_completions')
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(sessionsApi.setSessionModel).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
'deepseek-v4-pro',
|
||||
'deepseek',
|
||||
'chat_completions',
|
||||
)
|
||||
expect(session.provider).toBe('deepseek')
|
||||
expect(session.model).toBe('deepseek-v4-pro')
|
||||
expect(session.baseUrl).toBeUndefined()
|
||||
expect(session.apiKey).toBeUndefined()
|
||||
expect(session.apiMode).toBe('chat_completions')
|
||||
})
|
||||
|
||||
|
||||
it('preserves a scoped coding-agent API mode when reselecting the same provider', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'codex'
|
||||
session.codingAgentId = 'codex'
|
||||
session.codingAgentMode = 'scoped'
|
||||
session.provider = 'fun-codex'
|
||||
session.model = 'gpt-5.4'
|
||||
session.apiMode = 'chat_completions'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
const ok = await store.switchSessionModel('gpt-5.5', 'fun-codex', 'session-1')
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(sessionsApi.setSessionModel).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
'gpt-5.5',
|
||||
'fun-codex',
|
||||
'chat_completions',
|
||||
)
|
||||
expect(session.apiMode).toBe('chat_completions')
|
||||
})
|
||||
|
||||
it('sends the selected workspace when starting a coding-agent run', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'claude'
|
||||
session.codingAgentId = 'claude-code'
|
||||
session.codingAgentMode = 'scoped'
|
||||
session.source = 'cli'
|
||||
session.provider = 'openrouter'
|
||||
session.model = 'anthropic/claude-sonnet-4.6'
|
||||
session.baseUrl = 'https://openrouter.ai/api/v1'
|
||||
session.apiKey = 'sk-test'
|
||||
session.apiMode = 'anthropic_messages'
|
||||
session.workspace = '/workspace/project-a'
|
||||
session.reasoningEffort = 'high'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
@@ -596,21 +453,62 @@ describe('chat store reasoning/tool boundaries', () => {
|
||||
await store.sendMessage('use this project')
|
||||
|
||||
expect(chatApi.startRunViaSocket.mock.calls[0][0]).toEqual(expect.objectContaining({
|
||||
source: 'coding_agent',
|
||||
coding_agent_id: 'claude-code',
|
||||
mode: 'scoped',
|
||||
source: 'cli',
|
||||
workspace: '/workspace/project-a',
|
||||
reasoning_effort: 'high',
|
||||
}))
|
||||
})
|
||||
|
||||
it('keeps a local stream controller for active coding-agent runs', async () => {
|
||||
it('sends only the bridge run fields on the run payload', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
await store.sendMessage('hello')
|
||||
|
||||
expect(Object.keys(chatApi.startRunViaSocket.mock.calls[0][0] as Record<string, unknown>).sort())
|
||||
.toEqual([
|
||||
'input',
|
||||
'model',
|
||||
'model_groups',
|
||||
'profile',
|
||||
'provider',
|
||||
'queue_id',
|
||||
'reasoning_effort',
|
||||
'session_id',
|
||||
'source',
|
||||
'workspace',
|
||||
])
|
||||
})
|
||||
|
||||
it('switches the session model without touching runtime credentials', async () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.provider = 'xiaomi'
|
||||
session.model = 'mimo-v2.5-pro'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
const ok = await store.switchSessionModel('deepseek-v4-pro', 'deepseek', 'session-1')
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(sessionsApi.setSessionModel).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
'deepseek-v4-pro',
|
||||
'deepseek',
|
||||
)
|
||||
expect(session.provider).toBe('deepseek')
|
||||
expect(session.model).toBe('deepseek-v4-pro')
|
||||
})
|
||||
|
||||
it('keeps a local stream controller for active runs', async () => {
|
||||
const abort = vi.fn()
|
||||
chatApi.startRunViaSocket.mockReturnValue({ abort })
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.source = 'coding_agent'
|
||||
session.agent = 'claude'
|
||||
session.codingAgentId = 'claude-code'
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CODING_AGENT_API_MODES,
|
||||
inferCodingAgentApiMode,
|
||||
normalizeCodingAgentApiMode,
|
||||
} from '@/api/coding-agents'
|
||||
|
||||
describe('coding agent api modes', () => {
|
||||
it('only exposes scoped coding-agent protocols supported by launch flows', () => {
|
||||
expect(CODING_AGENT_API_MODES).toEqual([
|
||||
'chat_completions',
|
||||
'codex_responses',
|
||||
'anthropic_messages',
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps supported provider api modes and downgrades unsupported ones', () => {
|
||||
expect(normalizeCodingAgentApiMode('anthropic_messages', 'codex_responses')).toBe('anthropic_messages')
|
||||
expect(normalizeCodingAgentApiMode('bedrock_converse', 'chat_completions')).toBe('chat_completions')
|
||||
expect(normalizeCodingAgentApiMode('codex_app_server', 'codex_responses')).toBe('codex_responses')
|
||||
})
|
||||
|
||||
it('infers a safe fallback protocol from provider identity and base URL', () => {
|
||||
expect(inferCodingAgentApiMode('anthropic', 'https://api.anthropic.com')).toBe('anthropic_messages')
|
||||
expect(inferCodingAgentApiMode('deepseek', 'https://api.deepseek.com')).toBe('chat_completions')
|
||||
expect(inferCodingAgentApiMode('xiaomi', 'https://api.xiaomimimo.com/v1')).toBe('chat_completions')
|
||||
expect(inferCodingAgentApiMode('openrouter', 'https://openrouter.ai/api/v1')).toBe('chat_completions')
|
||||
})
|
||||
})
|
||||
@@ -151,7 +151,7 @@ describe('SessionListItem', () => {
|
||||
})
|
||||
|
||||
const logo = wrapper.get('.session-item-agent-logo')
|
||||
expect(logo.attributes('src')).toBe('/coding-agents/hermes.png')
|
||||
expect(logo.attributes('src')).toBe('/logo.png')
|
||||
expect(logo.attributes('alt')).toBe('Hermes')
|
||||
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
||||
})
|
||||
@@ -159,7 +159,7 @@ describe('SessionListItem', () => {
|
||||
it('defaults old sessions without agent metadata to the Hermes logo', () => {
|
||||
const wrapper = mount(SessionListItem, {
|
||||
props: {
|
||||
session: { ...session, source: undefined, agent: undefined, codingAgentId: undefined },
|
||||
session: { ...session, source: undefined, agent: undefined },
|
||||
active: false,
|
||||
pinned: false,
|
||||
canDelete: true,
|
||||
@@ -172,50 +172,8 @@ describe('SessionListItem', () => {
|
||||
})
|
||||
|
||||
const logo = wrapper.get('.session-item-agent-logo')
|
||||
expect(logo.attributes('src')).toBe('/coding-agents/hermes.png')
|
||||
expect(logo.attributes('src')).toBe('/logo.png')
|
||||
expect(logo.attributes('alt')).toBe('Hermes')
|
||||
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders the Claude Code logo for Claude coding agent sessions', () => {
|
||||
const wrapper = mount(SessionListItem, {
|
||||
props: {
|
||||
session: { ...session, source: 'coding_agent', agent: 'claude', codingAgentId: 'claude-code' },
|
||||
active: false,
|
||||
pinned: false,
|
||||
canDelete: true,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
ProfileAvatar: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const logo = wrapper.get('.session-item-agent-logo')
|
||||
expect(logo.attributes('src')).toBe('/coding-agents/claude-code.svg')
|
||||
expect(logo.attributes('alt')).toBe('Claude Code')
|
||||
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders the Codex logo for Codex coding agent sessions', () => {
|
||||
const wrapper = mount(SessionListItem, {
|
||||
props: {
|
||||
session: { ...session, source: 'coding_agent', agent: 'codex', codingAgentId: 'codex' },
|
||||
active: false,
|
||||
pinned: false,
|
||||
canDelete: true,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
ProfileAvatar: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const logo = wrapper.get('.session-item-agent-logo')
|
||||
expect(logo.attributes('src')).toBe('/coding-agents/codex-openai.png')
|
||||
expect(logo.attributes('alt')).toBe('Codex')
|
||||
expect(wrapper.find('.session-item-agent-name').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user