refactor: Rename message stream handler and update session creation logic

This commit is contained in:
Vaayne 2025-09-17 14:10:10 +08:00
parent 219d162e1a
commit d1ff8591a6
20 changed files with 147 additions and 269 deletions

View File

@ -24,13 +24,13 @@ const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
return session return session
} }
export const createMessageStream = async (req: Request, res: Response): Promise<void> => { export const createMessage = async (req: Request, res: Response): Promise<void> => {
try { try {
const { agentId, sessionId } = req.params const { agentId, sessionId } = req.params
const session = await verifyAgentAndSession(agentId, sessionId) const session = await verifyAgentAndSession(agentId, sessionId)
const messageData = { ...req.body, session_id: sessionId } const messageData = req.body
logger.info(`Creating streaming message for session: ${sessionId}`) logger.info(`Creating streaming message for session: ${sessionId}`)
logger.debug('Streaming message data:', messageData) logger.debug('Streaming message data:', messageData)
@ -45,7 +45,7 @@ export const createMessageStream = async (req: Request, res: Response): Promise<
// Send initial connection event // Send initial connection event
res.write('data: {"type":"start"}\n\n') res.write('data: {"type":"start"}\n\n')
const messageStream = sessionMessageService.createSessionMessageStream(session, messageData) const messageStream = sessionMessageService.createSessionMessage(session, messageData)
// Track if the response has ended to prevent further writes // Track if the response has ended to prevent further writes
let responseEnded = false let responseEnded = false

View File

@ -1,19 +1,18 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { sessionMessageService, sessionService } from '@main/services/agents'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { sessionMessageService, sessionService } from '../../../../services/agents'
const logger = loggerService.withContext('ApiServerSessionsHandlers') const logger = loggerService.withContext('ApiServerSessionsHandlers')
export const createSession = async (req: Request, res: Response): Promise<Response> => { export const createSession = async (req: Request, res: Response): Promise<Response> => {
try { try {
const { agentId } = req.params const { agentId } = req.params
const sessionData = { ...req.body, main_agent_id: agentId } const sessionData = req.body
logger.info(`Creating new session for agent: ${agentId}`) logger.info(`Creating new session for agent: ${agentId}`)
logger.debug('Session data:', sessionData) logger.debug('Session data:', sessionData)
const session = await sessionService.createSession(sessionData) const session = await sessionService.createSession(agentId, sessionData)
logger.info(`Session created successfully: ${session.id}`) logger.info(`Session created successfully: ${session.id}`)
return res.status(201).json(session) return res.status(201).json(session)
@ -38,7 +37,7 @@ export const listSessions = async (req: Request, res: Response): Promise<Respons
logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${status}`) logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${status}`)
const result = await sessionService.listSessions(agentId, { limit, offset, status }) const result = await sessionService.listSessions(agentId, { limit, offset })
logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total}) for agent: ${agentId}`) logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total}) for agent: ${agentId}`)
return res.json({ return res.json({
@ -77,16 +76,16 @@ export const getSession = async (req: Request, res: Response): Promise<Response>
}) })
} }
// Verify session belongs to the agent // // Verify session belongs to the agent
logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`) // logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`)
return res.status(404).json({ // return res.status(404).json({
error: { // error: {
message: 'Session not found for this agent', // message: 'Session not found for this agent',
type: 'not_found', // type: 'not_found',
code: 'session_not_found' // code: 'session_not_found'
} // }
}) // })
} // }
// Fetch session messages // Fetch session messages
logger.info(`Fetching messages for session: ${sessionId}`) logger.info(`Fetching messages for session: ${sessionId}`)
@ -261,7 +260,7 @@ export const listAllSessions = async (req: Request, res: Response): Promise<Resp
logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`) logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`)
const result = await sessionService.listSessions(undefined, { limit, offset, status }) const result = await sessionService.listSessions(undefined, { limit, offset })
logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`) logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`)
return res.json({ return res.json({

View File

@ -188,7 +188,7 @@ const createMessagesRouter = (): express.Router => {
const messagesRouter = express.Router({ mergeParams: true }) const messagesRouter = express.Router({ mergeParams: true })
// Message CRUD routes (nested under agent/session) // Message CRUD routes (nested under agent/session)
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessageStream) messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
return messagesRouter return messagesRouter
} }

View File

@ -1,6 +1,5 @@
import { body } from 'express-validator' import { body } from 'express-validator'
export const validateSessionMessage = [ export const validateSessionMessage = [
body('role').notEmpty().isIn(['user', 'agent', 'system', 'tool']).withMessage('Valid role is required'),
body('content').notEmpty().isString().withMessage('Content must be a valid string') body('content').notEmpty().isString().withMessage('Content must be a valid string')
] ]

View File

@ -1,11 +1,11 @@
import { type Client, createClient } from '@libsql/client' import { type Client, createClient } from '@libsql/client'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { drizzle } from 'drizzle-orm/libsql' import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import * as schema from './database/schema'
import { MigrationService } from './database/MigrationService' import { MigrationService } from './database/MigrationService'
import * as schema from './database/schema'
import { dbPath } from './drizzle.config' import { dbPath } from './drizzle.config'
const logger = loggerService.withContext('BaseService') const logger = loggerService.withContext('BaseService')
@ -24,7 +24,7 @@ const logger = loggerService.withContext('BaseService')
*/ */
export abstract class BaseService { export abstract class BaseService {
protected static client: Client | null = null protected static client: Client | null = null
protected static db: ReturnType<typeof drizzle> | null = null protected static db: LibSQLDatabase<typeof schema> | null = null
protected static isInitialized = false protected static isInitialized = false
protected static initializationPromise: Promise<void> | null = null protected static initializationPromise: Promise<void> | null = null
protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths'] protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths']
@ -110,7 +110,7 @@ export abstract class BaseService {
} }
} }
protected get database(): ReturnType<typeof drizzle> { protected get database(): LibSQLDatabase<typeof schema> {
this.ensureInitialized() this.ensureInitialized()
return BaseService.db! return BaseService.db!
} }

