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:
@@ -1,7 +1,6 @@
|
||||
import { startRunViaSocket, resumeSession, registerSessionHandlers, unregisterSessionHandlers, getChatRunSocket, respondToolApproval, onPeerUserMessage, onSessionCommand, onSessionTitleUpdated, onSessionWorkspaceUpdated, respondClarify, type ChatRunTransport, type RunEvent, type ResumeSessionPayload, type StartRunRequest, type ContentBlock as ContentBlockImport } from '@/api/hermes/chat'
|
||||
import { archiveSession as archiveSessionApi, deleteSession as deleteSessionApi, fetchSessionMessagesPage, fetchSessions, fetchWorkspaceRunChangeFile, fetchWorkspaceRunChangesForSession, setSessionModel, type HermesMessage, type SessionSummary, type WorkspaceRunChangeFileDetail, type WorkspaceRunChangeSummary } from '@/api/hermes/sessions'
|
||||
import { getActiveProfileName } from '@/api/client'
|
||||
import { inferCodingAgentApiMode, normalizeCodingAgentApiMode, type ChatCodingAgentId } from '@/api/coding-agents'
|
||||
import { getDownloadUrl } from '@/api/hermes/download'
|
||||
import type { ProviderApiMode } from '@/api/hermes/system'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -21,21 +20,7 @@ export type ContentBlock = ContentBlockImport
|
||||
export const LIVE_CHAT_MESSAGE_PAGE_SIZE = 150
|
||||
export const LIVE_CHAT_MAX_LOADED_MESSAGES = 300
|
||||
const WORKSPACE_RUN_CHANGE_MESSAGE_PREFIX = 'workspace-run-change:'
|
||||
type ChatAgentId = 'hermes' | 'claude' | 'codex' | 'ekko-agent'
|
||||
|
||||
function agentToCodingAgentId(agent?: string): ChatCodingAgentId | undefined {
|
||||
if (agent === 'codex') return 'codex'
|
||||
if (agent === 'claude') return 'claude-code'
|
||||
if (agent === 'ekko-agent') return 'ekko-agent'
|
||||
return undefined
|
||||
}
|
||||
|
||||
function codingAgentIdToAgent(id?: ChatCodingAgentId): ChatAgentId | undefined {
|
||||
if (id === 'codex') return 'codex'
|
||||
if (id === 'claude-code') return 'claude'
|
||||
if (id === 'ekko-agent') return 'ekko-agent'
|
||||
return undefined
|
||||
}
|
||||
type ChatAgentId = 'hermes'
|
||||
|
||||
function moaReferenceLabel(evt: RunEvent): string {
|
||||
const label = typeof evt.label === 'string' && evt.label.trim()
|
||||
@@ -113,8 +98,6 @@ export interface Session {
|
||||
agent?: string
|
||||
agentSessionId?: string
|
||||
agentNativeSessionId?: string
|
||||
codingAgentId?: ChatCodingAgentId
|
||||
codingAgentMode?: 'global' | 'scoped'
|
||||
messages: Message[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
@@ -540,13 +523,6 @@ function lastVisibleMessageRole(messages?: Message[] | null): string | null {
|
||||
}
|
||||
|
||||
function mapHermesSession(s: SessionSummary): Session {
|
||||
const codingAgentId = agentToCodingAgentId(s.agent)
|
||||
const isCodingAgentSession = s.source === 'coding_agent' || Boolean(codingAgentId)
|
||||
const codingAgentMode = isCodingAgentSession
|
||||
? (s.agent_mode === 'global' || s.agent_mode === 'scoped'
|
||||
? s.agent_mode
|
||||
: s.provider === 'global' ? 'global' : 'scoped')
|
||||
: undefined
|
||||
const activitySeconds = sessionActivitySeconds(s)
|
||||
return {
|
||||
id: s.id,
|
||||
@@ -556,8 +532,6 @@ function mapHermesSession(s: SessionSummary): Session {
|
||||
agent: s.agent || undefined,
|
||||
agentSessionId: s.agent_session_id || undefined,
|
||||
agentNativeSessionId: s.agent_native_session_id || undefined,
|
||||
codingAgentId,
|
||||
codingAgentMode,
|
||||
messages: [],
|
||||
createdAt: Math.round(s.started_at * 1000),
|
||||
updatedAt: Math.round(activitySeconds * 1000),
|
||||
@@ -605,22 +579,6 @@ function runtimeStoragePrefix(): string {
|
||||
function storageKey(): string { return runtimeStoragePrefix() + getProfileName() }
|
||||
function legacyStorageKey(): string | null { return activeRuntimeMode === 'default' && getProfileName() === 'default' ? LEGACY_STORAGE_KEY : null }
|
||||
|
||||
function isCodingAgentLikeSession(session?: Pick<Session, 'source' | 'agent' | 'codingAgentId'> | null): boolean {
|
||||
return session?.source === 'coding_agent' ||
|
||||
Boolean(session?.codingAgentId) ||
|
||||
Boolean(agentToCodingAgentId(session?.agent))
|
||||
}
|
||||
|
||||
function clearCodingAgentRuntimeCredentials(session?: Session | null) {
|
||||
if (!session || !isCodingAgentLikeSession(session)) return
|
||||
session.baseUrl = undefined
|
||||
session.apiKey = undefined
|
||||
}
|
||||
|
||||
function shouldPreserveRuntimeApiMode(session?: Session | null): boolean {
|
||||
return isCodingAgentLikeSession(session) && session?.codingAgentMode !== 'global'
|
||||
}
|
||||
|
||||
function isQuotaExceededError(error: unknown): boolean {
|
||||
if (!error || typeof error !== 'object') return false
|
||||
const e = error as { name?: string, code?: number }
|
||||
@@ -1161,26 +1119,20 @@ export const useChatStore = defineStore('chat', () => {
|
||||
profile?: string
|
||||
model?: string
|
||||
provider?: string
|
||||
source?: 'api_server' | 'cli' | 'coding_agent' | 'global_agent' | 'workflow'
|
||||
source?: 'api_server' | 'cli' | 'global_agent' | 'workflow'
|
||||
agent?: ChatAgentId
|
||||
codingAgentId?: ChatCodingAgentId
|
||||
codingAgentMode?: 'global' | 'scoped'
|
||||
workspace?: string | null
|
||||
baseUrl?: string
|
||||
apiKey?: string
|
||||
apiMode?: ProviderApiMode
|
||||
} = {}): Session {
|
||||
const source = runtimeMode.value === 'global_agent' ? 'global_agent' : options.source || 'cli'
|
||||
const codingAgentId = options.codingAgentId || agentToCodingAgentId(options.agent)
|
||||
const codingAgentMode = codingAgentId ? (options.codingAgentMode || 'scoped') : undefined
|
||||
const session: Session = {
|
||||
id: uid(),
|
||||
profile: options.profile || useProfilesStore().activeProfileName || 'default',
|
||||
title: '',
|
||||
source,
|
||||
agent: options.agent || codingAgentIdToAgent(codingAgentId) || 'hermes',
|
||||
codingAgentId,
|
||||
codingAgentMode,
|
||||
agent: options.agent || 'hermes',
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
@@ -1446,10 +1398,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
profile?: string
|
||||
model?: string
|
||||
provider?: string
|
||||
source?: 'api_server' | 'cli' | 'coding_agent' | 'global_agent' | 'workflow'
|
||||
source?: 'api_server' | 'cli' | 'global_agent' | 'workflow'
|
||||
agent?: ChatAgentId
|
||||
codingAgentId?: ChatCodingAgentId
|
||||
codingAgentMode?: 'global' | 'scoped'
|
||||
workspace?: string | null
|
||||
baseUrl?: string
|
||||
apiKey?: string
|
||||
@@ -1457,16 +1407,12 @@ export const useChatStore = defineStore('chat', () => {
|
||||
} = {}): Session {
|
||||
const appStore = useAppStore()
|
||||
const storageSource = runtimeMode.value === 'global_agent' ? 'global_agent' : options.source || 'cli'
|
||||
const codingAgentId = options.codingAgentId || agentToCodingAgentId(options.agent)
|
||||
const isGlobalCodingAgent = Boolean(codingAgentId) && options.codingAgentMode === 'global'
|
||||
const session = createSession({
|
||||
profile: options.profile,
|
||||
model: isGlobalCodingAgent ? undefined : options.model || appStore.selectedModel || undefined,
|
||||
provider: isGlobalCodingAgent ? '' : options.provider || appStore.selectedProvider || '',
|
||||
model: options.model || appStore.selectedModel || undefined,
|
||||
provider: options.provider || appStore.selectedProvider || '',
|
||||
source: storageSource,
|
||||
agent: options.agent,
|
||||
codingAgentId,
|
||||
codingAgentMode: options.codingAgentMode,
|
||||
workspace: options.workspace,
|
||||
baseUrl: options.baseUrl,
|
||||
apiKey: options.apiKey,
|
||||
@@ -1476,33 +1422,20 @@ export const useChatStore = defineStore('chat', () => {
|
||||
return session
|
||||
}
|
||||
|
||||
async function switchSessionModel(modelId: string, provider?: string, sessionId?: string, apiMode?: ProviderApiMode): Promise<boolean> {
|
||||
async function switchSessionModel(modelId: string, provider?: string, sessionId?: string): Promise<boolean> {
|
||||
const targetId = sessionId || activeSession.value?.id
|
||||
if (!targetId) return false
|
||||
const target = sessions.value.find(s => s.id === targetId)
|
||||
const activeTarget = activeSession.value?.id === targetId ? activeSession.value : null
|
||||
const previousProvider = String(target?.provider ?? activeTarget?.provider ?? '')
|
||||
const nextProvider = provider || ''
|
||||
const shouldClearRuntimeCredentials = previousProvider !== nextProvider && (
|
||||
isCodingAgentLikeSession(target) || isCodingAgentLikeSession(activeTarget)
|
||||
)
|
||||
const preservedApiMode = apiMode || (previousProvider === nextProvider
|
||||
? (shouldPreserveRuntimeApiMode(target) ? target?.apiMode : undefined) ||
|
||||
(shouldPreserveRuntimeApiMode(activeTarget) ? activeTarget?.apiMode : undefined)
|
||||
: undefined)
|
||||
const ok = await setSessionModel(targetId, modelId, provider || '', preservedApiMode)
|
||||
const ok = await setSessionModel(targetId, modelId, provider || '')
|
||||
if (!ok) return false
|
||||
if (target) {
|
||||
target.model = modelId
|
||||
target.provider = provider || ''
|
||||
target.apiMode = preservedApiMode
|
||||
if (shouldClearRuntimeCredentials) clearCodingAgentRuntimeCredentials(target)
|
||||
}
|
||||
if (activeTarget) {
|
||||
activeTarget.model = modelId
|
||||
activeTarget.provider = provider || ''
|
||||
activeTarget.apiMode = preservedApiMode
|
||||
if (shouldClearRuntimeCredentials) clearCodingAgentRuntimeCredentials(activeTarget)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1882,7 +1815,6 @@ export const useChatStore = defineStore('chat', () => {
|
||||
function handleAgentEvent(evt: RunEvent) {
|
||||
const sid = evt.session_id
|
||||
if (!sid) return
|
||||
if ((evt as any).source === 'coding_agent' && (evt as any).kind === 'status') return
|
||||
const text = String((evt as any).text || (evt as any).message || '').trim()
|
||||
if (!text) return
|
||||
|
||||
@@ -2237,18 +2169,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`
|
||||
}
|
||||
|
||||
function completionNotificationAgent(session: Session): { icon: string } {
|
||||
const codingAgentId = session.codingAgentId || agentToCodingAgentId(session.agent)
|
||||
if (codingAgentId === 'codex') {
|
||||
return { icon: '/coding-agents/codex-openai.png' }
|
||||
}
|
||||
if (codingAgentId === 'claude-code') {
|
||||
return { icon: '/coding-agents/claude-code.svg' }
|
||||
}
|
||||
if (codingAgentId === 'ekko-agent') {
|
||||
return { icon: '/coding-agents/ekko-agent.png' }
|
||||
}
|
||||
return { icon: '/coding-agents/hermes.png' }
|
||||
function completionNotificationAgent(_session: Session): { icon: string } {
|
||||
return { icon: '/favicon.ico' }
|
||||
}
|
||||
|
||||
function completionNotificationBody(session: Session, message?: Message): string {
|
||||
@@ -2292,15 +2214,14 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const shouldSendInitialSessionConfig = activeSession.value
|
||||
? activeSession.value.messageCount == null || activeSession.value.messageCount === 0
|
||||
: false
|
||||
const isCodingAgentSession = isCodingAgentLikeSession(activeSession.value)
|
||||
const isBridgeSlashCommand = !isCodingAgentSession && isKnownBridgeSessionCommand(trimmedContent)
|
||||
const isBridgeSlashCommand = isKnownBridgeSessionCommand(trimmedContent)
|
||||
const isBridgeCompressCommand = isBridgeSlashCommand && /^\/compress(?:\s|$)/i.test(trimmedContent)
|
||||
const isBridgePlanCommand = isBridgeSlashCommand && /^\/plan(?:\s|$)/i.test(trimmedContent)
|
||||
const isBridgeSkillCommand = isBridgeSlashCommand && /^\/skill(?:\s|$)/i.test(trimmedContent)
|
||||
const isBridgeMoaCommand = isBridgeSlashCommand && /^\/moa(?:\s|$)/i.test(trimmedContent)
|
||||
const isBridgeGoalCommand = isBridgeSlashCommand && /^\/goal(?:\s|$)/i.test(trimmedContent)
|
||||
const isBridgeForkCommand = isBridgeSlashCommand && /^\/fork(?:\s|$)/i.test(trimmedContent)
|
||||
const shouldOptimisticallyShowRunStatus = !isCodingAgentSession && !isBridgeForkCommand
|
||||
const shouldOptimisticallyShowRunStatus = !isBridgeForkCommand
|
||||
const wasLiveBeforeSend = isSessionLive(sid)
|
||||
if (isBridgeForkCommand) {
|
||||
if (pendingForkCommands.value.has(sid)) return
|
||||
@@ -2380,42 +2301,20 @@ export const useChatStore = defineStore('chat', () => {
|
||||
? appStore.profileModelGroups.find(entry => entry.profile === sessionProfile)?.groups
|
||||
: undefined
|
||||
const runModelGroups = profileModelGroups?.length ? profileModelGroups : appStore.modelGroups
|
||||
const providerGroup = runModelGroups.find(group => group.provider === sessionProvider)
|
||||
const storedSource = activeSession.value?.source
|
||||
const sessionSource: StartRunRequest['source'] = storedSource === 'global_agent'
|
||||
? 'global_agent'
|
||||
: storedSource === 'workflow'
|
||||
? 'workflow'
|
||||
: isCodingAgentSession
|
||||
? 'coding_agent'
|
||||
: storedSource === 'api_server'
|
||||
: storedSource === 'api_server'
|
||||
? 'api_server'
|
||||
: 'cli'
|
||||
const isCodingAgentExecution = sessionSource === 'coding_agent' || (sessionSource === 'workflow' && isCodingAgentSession)
|
||||
const codingAgentId: ChatCodingAgentId =
|
||||
activeSession.value?.codingAgentId ||
|
||||
agentToCodingAgentId(activeSession.value?.agent) ||
|
||||
'claude-code'
|
||||
const codingAgentMode = activeSession.value?.codingAgentMode || 'scoped'
|
||||
const codingAgentApiMode = isCodingAgentExecution && codingAgentId !== 'ekko-agent' && codingAgentMode !== 'global'
|
||||
? normalizeCodingAgentApiMode(
|
||||
activeSession.value?.apiMode || providerGroup?.api_mode,
|
||||
inferCodingAgentApiMode(
|
||||
sessionProvider || providerGroup?.provider,
|
||||
activeSession.value?.baseUrl || providerGroup?.base_url,
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
const runPayload: StartRunRequest = {
|
||||
input,
|
||||
session_id: sid,
|
||||
profile: sessionProfile,
|
||||
model: isCodingAgentExecution
|
||||
? (codingAgentMode === 'global' ? undefined : sessionModel || undefined)
|
||||
: shouldSendInitialSessionConfig ? sessionModel || undefined : undefined,
|
||||
provider: isCodingAgentExecution
|
||||
? (codingAgentMode === 'global' ? undefined : sessionProvider || undefined)
|
||||
: shouldSendInitialSessionConfig ? sessionProvider || undefined : undefined,
|
||||
model: shouldSendInitialSessionConfig ? sessionModel || undefined : undefined,
|
||||
provider: shouldSendInitialSessionConfig ? sessionProvider || undefined : undefined,
|
||||
model_groups: runModelGroups.map(group => ({
|
||||
provider: group.provider,
|
||||
models: group.models,
|
||||
@@ -2425,22 +2324,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
source: sessionSource,
|
||||
...(runtimeMode.value === 'global_agent' ? { session_source: 'global_agent' as const } : {}),
|
||||
...(sessionSource === 'workflow' ? { session_source: 'workflow' as const } : {}),
|
||||
...(isCodingAgentExecution
|
||||
? {
|
||||
coding_agent_id: codingAgentId,
|
||||
mode: codingAgentMode,
|
||||
baseUrl: codingAgentMode === 'global' ? undefined : activeSession.value?.baseUrl || providerGroup?.base_url || undefined,
|
||||
apiKey: codingAgentMode === 'global' ? undefined : activeSession.value?.apiKey || providerGroup?.api_key || undefined,
|
||||
apiMode: codingAgentApiMode,
|
||||
}
|
||||
: {}),
|
||||
// Per-session reasoning effort override. Hermes bridge and scoped coding
|
||||
// agents both consume this when the selected provider/API supports it.
|
||||
// Global coding-agent mode uses the user's native CLI config, so avoid
|
||||
// injecting a per-session override there.
|
||||
reasoning_effort: isCodingAgentExecution && codingAgentMode === 'global'
|
||||
? undefined
|
||||
: activeSession.value?.reasoningEffort || undefined,
|
||||
// Per-session reasoning effort override. Empty/undefined falls back to
|
||||
// the profile default in config.yaml.
|
||||
reasoning_effort: activeSession.value?.reasoningEffort || undefined,
|
||||
}
|
||||
if (shouldSendInitialSessionConfig && activeSession.value) {
|
||||
activeSession.value.messageCount = Math.max(activeSession.value.messageCount || 0, 1)
|
||||
@@ -3143,10 +3029,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
)
|
||||
runSubmitted = true
|
||||
|
||||
if (isCodingAgentSession) {
|
||||
serverWorking.value.add(sid)
|
||||
streamStates.value.set(sid, ctrl)
|
||||
} else if (!isBridgeSlashCommand || isBridgeCompressCommand || isBridgePlanCommand || isBridgeGoalCommand) {
|
||||
if (!isBridgeSlashCommand || isBridgeCompressCommand || isBridgePlanCommand || isBridgeGoalCommand) {
|
||||
streamStates.value.set(sid, ctrl)
|
||||
}
|
||||
} catch (err: any) {
|
||||
|
||||
Reference in New Issue
Block a user