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
File diff suppressed because it is too large Load Diff
@@ -175,18 +175,11 @@ function mapHistoryMessages(messages: HermesMessage[]): Session['messages'] {
})
}
function codingAgentFields(summary: SessionSummary): Pick<Session, 'agent' | 'agentSessionId' | 'agentNativeSessionId' | 'codingAgentId' | 'codingAgentMode'> {
const isCodingAgentSession = summary.source === 'coding_agent' || summary.agent === 'claude' || summary.agent === 'codex'
function agentFields(summary: SessionSummary): Pick<Session, 'agent' | 'agentSessionId' | 'agentNativeSessionId'> {
return {
agent: summary.agent || undefined,
agentSessionId: summary.agent_session_id || undefined,
agentNativeSessionId: summary.agent_native_session_id || undefined,
codingAgentId: summary.agent === 'codex' ? 'codex' : summary.agent === 'claude' ? 'claude-code' : undefined,
codingAgentMode: isCodingAgentSession
? (summary.agent_mode === 'global' || summary.agent_mode === 'scoped'
? summary.agent_mode
: summary.provider === 'global' ? 'global' : 'scoped')
: undefined,
}
}
@@ -196,7 +189,7 @@ function sessionFromSummary(summary: SessionSummary, messages: Session['messages
profile: summary.profile || undefined,
title: summary.title || '',
source: summary.source,
...codingAgentFields(summary),
...agentFields(summary),
createdAt: summary.started_at * 1000,
updatedAt: (summary.last_active || summary.ended_at || summary.started_at) * 1000,
model: summary.model,
@@ -405,7 +398,7 @@ function sessionSummaryToSession(summary: SessionSummary): Session {
profile: summary.profile || undefined,
title: summary.title || '',
source: summary.source,
...codingAgentFields(summary),
...agentFields(summary),
createdAt: summary.started_at * 1000,
updatedAt: (summary.last_active || summary.started_at) * 1000,
model: summary.model,
@@ -49,7 +49,6 @@ import {
} from '@/api/hermes/workflow-socket'
import { fetchSkills } from '@/api/hermes/skills'
import { fetchSession } from '@/api/hermes/sessions'
import { inferCodingAgentApiMode, normalizeCodingAgentApiMode } from '@/api/coding-agents'
import { buildWorkflowSkillOptions, workflowAgentToSkillTarget } from '@/utils/hermes/workflow-skills'
import type {
WorkflowAgentNodeData,
@@ -171,8 +170,6 @@ let workflowRunsLoadingSeq = 0
const agentOptions = computed<WorkflowSelectOption[]>(() => [
{ label: 'Hermes', value: 'hermes' },
{ label: 'Claude Code', value: 'claude-code' },
{ label: 'Codex', value: 'codex' },
])
const modelGroups = computed<AvailableModelGroup[]>(() => appStore.modelGroups)
@@ -358,7 +355,6 @@ function makeNode(
agent: data.agent || agentOptions.value[0]?.value || 'hermes',
provider: data.provider || defaultModelSelection.value.provider,
model: data.model || defaultModelSelection.value.model,
apiMode: data.apiMode || defaultApiMode(data.provider || defaultModelSelection.value.provider),
input: data.input || '',
skills: data.skills || [],
images: data.images || [],
@@ -516,23 +512,14 @@ function closeWorkflowChatPanel() {
workflowChatPanelTitle.value = ''
}
function defaultApiMode(provider: string) {
const group = modelGroups.value.find(item => item.provider === provider)
return normalizeCodingAgentApiMode(
group?.api_mode,
inferCodingAgentApiMode(group?.provider || provider, group?.base_url),
)
}
function normalizeNodeModel(data: WorkflowAgentNodeData): Pick<WorkflowAgentNodeData, 'provider' | 'model' | 'apiMode'> {
function normalizeNodeModel(data: WorkflowAgentNodeData): Pick<WorkflowAgentNodeData, 'provider' | 'model'> {
const currentGroup = modelGroups.value.find(group => group.provider === data.provider)
if (currentGroup?.models.includes(data.model)) {
return { provider: data.provider, model: data.model, apiMode: data.apiMode || defaultApiMode(data.provider) }
return { provider: data.provider, model: data.model }
}
return {
provider: defaultModelSelection.value.provider,
model: defaultModelSelection.value.model,
apiMode: defaultApiMode(defaultModelSelection.value.provider),
}
}
@@ -578,7 +565,6 @@ function serializeWorkflowNodes(source: WorkflowNode[]): unknown[] {
agent: node.data.agent,
provider: node.data.provider,
model: node.data.model,
apiMode: node.data.apiMode,
input: node.data.input,
skills: [...node.data.skills],
images: [...node.data.images],
@@ -624,7 +610,6 @@ function normalizeStoredNode(raw: unknown, index: number): WorkflowNode {
agent: data.agent,
provider: data.provider,
model: data.model,
apiMode: data.apiMode,
input: data.input,
skills: Array.isArray(data.skills) ? data.skills.filter(item => typeof item === 'string') : [],
images: Array.isArray(data.images) ? data.images.filter(item => typeof item === 'string') : [],
@@ -1338,9 +1323,6 @@ function workflowValidationError(): string | null {
if (!node.data.title.trim()) return t('workflow.validation.nodeNameRequired', { node: node.id })
if (!node.data.provider.trim()) return t('workflow.validation.providerRequired', { node: label })
if (!node.data.model.trim()) return t('workflow.validation.modelRequired', { node: label })
if (node.data.agent !== 'hermes' && !node.data.apiMode) {
return t('workflow.validation.apiModeRequired', { node: label })
}
if (!node.data.input.trim()) return t('workflow.validation.inputRequired', { node: label })
}