View File

@ -3,6 +3,6 @@
*/ */
export * from './agents.schema' export * from './agents.schema'
export * from './sessions.schema'
export * from './messages.schema' export * from './messages.schema'
export * from './migrations.schema' export * from './migrations.schema'
export * from './sessions.schema'

View File

@ -1,4 +1,5 @@
import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { sessionsTable } from './sessions.schema' import { sessionsTable } from './sessions.schema'
// session_messages table to log all messages, thoughts, actions, observations in a session // session_messages table to log all messages, thoughts, actions, observations in a session

View File

@ -3,10 +3,12 @@
*/ */
import { foreignKey, index, sqliteTable, text } from 'drizzle-orm/sqlite-core' import { foreignKey, index, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { agentsTable } from './agents.schema' import { agentsTable } from './agents.schema'
export const sessionsTable = sqliteTable('sessions', { export const sessionsTable = sqliteTable('sessions', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
agent_type: text('agent_type').notNull(),
agent_id: text('agent_id').notNull(), // Primary agent ID for the session agent_id: text('agent_id').notNull(), // Primary agent ID for the session
name: text('name').notNull(), name: text('name').notNull(),
description: text('description'), description: text('description'),

View File

@ -16,12 +16,16 @@ function getDbPath() {
return path.join(app.getPath('userData'), 'agents.db') return path.join(app.getPath('userData'), 'agents.db')
} }
const resolvedDbPath = getDbPath()
export const dbPath = resolvedDbPath
export default defineConfig({ export default defineConfig({
dialect: 'sqlite', dialect: 'sqlite',
schema: './src/main/services/agents/database/schema/index.ts', schema: './src/main/services/agents/database/schema/index.ts',
out: './src/main/services/agents/database/drizzle', out: './src/main/services/agents/database/drizzle',
dbCredentials: { dbCredentials: {
url: `file:${getDbPath()}` url: `file:${resolvedDbPath}`
}, },
verbose: true, verbose: true,
strict: true strict: true

View File

@ -11,7 +11,7 @@ import { UIMessageChunk } from 'ai'
import { count, eq } from 'drizzle-orm' import { count, eq } from 'drizzle-orm'
import { BaseService } from '../BaseService' import { BaseService } from '../BaseService'
import { type InsertSessionMessageRow, sessionMessagesTable } from '../database/schema' import { sessionMessagesTable } from '../database/schema'
import ClaudeCodeService from './claudecode' import ClaudeCodeService from './claudecode'
const logger = loggerService.withContext('SessionMessageService') const logger = loggerService.withContext('SessionMessageService')
@ -76,19 +76,19 @@ export class SessionMessageService extends BaseService {
return { messages, total } return { messages, total }
} }
createSessionMessageStream(session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest): EventEmitter { createSessionMessage(session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest): EventEmitter {
this.ensureInitialized() this.ensureInitialized()
// Create a new EventEmitter to manage the session message lifecycle // Create a new EventEmitter to manage the session message lifecycle
const sessionStream = new EventEmitter() const sessionStream = new EventEmitter()
// No parent validation needed, start immediately // No parent validation needed, start immediately
this.startClaudeCodeStream(session, messageData, sessionStream) this.startSessionMessageStream(session, messageData, sessionStream)
return sessionStream return sessionStream
} }
private startClaudeCodeStream( private startSessionMessageStream(
session: GetAgentSessionResponse, session: GetAgentSessionResponse,
req: CreateSessionMessageRequest, req: CreateSessionMessageRequest,
sessionStream: EventEmitter sessionStream: EventEmitter
@ -99,7 +99,12 @@ export class SessionMessageService extends BaseService {
session_id = previousMessages[0].session_id session_id = previousMessages[0].session_id
} }
logger.debug('Claude Code stream message data:', { message: req, session_id }) logger.debug('Session Message stream message data:', { message: req, session_id })
if (session.agent_type !== 'claude-code') {
logger.error('Unsupported agent type for streaming:', { agent_type: session.agent_type })
throw new Error('Unsupported agent type for streaming')
}
// Create the streaming agent invocation (using invokeStream for streaming) // Create the streaming agent invocation (using invokeStream for streaming)
const claudeStream = this.cc.invoke(req.content, session.accessible_paths[0], session_id, { const claudeStream = this.cc.invoke(req.content, session.accessible_paths[0], session_id, {
@ -107,7 +112,6 @@ export class SessionMessageService extends BaseService {
maxTurns: session.configuration?.maxTurns || 10 maxTurns: session.configuration?.maxTurns || 10
}) })
let sessionMessage: AgentSessionMessageEntity | null = null
const streamedChunks: UIMessageChunk[] = [] const streamedChunks: UIMessageChunk[] = []
const rawAgentMessages: any[] = [] // Generic agent messages storage const rawAgentMessages: any[] = [] // Generic agent messages storage
@ -202,6 +206,10 @@ export class SessionMessageService extends BaseService {
// error: new Error('Failed to save session message to database') // error: new Error('Failed to save session message to database')
// }) // })
// } // }
sessionStream.emit('data', {
type: 'complete',
result: structuredContent
})
break break
} }

View File

@ -1,4 +1,5 @@
import type { import type {
AgentEntity,
AgentSessionEntity, AgentSessionEntity,
CreateSessionRequest, CreateSessionRequest,
GetAgentSessionResponse, GetAgentSessionResponse,
@ -25,18 +26,18 @@ export class SessionService extends BaseService {
await BaseService.initialize() await BaseService.initialize()
} }
async createSession(req: CreateSessionRequest): Promise<AgentSessionEntity> { async createSession(agentId: string, req: CreateSessionRequest): Promise<AgentSessionEntity> {
this.ensureInitialized() this.ensureInitialized()
// Validate agent exists - we'll need to import AgentService for this check // Validate agent exists - we'll need to import AgentService for this check
// For now, we'll skip this validation to avoid circular dependencies // For now, we'll skip this validation to avoid circular dependencies
// The database foreign key constraint will handle this // The database foreign key constraint will handle this
const agents = await this.database.select().from(agentsTable).where(eq(agentsTable.id, req.agent_id)).limit(1) const agents = await this.database.select().from(agentsTable).where(eq(agentsTable.id, agentId)).limit(1)
if (!agents[0]) { if (!agents[0]) {
throw new Error('Agent not found') throw new Error('Agent not found')
} }
const agent = this.deserializeJsonFields(agents[0]) as AgentSessionEntity const agent = this.deserializeJsonFields(agents[0]) as AgentEntity
const id = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` const id = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
const now = new Date().toISOString() const now = new Date().toISOString()
@ -51,15 +52,17 @@ export class SessionService extends BaseService {
const insertData: InsertSessionRow = { const insertData: InsertSessionRow = {
id, id,
agent_id: agentId,
agent_type: agent.type,
name: serializedData.name || null, name: serializedData.name || null,
agent_id: serializedData.agent_id,
description: serializedData.description || null, description: serializedData.description || null,
accessible_paths: serializedData.accessible_paths || null,
instructions: serializedData.instructions || null,
model: serializedData.model || null, model: serializedData.model || null,
plan_model: serializedData.plan_model || null, plan_model: serializedData.plan_model || null,
small_model: serializedData.small_model || null, small_model: serializedData.small_model || null,
mcps: serializedData.mcps || null, mcps: serializedData.mcps || null,
configuration: serializedData.configuration || null, configuration: serializedData.configuration || null,
accessible_paths: serializedData.accessible_paths || null,
created_at: now, created_at: now,
updated_at: now updated_at: now
} }

View File

@ -242,155 +242,6 @@ class ClaudeCodeService implements AgentServiceInterface {
process.on('error', () => clearTimeout(timeout)) process.on('error', () => clearTimeout(timeout))
} }
/**
* Set up process event handlers and return a promise that resolves with complete output
*/
private setupProcessHandlers(process: ChildProcess): Promise<ClaudeCodeResult> {
return new Promise((resolve, reject) => {
let stdoutData = ''
let stderrData = ''
const jsonOutput: any[] = []
let hasResolved = false
const startTime = Date.now()
// Handle stdout with proper encoding and buffering
if (process.stdout) {
process.stdout.setEncoding('utf8')
process.stdout.on('data', (data: string) => {
stdoutData += data
logger.debug('Agent stdout chunk:', { length: data.length })
// Parse JSON stream output line by line
const lines = data.split('\n')
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line.trim())
jsonOutput.push(parsed)
logger.silly('Parsed JSON output:', parsed)
} catch (e) {
// Not JSON, might be plain text output
logger.debug('Non-JSON stdout line:', { line: line.trim() })
}
}
}
})
process.stdout.on('end', () => {
logger.debug('Agent stdout stream ended')
})
}
// Handle stderr with proper encoding
if (process.stderr) {
process.stderr.setEncoding('utf8')
process.stderr.on('data', (data: string) => {
stderrData += data
logger.warn('Agent stderr chunk:', { data: data.trim() })
})
process.stderr.on('end', () => {
logger.debug('Agent stderr stream ended')
})
}
// Handle process exit
process.on('exit', (code, signal) => {
const duration = Date.now() - startTime
const success = code === 0
const status = success ? 'completed' : 'failed'
logger.info('Agent process exited', {
code,
signal,
success,
status,
duration,
stdoutLength: stdoutData.length,
stderrLength: stderrData.length,
jsonItems: jsonOutput.length
})
if (!hasResolved) {
hasResolved = true
resolve({
success,
stdout: stdoutData,
stderr: stderrData,
jsonOutput,
exitCode: code || undefined
})
}
})
// Handle process errors
process.on('error', (error) => {
const duration = Date.now() - startTime
logger.error('Agent process error:', {
error: error.message,
duration,
stdoutLength: stdoutData.length,
stderrLength: stderrData.length
})
if (!hasResolved) {
hasResolved = true
reject({
success: false,
stdout: stdoutData,
stderr: stderrData,
jsonOutput,
error
})
}
})
// Handle close event as a fallback
process.on('close', (code, signal) => {
const duration = Date.now() - startTime
logger.debug('Agent process closed', { code, signal, duration })
// Only resolve here if exit event hasn't fired
if (!hasResolved) {
hasResolved = true
const success = code === 0
resolve({
success,
stdout: stdoutData,
stderr: stderrData,
jsonOutput,
exitCode: code || undefined
})
}
})
// Set a timeout to prevent hanging indefinitely (reduced for debugging)
const timeout = setTimeout(() => {
if (!hasResolved) {
hasResolved = true
logger.error('Agent process timeout after 30 seconds', {
pid: process.pid,
stdoutLength: stdoutData.length,
stderrLength: stderrData.length,
jsonItems: jsonOutput.length
})
process.kill('SIGTERM')
reject({
success: false,
stdout: stdoutData,
stderr: stderrData,
jsonOutput,
error: new Error('Process timeout after 30 seconds')
})
}
}, 30 * 1000) // 30 seconds timeout for debugging
// Clear timeout when process ends
process.on('exit', () => clearTimeout(timeout))
process.on('error', () => clearTimeout(timeout))
})
}
} }
export default ClaudeCodeService export default ClaudeCodeService

View File

@ -16,5 +16,10 @@ export { sessionMessageService } from './SessionMessageService'
export { sessionService } from './SessionService' export { sessionService } from './SessionService'
// Type definitions for service requests and responses // Type definitions for service requests and responses
export type { AgentEntity, AgentSessionEntity,CreateAgentRequest, UpdateAgentRequest } from '@types'
export type { CreateSessionRequest, ListSessionsOptions, UpdateSessionRequest } from './SessionService' export type {
AgentSessionMessageEntity,
CreateSessionRequest,
GetAgentSessionResponse,
ListOptions as SessionListOptions,
UpdateSessionRequest} from '@types'

View File

@ -23,7 +23,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { AgentEntity, AgentType, isAgentType } from '@renderer/types' import { AgentEntity, AgentType, isAgentType } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { ChangeEvent, FormEvent, ReactNode, useCallback, useMemo, useRef, useState } from 'react' import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ErrorBoundary } from '../ErrorBoundary' import { ErrorBoundary } from '../ErrorBoundary'
@ -45,13 +45,23 @@ interface AgentTypeOption extends Option {
type ModelOption = Option type ModelOption = Option
type AgentForm = { type AgentForm = {
type: AgentEntity['type'] type: AgentType
name: AgentEntity['name'] name: string
description?: AgentEntity['description'] description?: string
instructions?: AgentEntity['instructions'] instructions?: string
model?: AgentEntity['model'] model: string
accessible_paths: string[]
} }
const buildAgentForm = (existing?: AgentEntity): AgentForm => ({
type: existing?.type ?? 'claude-code',
name: existing?.name ?? 'Claude Code',
description: existing?.description,
instructions: existing?.instructions,
model: existing?.model ?? 'claude-4-sonnet',
accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : []
})
interface BaseProps { interface BaseProps {
agent?: AgentEntity agent?: AgentEntity
} }
@ -88,16 +98,13 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
const { addAgent, updateAgent } = useAgents() const { addAgent, updateAgent } = useAgents()
const isEditing = (agent?: AgentEntity) => agent !== undefined const isEditing = (agent?: AgentEntity) => agent !== undefined
// default values. may change to undefined. const [form, setForm] = useState<AgentForm>(() => buildAgentForm(agent))
const [form, setForm] = useState<AgentForm>(
isEditing(agent) useEffect(() => {
? agent if (isOpen) {
: { setForm(buildAgentForm(agent))
type: 'claude-code', }
name: 'Claude Code', }, [agent, isOpen])
model: 'claude-4-sonnet'
}
)
const Option = useCallback( const Option = useCallback(
({ option }: { option?: Option | null }) => { ({ option }: { option?: Option | null }) => {
@ -222,44 +229,51 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
// Additional validation check besides native HTML validation to ensure security // Additional validation check besides native HTML validation to ensure security
if (!isAgentType(form.type)) { if (!isAgentType(form.type)) {
window.toast.error(t('agent.add.error.invalid_agent')) window.toast.error(t('agent.add.error.invalid_agent'))
loadingRef.current = false
return return
} }
if (form.model === undefined) { if (!form.model) {
window.toast.error(t('error.model.not_exists')) window.toast.error(t('error.model.not_exists'))
loadingRef.current = false
return return
} }
let _agent: AgentEntity let resultAgent: AgentEntity
if (isEditing(agent)) { if (isEditing(agent)) {
_agent = { if (!agent) {
...agent, throw new Error('Agent is required for editing mode')
// type: form.type, }
const updatePayload: Partial<AgentEntity> & { id: string } = {
id: agent.id,
name: form.name, name: form.name,
description: form.description, description: form.description,
instructions: form.instructions, instructions: form.instructions,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
model: form.model model: form.model
// avatar: getAvatar(form.type) }
} satisfies AgentEntity
updateAgent(_agent) updateAgent(updatePayload)
resultAgent = { ...agent, ...updatePayload }
window.toast.success(t('common.update_success')) window.toast.success(t('common.update_success'))
} else { } else {
_agent = { const now = new Date().toISOString()
resultAgent = {
id: uuid(), id: uuid(),
type: form.type, type: form.type,
name: form.name, name: form.name,
description: form.description, description: form.description,
instructions: form.instructions, instructions: form.instructions,
created_at: new Date().toISOString(), created_at: now,
updated_at: new Date().toISOString(), updated_at: now,
model: form.model, model: form.model,
avatar: getAvatar(form.type) accessible_paths: [...form.accessible_paths]
} satisfies AgentEntity }
addAgent(_agent) addAgent(resultAgent)
window.toast.success(t('common.add_success')) window.toast.success(t('common.add_success'))
} }
logger.debug('Agent', _agent) logger.debug('Agent mutation payload', { agent: resultAgent })
loadingRef.current = false loadingRef.current = false
setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0) setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
@ -271,6 +285,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
form.name, form.name,
form.description, form.description,
form.instructions, form.instructions,
form.accessible_paths,
agent, agent,
setTimeoutTimer, setTimeoutTimer,
onClose, onClose,
@ -339,8 +354,16 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
</SelectItem> </SelectItem>
)} )}
</Select> </Select>
<Textarea label={t('common.description')} value={form.description} onValueChange={onDescChange} /> <Textarea
<Textarea label={t('common.prompt')} value={form.instructions} onValueChange={onInstChange} /> label={t('common.description')}
value={form.description ?? ''}
onValueChange={onDescChange}
/>
<Textarea
label={t('common.prompt')}
value={form.instructions ?? ''}
onValueChange={onInstChange}
/>
</ModalBody> </ModalBody>
<ModalFooter className="w-full"> <ModalFooter className="w-full">
<Button onPress={onClose}>{t('common.close')}</Button> <Button onPress={onClose}>{t('common.close')}</Button>
@ -356,11 +379,3 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
</ErrorBoundary> </ErrorBoundary>
) )
} }
const getAvatar = (type: AgentType) => {
switch (type) {
case 'claude-code':
return ClaudeIcon
}
return undefined
}

View File

@ -1,7 +1,9 @@
import { AgentBase } from '@renderer/types' import { AgentBase } from '@renderer/types'
// base agent config. no default config for now. // base agent config. no default config for now.
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {} as const const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
accessible_paths: []
} as const
// no default config for now. // no default config for now.
export const DEFAULT_CLAUDE_CODE_CONFIG: Omit<AgentBase, 'model'> = { export const DEFAULT_CLAUDE_CODE_CONFIG: Omit<AgentBase, 'model'> = {

View File

@ -6,8 +6,9 @@ export const useUpdateAgent = () => {
// TODO: use api // TODO: use api
return useMutation({ return useMutation({
// @ts-expect-error not-implemented mutationFn: async (agentUpdate: Partial<AgentEntity> & { id: string }) => {
mutationFn: async ({}: Partial<AgentEntity> & { id: string }) => {}, throw new Error(`useUpdateAgent mutationFn not implemented for agent ${agentUpdate.id}`)
},
onSuccess: (updated: AgentEntity) => { onSuccess: (updated: AgentEntity) => {
qc.setQueryData<AgentEntity[]>(['todos'], (old) => qc.setQueryData<AgentEntity[]>(['todos'], (old) =>
old ? old.map((t) => (t.id === updated.id ? updated : t)) : [] old ? old.map((t) => (t.id === updated.id ? updated : t)) : []

View File

@ -22,13 +22,14 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
// const { agents } = useAgents() // const { agents } = useAgents()
const AgentLabel = useCallback(() => { const AgentLabel = useCallback(() => {
const displayName = agent.name ?? agent.id
return ( return (
<> <>
{agent.avatar && <Avatar className="h-6 w-6" src={agent.avatar} />} <Avatar className="h-6 w-6" name={displayName} />
<span className="text-sm">{agent.name}</span> <span className="text-sm">{displayName}</span>
</> </>
) )
}, [agent.avatar, agent.name]) }, [agent.id, agent.name])
const handleClick = () => logger.debug('not implemented') const handleClick = () => logger.debug('not implemented')
@ -37,7 +38,7 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
<ContextMenu modal={false}> <ContextMenu modal={false}>
<ContextMenuTrigger> <ContextMenuTrigger>
<Container onClick={handleClick} className={isActive ? 'active' : ''}> <Container onClick={handleClick} className={isActive ? 'active' : ''}>
<AssistantNameRow className="name" title={agent.name}> <AssistantNameRow className="name" title={agent.name ?? agent.id}>
<AgentLabel /> <AgentLabel />
</AssistantNameRow> </AssistantNameRow>
</Container> </Container>

View File

@ -2,7 +2,7 @@
* Database entity types for Agent, Session, and SessionMessage * Database entity types for Agent, Session, and SessionMessage
* Shared between main and renderer processes * Shared between main and renderer processes
*/ */
import { TextStreamPart, UIMessageChunk, ModelMessage } from 'ai' import { ModelMessage, TextStreamPart, UIMessageChunk } from 'ai'
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
export type SessionMessageRole = ModelMessage['role'] export type SessionMessageRole = ModelMessage['role']
export type AgentType = 'claude-code' export type AgentType = 'claude-code'
@ -75,15 +75,14 @@ export interface ListOptions {
export interface AgentSessionEntity extends AgentBase { export interface AgentSessionEntity extends AgentBase {
id: string id: string
agent_id: string // Primary agent ID for the session agent_id: string // Primary agent ID for the session
agent_type: AgentType
// sub_agent_ids?: string[] // Array of sub-agent IDs involved in the session // sub_agent_ids?: string[] // Array of sub-agent IDs involved in the session
created_at: string created_at: string
updated_at: string updated_at: string
} }
export interface CreateSessionRequest extends AgentBase { export type CreateSessionRequest = AgentBase
agent_id: string // Primary agent ID for the session
}
export interface UpdateSessionRequest extends Partial<AgentBase> {} export interface UpdateSessionRequest extends Partial<AgentBase> {}

View File

@ -16,13 +16,14 @@ Content-Type: application/json
"type": "claude-code", "type": "claude-code",
"model": "anthropic:claude-sonnet-4", "model": "anthropic:claude-sonnet-4",
"description": "An AI assistant specialized in code review and debugging", "description": "An AI assistant specialized in code review and debugging",
"avatar": "https://example.com/avatar.png",
"instructions": "You are a helpful coding assistant. Focus on writing clean, maintainable code and providing constructive feedback.", "instructions": "You are a helpful coding assistant. Focus on writing clean, maintainable code and providing constructive feedback.",
"accessible_paths": [ "accessible_paths": [
"/tmp/workspace" "/tmp/workspace"
], ],
"permission_mode": "acceptEdits", "configuration": {
"max_steps": 10 "permission_mode": "acceptEdits",
"max_turns": 5
}
} }
### Get Agent Details ### Get Agent Details
@ -42,14 +43,16 @@ Content-Type: application/json
{ {
"name": "Claude Code", "name": "Claude Code",
"type": "claude-code",
"model": "anthropic:claude-sonnet-4", "model": "anthropic:claude-sonnet-4",
"description": "An AI assistant specialized in code review and debugging", "description": "An AI assistant specialized in code review and debugging",
"avatar": "https://example.com/avatar.png",
"instructions": "You are a helpful coding assistant. Focus on writing clean, maintainable code and providing constructive feedback.", "instructions": "You are a helpful coding assistant. Focus on writing clean, maintainable code and providing constructive feedback.",
"accessible_paths": [ "accessible_paths": [
"/tmp/workspace" "/tmp/workspace"
], ],
"permission_mode": "acceptEdits", "configuration": {
"max_steps": 10 "permission_mode": "acceptEdits",
"max_turns": 5
}
} }

View File

@ -1,8 +1,8 @@
@host=http://localhost:23333 @host=http://localhost:23333
@token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194 @token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194
@agent_id=agent_1757947603408_t1y2mbnq4 @agent_id=agent_1758084953648_my4oxnbm3
@session_id=session_1757947684264_z2wcwn8t7 @session_id=session_1758087299124_2etavjo2x
### List Sessions ### List Sessions
GET {{host}}/v1/agents/{{agent_id}}/sessions GET {{host}}/v1/agents/{{agent_id}}/sessions
@ -15,29 +15,26 @@ POST {{host}}/v1/agents/{{agent_id}}/sessions
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{ {}
"name": "Joke telling Session",
"user_goal": "Tell me a funny joke"
}
### Get Session Details ### Get Session Details
GET {{host}}/v1/agents/{{agent_id}}/sessions/session_1757815260195_eldvompnv GET {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
### Delete Session ### Delete Session
DELETE {{host}}/v1/agents/{{agent_id}}/sessions/session_1757815245456_tfs6oogl0 DELETE {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
### Update Session ### Update Session
PUT {{host}}/v1/agents/{{agent_id}}/sessions/session_1757815281790_q4yxgdk74 PUT {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{ {
"name": "Code Review Session 1", "name": "Code Review Session 1",
"user_goal": "Review the newly implemented feature for bugs and improvements" "instructions": "Review the newly implemented feature for bugs and improvements"
} }
@ -47,17 +44,5 @@ Authorization: Bearer {{token}}
Content-Type: application/json Content-Type: application/json
{ {
"role": "user",
"content": "a joke about programmers"
}
### Create Session Message Stream
POST {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}/messages/stream
Authorization: Bearer {{token}}
Content-Type: application/json
{
"role": "user",
"content": "a joke about programmers" "content": "a joke about programmers"
} }