mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
refactor: Rename message stream handler and update session creation logic
This commit is contained in:
parent
219d162e1a
commit
d1ff8591a6
@ -24,13 +24,13 @@ const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
|
||||
return session
|
||||
}
|
||||
|
||||
export const createMessageStream = async (req: Request, res: Response): Promise<void> => {
|
||||
export const createMessage = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { agentId, sessionId } = req.params
|
||||
|
||||
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.debug('Streaming message data:', messageData)
|
||||
@ -45,7 +45,7 @@ export const createMessageStream = async (req: Request, res: Response): Promise<
|
||||
// Send initial connection event
|
||||
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
|
||||
let responseEnded = false
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { sessionMessageService, sessionService } from '@main/services/agents'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { sessionMessageService, sessionService } from '../../../../services/agents'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerSessionsHandlers')
|
||||
|
||||
export const createSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
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.debug('Session data:', sessionData)
|
||||
|
||||
const session = await sessionService.createSession(sessionData)
|
||||
const session = await sessionService.createSession(agentId, sessionData)
|
||||
|
||||
logger.info(`Session created successfully: ${session.id}`)
|
||||
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}`)
|
||||
|
||||
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}`)
|
||||
return res.json({
|
||||
@ -77,16 +76,16 @@ export const getSession = async (req: Request, res: Response): Promise<Response>
|
||||
})
|
||||
}
|
||||
|
||||
// Verify session belongs to the agent
|
||||
logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found for this agent',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
// // Verify session belongs to the agent
|
||||
// logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`)
|
||||
// return res.status(404).json({
|
||||
// error: {
|
||||
// message: 'Session not found for this agent',
|
||||
// type: 'not_found',
|
||||
// code: 'session_not_found'
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// Fetch session messages
|
||||
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}`)
|
||||
|
||||
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})`)
|
||||
return res.json({
|
||||
|
||||
@ -188,7 +188,7 @@ const createMessagesRouter = (): express.Router => {
|
||||
const messagesRouter = express.Router({ mergeParams: true })
|
||||
|
||||
// Message CRUD routes (nested under agent/session)
|
||||
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessageStream)
|
||||
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
|
||||
return messagesRouter
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { body } from 'express-validator'
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { type Client, createClient } from '@libsql/client'
|
||||
import { loggerService } from '@logger'
|
||||
import { drizzle } from 'drizzle-orm/libsql'
|
||||
import { drizzle, type LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import * as schema from './database/schema'
|
||||
import { MigrationService } from './database/MigrationService'
|
||||
import * as schema from './database/schema'
|
||||
import { dbPath } from './drizzle.config'
|
||||
|
||||
const logger = loggerService.withContext('BaseService')
|
||||
@ -24,7 +24,7 @@ const logger = loggerService.withContext('BaseService')
|
||||
*/
|
||||
export abstract class BaseService {
|
||||
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 initializationPromise: Promise<void> | null = null
|
||||
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()
|
||||
return BaseService.db!
|
||||
}
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
*/
|
||||
|
||||
export * from './agents.schema'
|
||||
export * from './sessions.schema'
|
||||
export * from './messages.schema'
|
||||
export * from './migrations.schema'
|
||||
export * from './sessions.schema'
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { foreignKey, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { sessionsTable } from './sessions.schema'
|
||||
|
||||
// session_messages table to log all messages, thoughts, actions, observations in a session
|
||||
|
||||
@ -3,10 +3,12 @@
|
||||
*/
|
||||
|
||||
import { foreignKey, index, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { agentsTable } from './agents.schema'
|
||||
|
||||
export const sessionsTable = sqliteTable('sessions', {
|
||||
id: text('id').primaryKey(),
|
||||
agent_type: text('agent_type').notNull(),
|
||||
agent_id: text('agent_id').notNull(), // Primary agent ID for the session
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
|
||||
@ -16,12 +16,16 @@ function getDbPath() {
|
||||
return path.join(app.getPath('userData'), 'agents.db')
|
||||
}
|
||||
|
||||
const resolvedDbPath = getDbPath()
|
||||
|
||||
export const dbPath = resolvedDbPath
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'sqlite',
|
||||
schema: './src/main/services/agents/database/schema/index.ts',
|
||||
out: './src/main/services/agents/database/drizzle',
|
||||
dbCredentials: {
|
||||
url: `file:${getDbPath()}`
|
||||
url: `file:${resolvedDbPath}`
|
||||
},
|
||||
verbose: true,
|
||||
strict: true
|
||||
|
||||
@ -11,7 +11,7 @@ import { UIMessageChunk } from 'ai'
|
||||
import { count, eq } from 'drizzle-orm'
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import { type InsertSessionMessageRow, sessionMessagesTable } from '../database/schema'
|
||||
import { sessionMessagesTable } from '../database/schema'
|
||||
import ClaudeCodeService from './claudecode'
|
||||
|
||||
const logger = loggerService.withContext('SessionMessageService')
|
||||
@ -76,19 +76,19 @@ export class SessionMessageService extends BaseService {
|
||||
return { messages, total }
|
||||
}
|
||||
|
||||
createSessionMessageStream(session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest): EventEmitter {
|
||||
createSessionMessage(session: GetAgentSessionResponse, messageData: CreateSessionMessageRequest): EventEmitter {
|
||||
this.ensureInitialized()
|
||||
|
||||
// Create a new EventEmitter to manage the session message lifecycle
|
||||
const sessionStream = new EventEmitter()
|
||||
|
||||
// No parent validation needed, start immediately
|
||||
this.startClaudeCodeStream(session, messageData, sessionStream)
|
||||
this.startSessionMessageStream(session, messageData, sessionStream)
|
||||
|
||||
return sessionStream
|
||||
}
|
||||
|
||||
private startClaudeCodeStream(
|
||||
private startSessionMessageStream(
|
||||
session: GetAgentSessionResponse,
|
||||
req: CreateSessionMessageRequest,
|
||||
sessionStream: EventEmitter
|
||||
@ -99,7 +99,12 @@ export class SessionMessageService extends BaseService {
|
||||
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)
|
||||
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
|
||||
})
|
||||
|
||||
let sessionMessage: AgentSessionMessageEntity | null = null
|
||||
const streamedChunks: UIMessageChunk[] = []
|
||||
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')
|
||||
// })
|
||||
// }
|
||||
sessionStream.emit('data', {
|
||||
type: 'complete',
|
||||
result: structuredContent
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AgentEntity,
|
||||
AgentSessionEntity,
|
||||
CreateSessionRequest,
|
||||
GetAgentSessionResponse,
|
||||
@ -25,18 +26,18 @@ export class SessionService extends BaseService {
|
||||
await BaseService.initialize()
|
||||
}
|
||||
|
||||
async createSession(req: CreateSessionRequest): Promise<AgentSessionEntity> {
|
||||
async createSession(agentId: string, req: CreateSessionRequest): Promise<AgentSessionEntity> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// Validate agent exists - we'll need to import AgentService for this check
|
||||
// For now, we'll skip this validation to avoid circular dependencies
|
||||
// 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]) {
|
||||
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 now = new Date().toISOString()
|
||||
@ -51,15 +52,17 @@ export class SessionService extends BaseService {
|
||||
|
||||
const insertData: InsertSessionRow = {
|
||||
id,
|
||||
agent_id: agentId,
|
||||
agent_type: agent.type,
|
||||
name: serializedData.name || null,
|
||||
agent_id: serializedData.agent_id,
|
||||
description: serializedData.description || null,
|
||||
accessible_paths: serializedData.accessible_paths || null,
|
||||
instructions: serializedData.instructions || null,
|
||||
model: serializedData.model || null,
|
||||
plan_model: serializedData.plan_model || null,
|
||||
small_model: serializedData.small_model || null,
|
||||
mcps: serializedData.mcps || null,
|
||||
configuration: serializedData.configuration || null,
|
||||
accessible_paths: serializedData.accessible_paths || null,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
|
||||
@ -242,155 +242,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
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
|
||||
|
||||
@ -16,5 +16,10 @@ export { sessionMessageService } from './SessionMessageService'
|
||||
export { sessionService } from './SessionService'
|
||||
|
||||
// Type definitions for service requests and responses
|
||||
|
||||
export type { CreateSessionRequest, ListSessionsOptions, UpdateSessionRequest } from './SessionService'
|
||||
export type { AgentEntity, AgentSessionEntity,CreateAgentRequest, UpdateAgentRequest } from '@types'
|
||||
export type {
|
||||
AgentSessionMessageEntity,
|
||||
CreateSessionRequest,
|
||||
GetAgentSessionResponse,
|
||||
ListOptions as SessionListOptions,
|
||||
UpdateSessionRequest} from '@types'
|
||||
|
||||
@ -23,7 +23,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { AgentEntity, AgentType, isAgentType } from '@renderer/types'
|
||||
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 { ErrorBoundary } from '../ErrorBoundary'
|
||||
@ -45,13 +45,23 @@ interface AgentTypeOption extends Option {
|
||||
type ModelOption = Option
|
||||
|
||||
type AgentForm = {
|
||||
type: AgentEntity['type']
|
||||
name: AgentEntity['name']
|
||||
description?: AgentEntity['description']
|
||||
instructions?: AgentEntity['instructions']
|
||||
model?: AgentEntity['model']
|
||||
type: AgentType
|
||||
name: string
|
||||
description?: string
|
||||
instructions?: string
|
||||
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 {
|
||||
agent?: AgentEntity
|
||||
}
|
||||
@ -88,16 +98,13 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
const { addAgent, updateAgent } = useAgents()
|
||||
const isEditing = (agent?: AgentEntity) => agent !== undefined
|
||||
|
||||
// default values. may change to undefined.
|
||||
const [form, setForm] = useState<AgentForm>(
|
||||
isEditing(agent)
|
||||
? agent
|
||||
: {
|
||||
type: 'claude-code',
|
||||
name: 'Claude Code',
|
||||
model: 'claude-4-sonnet'
|
||||
}
|
||||
)
|
||||
const [form, setForm] = useState<AgentForm>(() => buildAgentForm(agent))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setForm(buildAgentForm(agent))
|
||||
}
|
||||
}, [agent, isOpen])
|
||||
|
||||
const Option = useCallback(
|
||||
({ 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
|
||||
if (!isAgentType(form.type)) {
|
||||
window.toast.error(t('agent.add.error.invalid_agent'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
if (form.model === undefined) {
|
||||
if (!form.model) {
|
||||
window.toast.error(t('error.model.not_exists'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
let _agent: AgentEntity
|
||||
let resultAgent: AgentEntity
|
||||
if (isEditing(agent)) {
|
||||
_agent = {
|
||||
...agent,
|
||||
// type: form.type,
|
||||
if (!agent) {
|
||||
throw new Error('Agent is required for editing mode')
|
||||
}
|
||||
|
||||
const updatePayload: Partial<AgentEntity> & { id: string } = {
|
||||
id: agent.id,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
updated_at: new Date().toISOString(),
|
||||
model: form.model
|
||||
// avatar: getAvatar(form.type)
|
||||
} satisfies AgentEntity
|
||||
updateAgent(_agent)
|
||||
}
|
||||
|
||||
updateAgent(updatePayload)
|
||||
resultAgent = { ...agent, ...updatePayload }
|
||||
window.toast.success(t('common.update_success'))
|
||||
} else {
|
||||
_agent = {
|
||||
const now = new Date().toISOString()
|
||||
resultAgent = {
|
||||
id: uuid(),
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
model: form.model,
|
||||
avatar: getAvatar(form.type)
|
||||
} satisfies AgentEntity
|
||||
addAgent(_agent)
|
||||
accessible_paths: [...form.accessible_paths]
|
||||
}
|
||||
addAgent(resultAgent)
|
||||
window.toast.success(t('common.add_success'))
|
||||
}
|
||||
|
||||
logger.debug('Agent', _agent)
|
||||
logger.debug('Agent mutation payload', { agent: resultAgent })
|
||||
loadingRef.current = false
|
||||
|
||||
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.description,
|
||||
form.instructions,
|
||||
form.accessible_paths,
|
||||
agent,
|
||||
setTimeoutTimer,
|
||||
onClose,
|
||||
@ -339,8 +354,16 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Textarea label={t('common.description')} value={form.description} onValueChange={onDescChange} />
|
||||
<Textarea label={t('common.prompt')} value={form.instructions} onValueChange={onInstChange} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
<Textarea
|
||||
label={t('common.prompt')}
|
||||
value={form.instructions ?? ''}
|
||||
onValueChange={onInstChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
@ -356,11 +379,3 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const getAvatar = (type: AgentType) => {
|
||||
switch (type) {
|
||||
case 'claude-code':
|
||||
return ClaudeIcon
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { AgentBase } from '@renderer/types'
|
||||
|
||||
// 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.
|
||||
export const DEFAULT_CLAUDE_CODE_CONFIG: Omit<AgentBase, 'model'> = {
|
||||
|
||||
@ -6,8 +6,9 @@ export const useUpdateAgent = () => {
|
||||
|
||||
// TODO: use api
|
||||
return useMutation({
|
||||
// @ts-expect-error not-implemented
|
||||
mutationFn: async ({}: Partial<AgentEntity> & { id: string }) => {},
|
||||
mutationFn: async (agentUpdate: Partial<AgentEntity> & { id: string }) => {
|
||||
throw new Error(`useUpdateAgent mutationFn not implemented for agent ${agentUpdate.id}`)
|
||||
},
|
||||
onSuccess: (updated: AgentEntity) => {
|
||||
qc.setQueryData<AgentEntity[]>(['todos'], (old) =>
|
||||
old ? old.map((t) => (t.id === updated.id ? updated : t)) : []
|
||||
|
||||
@ -22,13 +22,14 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
|
||||
// const { agents } = useAgents()
|
||||
|
||||
const AgentLabel = useCallback(() => {
|
||||
const displayName = agent.name ?? agent.id
|
||||
return (
|
||||
<>
|
||||
{agent.avatar && <Avatar className="h-6 w-6" src={agent.avatar} />}
|
||||
<span className="text-sm">{agent.name}</span>
|
||||
<Avatar className="h-6 w-6" name={displayName} />
|
||||
<span className="text-sm">{displayName}</span>
|
||||
</>
|
||||
)
|
||||
}, [agent.avatar, agent.name])
|
||||
}, [agent.id, agent.name])
|
||||
|
||||
const handleClick = () => logger.debug('not implemented')
|
||||
|
||||
@ -37,7 +38,7 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete }) => {
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<Container onClick={handleClick} className={isActive ? 'active' : ''}>
|
||||
<AssistantNameRow className="name" title={agent.name}>
|
||||
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||
<AgentLabel />
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Database entity types for Agent, Session, and SessionMessage
|
||||
* 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 SessionMessageRole = ModelMessage['role']
|
||||
export type AgentType = 'claude-code'
|
||||
@ -75,15 +75,14 @@ export interface ListOptions {
|
||||
export interface AgentSessionEntity extends AgentBase {
|
||||
id: string
|
||||
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
|
||||
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CreateSessionRequest extends AgentBase {
|
||||
agent_id: string // Primary agent ID for the session
|
||||
}
|
||||
export type CreateSessionRequest = AgentBase
|
||||
|
||||
export interface UpdateSessionRequest extends Partial<AgentBase> {}
|
||||
|
||||
|
||||
@ -16,13 +16,14 @@ Content-Type: application/json
|
||||
"type": "claude-code",
|
||||
"model": "anthropic:claude-sonnet-4",
|
||||
"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.",
|
||||
"accessible_paths": [
|
||||
"/tmp/workspace"
|
||||
],
|
||||
"permission_mode": "acceptEdits",
|
||||
"max_steps": 10
|
||||
"configuration": {
|
||||
"permission_mode": "acceptEdits",
|
||||
"max_turns": 5
|
||||
}
|
||||
}
|
||||
|
||||
### Get Agent Details
|
||||
@ -42,14 +43,16 @@ Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Claude Code",
|
||||
"type": "claude-code",
|
||||
"model": "anthropic:claude-sonnet-4",
|
||||
"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.",
|
||||
"accessible_paths": [
|
||||
"/tmp/workspace"
|
||||
],
|
||||
"permission_mode": "acceptEdits",
|
||||
"max_steps": 10
|
||||
"configuration": {
|
||||
"permission_mode": "acceptEdits",
|
||||
"max_turns": 5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
|
||||
@host=http://localhost:23333
|
||||
@token=cs-sk-af798ed4-7cf5-4fd7-ae4b-df203b164194
|
||||
@agent_id=agent_1757947603408_t1y2mbnq4
|
||||
@session_id=session_1757947684264_z2wcwn8t7
|
||||
@agent_id=agent_1758084953648_my4oxnbm3
|
||||
@session_id=session_1758087299124_2etavjo2x
|
||||
|
||||
### List Sessions
|
||||
GET {{host}}/v1/agents/{{agent_id}}/sessions
|
||||
@ -15,29 +15,26 @@ POST {{host}}/v1/agents/{{agent_id}}/sessions
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Joke telling Session",
|
||||
"user_goal": "Tell me a funny joke"
|
||||
}
|
||||
{}
|
||||
|
||||
### 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}}
|
||||
Content-Type: application/json
|
||||
|
||||
### Delete Session
|
||||
DELETE {{host}}/v1/agents/{{agent_id}}/sessions/session_1757815245456_tfs6oogl0
|
||||
DELETE {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
|
||||
### Update Session
|
||||
PUT {{host}}/v1/agents/{{agent_id}}/sessions/session_1757815281790_q4yxgdk74
|
||||
PUT {{host}}/v1/agents/{{agent_id}}/sessions/{{session_id}}
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"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
|
||||
|
||||
{
|
||||
"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"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user