220 lines
5.6 KiB
TypeScript
220 lines
5.6 KiB
TypeScript
import type { Command } from 'commander'
|
|
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
|
|
import type {
|
|
DaemonClientV2,
|
|
AgentStreamMessage,
|
|
AgentStreamSnapshotMessage,
|
|
AgentStreamEventPayload,
|
|
AgentTimelineItem,
|
|
} from '@paseo/server'
|
|
|
|
export interface AgentAttachOptions {
|
|
host?: string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
/**
|
|
* Format and print a timeline item to the terminal
|
|
*/
|
|
function printTimelineItem(item: AgentTimelineItem): void {
|
|
switch (item.type) {
|
|
case 'assistant_message':
|
|
// Print assistant text directly
|
|
process.stdout.write(item.text)
|
|
break
|
|
|
|
case 'reasoning':
|
|
// Print reasoning in a muted color if available
|
|
console.log(`\n[Reasoning] ${item.text}`)
|
|
break
|
|
|
|
case 'tool_call': {
|
|
const toolName = item.name
|
|
const status = item.status ?? 'started'
|
|
console.log(`\n[Tool: ${toolName}] ${status}`)
|
|
break
|
|
}
|
|
|
|
case 'todo': {
|
|
const completed = item.items.filter((i) => i.completed).length
|
|
const total = item.items.length
|
|
console.log(`\n[Todo] ${completed}/${total} completed`)
|
|
break
|
|
}
|
|
|
|
case 'error':
|
|
console.error(`\n[Error] ${item.message}`)
|
|
break
|
|
|
|
case 'user_message':
|
|
console.log(`\n[User] ${item.text}`)
|
|
break
|
|
|
|
default:
|
|
// Unknown item type, skip
|
|
break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format and print a stream event to the terminal
|
|
*/
|
|
function printStreamEvent(event: AgentStreamEventPayload): void {
|
|
switch (event.type) {
|
|
case 'timeline':
|
|
// Print the timeline item
|
|
printTimelineItem(event.item)
|
|
break
|
|
|
|
case 'permission_requested':
|
|
console.log(`\n[Permission Required] ${event.request.name}`)
|
|
if (event.request.description) {
|
|
console.log(` ${event.request.description}`)
|
|
}
|
|
break
|
|
|
|
case 'permission_resolved':
|
|
console.log(`\n[Permission ${event.resolution.behavior}]`)
|
|
break
|
|
|
|
case 'turn_failed':
|
|
console.error(`\n[Turn Failed] ${event.error}`)
|
|
break
|
|
|
|
case 'attention_required':
|
|
console.log(`\n[Attention Required: ${event.reason}]`)
|
|
break
|
|
|
|
default:
|
|
// Other event types (thread_started, provider_event, etc.) are internal
|
|
break
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach to a running agent's output stream
|
|
*/
|
|
export async function runAttachCommand(
|
|
id: string,
|
|
options: AgentAttachOptions,
|
|
_command: Command
|
|
): Promise<void> {
|
|
const host = getDaemonHost({ host: options.host as string | undefined })
|
|
|
|
if (!id) {
|
|
console.error('Error: Agent ID required')
|
|
console.error('Usage: paseo attach <id>')
|
|
process.exit(1)
|
|
}
|
|
|
|
let client: DaemonClientV2
|
|
try {
|
|
client = await connectToDaemon({ host: options.host as string | undefined })
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err)
|
|
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`)
|
|
console.error('Start the daemon with: paseo daemon start')
|
|
process.exit(1)
|
|
}
|
|
|
|
try {
|
|
// Request agent list
|
|
client.requestAgentList()
|
|
|
|
// Wait for agent list to be populated
|
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
|
|
const agents = client.listAgents()
|
|
const resolvedId = resolveAgentId(id, agents)
|
|
|
|
if (!resolvedId) {
|
|
console.error(`Error: No agent found matching: ${id}`)
|
|
console.error('Use `paseo ls` to list available agents')
|
|
await client.close()
|
|
process.exit(1)
|
|
}
|
|
|
|
const agent = agents.find((a) => a.id === resolvedId)
|
|
if (!agent) {
|
|
console.error(`Error: Agent not found: ${resolvedId}`)
|
|
await client.close()
|
|
process.exit(1)
|
|
}
|
|
|
|
// Print header
|
|
console.log(`Attaching to agent ${resolvedId.substring(0, 7)}...`)
|
|
console.log(`(Press Ctrl+C to detach)\n`)
|
|
|
|
// Get existing output from snapshot
|
|
const snapshotPromise = new Promise<void>((resolve) => {
|
|
const timeout = setTimeout(() => resolve(), 5000)
|
|
|
|
const unsubscribe = client.on('agent_stream_snapshot', (msg: unknown) => {
|
|
const message = msg as AgentStreamSnapshotMessage
|
|
if (message.type !== 'agent_stream_snapshot') return
|
|
if (message.payload.agentId !== resolvedId) return
|
|
|
|
clearTimeout(timeout)
|
|
unsubscribe()
|
|
|
|
// Print recent events from snapshot
|
|
for (const e of message.payload.events) {
|
|
printStreamEvent(e.event)
|
|
}
|
|
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
// Initialize agent to trigger snapshot
|
|
try {
|
|
await client.initializeAgent(resolvedId)
|
|
} catch {
|
|
// Agent might already be initialized, continue
|
|
}
|
|
|
|
// Wait for snapshot
|
|
await snapshotPromise
|
|
|
|
// Subscribe to new events
|
|
const unsubscribe = client.on('agent_stream', (msg: unknown) => {
|
|
const message = msg as AgentStreamMessage
|
|
if (message.type !== 'agent_stream') return
|
|
if (message.payload.agentId !== resolvedId) return
|
|
|
|
printStreamEvent(message.payload.event)
|
|
})
|
|
|
|
// Handle Ctrl+C to detach gracefully
|
|
let detached = false
|
|
const detach = () => {
|
|
if (detached) return
|
|
detached = true
|
|
|
|
console.log('\n\nDetaching from agent...')
|
|
unsubscribe()
|
|
client
|
|
.close()
|
|
.then(() => {
|
|
process.exit(0)
|
|
})
|
|
.catch(() => {
|
|
process.exit(1)
|
|
})
|
|
}
|
|
|
|
process.on('SIGINT', detach)
|
|
process.on('SIGTERM', detach)
|
|
|
|
// Keep the process alive
|
|
await new Promise(() => {
|
|
// Wait indefinitely until interrupted
|
|
})
|
|
} catch (err) {
|
|
await client.close().catch(() => {})
|
|
const message = err instanceof Error ? err.message : String(err)
|
|
console.error(`Error: Failed to attach to agent: ${message}`)
|
|
process.exit(1)
|
|
}
|
|
}
|