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:
2026-10-16 13:27:50 +08:00
parent c93e1fe1bd
commit 8c1ddf415f
123 changed files with 567 additions and 23599 deletions
@@ -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