mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
256 lines
7.7 KiB
TypeScript
256 lines
7.7 KiB
TypeScript
import type { Command } from 'commander'
|
|
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
|
|
import type { CommandOptions } from '../../output/index.js'
|
|
import type {
|
|
DaemonClient,
|
|
AgentStreamMessage,
|
|
AgentStreamSnapshotMessage,
|
|
AgentTimelineItem,
|
|
} from '@paseo/server'
|
|
import { curateAgentActivity } from '@paseo/server'
|
|
|
|
export interface AgentLogsOptions extends CommandOptions {
|
|
follow?: boolean
|
|
tail?: string
|
|
filter?: string
|
|
since?: string
|
|
}
|
|
|
|
// Logs command returns void - it outputs directly to console
|
|
export type AgentLogsResult = void
|
|
|
|
|
|
function parseTailCount(raw: string | undefined): number | undefined {
|
|
if (raw === undefined) return undefined
|
|
const parsed = Number.parseInt(raw, 10)
|
|
if (Number.isNaN(parsed) || parsed < 0) {
|
|
return undefined
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
/**
|
|
* Check if a timeline item matches the filter type
|
|
*/
|
|
function matchesFilter(item: AgentTimelineItem, filter?: string): boolean {
|
|
if (!filter) return true
|
|
|
|
const filterLower = filter.toLowerCase()
|
|
const type = item.type.toLowerCase()
|
|
|
|
switch (filterLower) {
|
|
case 'tools':
|
|
return type === 'tool_call'
|
|
case 'text':
|
|
return type === 'user_message' || type === 'assistant_message' || type === 'reasoning'
|
|
case 'errors':
|
|
return type === 'error'
|
|
case 'permissions':
|
|
// Permissions might be in tool_call status or a separate event type
|
|
return type.includes('permission')
|
|
default:
|
|
// If filter doesn't match predefined types, match against the actual type
|
|
return type.includes(filterLower)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract timeline items from an agent_stream_snapshot message
|
|
*/
|
|
function extractTimelineFromSnapshot(message: AgentStreamSnapshotMessage): AgentTimelineItem[] {
|
|
const items: AgentTimelineItem[] = []
|
|
for (const e of message.payload.events) {
|
|
if (e.event.type === 'timeline') {
|
|
items.push(e.event.item)
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
export async function runLogsCommand(
|
|
id: string,
|
|
options: AgentLogsOptions,
|
|
_command: Command
|
|
): Promise<AgentLogsResult> {
|
|
const host = getDaemonHost({ host: options.host as string | undefined })
|
|
|
|
if (!id) {
|
|
console.error('Error: Agent ID required')
|
|
console.error('Usage: paseo agent logs <id>')
|
|
process.exit(1)
|
|
}
|
|
|
|
let client: DaemonClient
|
|
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 {
|
|
const agent = await client.fetchAgent(id)
|
|
if (!agent) {
|
|
console.error(`Error: No agent found matching: ${id}`)
|
|
console.error('Use `paseo ls` to list available agents')
|
|
await client.close()
|
|
process.exit(1)
|
|
}
|
|
const resolvedId = agent.id
|
|
|
|
// For follow mode, we stream events continuously
|
|
if (options.follow) {
|
|
if (options.tail !== undefined && parseTailCount(options.tail) === undefined) {
|
|
console.error(`Error: Invalid --tail value: ${options.tail}`)
|
|
console.error('Usage: --tail <n> (where n is >= 0)')
|
|
await client.close().catch(() => {})
|
|
process.exit(1)
|
|
}
|
|
await runFollowMode(client, resolvedId, options)
|
|
return
|
|
}
|
|
|
|
// For non-follow mode, initialize the agent to get timeline snapshot
|
|
// Set up handler for timeline events before initializing
|
|
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
|
const timeout = setTimeout(() => resolve([]), 10000)
|
|
|
|
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()
|
|
resolve(extractTimelineFromSnapshot(message))
|
|
})
|
|
})
|
|
|
|
// Initialize agent to trigger timeline snapshot
|
|
try {
|
|
await client.initializeAgent(resolvedId)
|
|
} catch {
|
|
// Agent might already be initialized, continue to collect from queue
|
|
}
|
|
|
|
// Get timeline from snapshot
|
|
let timelineItems = await snapshotPromise
|
|
|
|
// Apply filter
|
|
if (options.filter) {
|
|
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter))
|
|
}
|
|
|
|
const tailCount = parseTailCount(options.tail)
|
|
if (options.tail !== undefined && tailCount === undefined) {
|
|
console.error(`Error: Invalid --tail value: ${options.tail}`)
|
|
console.error('Usage: --tail <n> (where n is >= 0)')
|
|
await client.close().catch(() => {})
|
|
process.exit(1)
|
|
}
|
|
|
|
await client.close()
|
|
|
|
// Use curateAgentActivity to format the transcript
|
|
if (tailCount === 0) {
|
|
return
|
|
}
|
|
|
|
const transcript = curateAgentActivity(timelineItems, tailCount !== undefined ? { maxItems: tailCount } : undefined)
|
|
console.log(transcript)
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err)
|
|
console.error(`Error: Failed to get logs: ${message}`)
|
|
await client.close().catch(() => {})
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Follow mode: stream logs in real-time until interrupted
|
|
*/
|
|
async function runFollowMode(
|
|
client: DaemonClient,
|
|
agentId: string,
|
|
options: AgentLogsOptions
|
|
): Promise<void> {
|
|
const DEFAULT_FOLLOW_TAIL = 10
|
|
const tailCount = parseTailCount(options.tail) ?? DEFAULT_FOLLOW_TAIL
|
|
|
|
// First, get existing timeline
|
|
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
|
const timeout = setTimeout(() => resolve([]), 10000)
|
|
|
|
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 !== agentId) return
|
|
|
|
clearTimeout(timeout)
|
|
unsubscribe()
|
|
resolve(extractTimelineFromSnapshot(message))
|
|
})
|
|
})
|
|
|
|
// Initialize agent to trigger timeline snapshot
|
|
try {
|
|
await client.initializeAgent(agentId)
|
|
} catch {
|
|
// Agent might already be initialized
|
|
}
|
|
|
|
// Get existing timeline
|
|
let existingItems = await snapshotPromise
|
|
|
|
// Apply filter to existing items
|
|
if (options.filter) {
|
|
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter))
|
|
}
|
|
|
|
// Print existing transcript (tail-like behavior)
|
|
if (tailCount > 0) {
|
|
const existingTranscript = curateAgentActivity(existingItems, { maxItems: tailCount })
|
|
if (existingTranscript !== 'No activity to display.') {
|
|
console.log(existingTranscript)
|
|
}
|
|
}
|
|
|
|
// Subscribe to new events
|
|
const tailLabel = tailCount === 0 ? 'no history' : `last ${tailCount} entr${tailCount === 1 ? 'y' : 'ies'}`
|
|
console.log(`\n--- Following logs (${tailLabel}; Ctrl+C to stop) ---\n`)
|
|
|
|
const unsubscribe = client.on('agent_stream', (msg: unknown) => {
|
|
const message = msg as AgentStreamMessage
|
|
if (message.type !== 'agent_stream') return
|
|
if (message.payload.agentId !== agentId) return
|
|
|
|
if (message.payload.event.type === 'timeline') {
|
|
const item = message.payload.event.item
|
|
// Apply filter
|
|
if (options.filter && !matchesFilter(item, options.filter)) {
|
|
return
|
|
}
|
|
// Print each timeline item as it arrives using the curator format
|
|
const transcript = curateAgentActivity([item])
|
|
if (transcript !== 'No activity to display.') {
|
|
console.log(transcript)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Wait for interrupt
|
|
await new Promise<void>((resolve) => {
|
|
const cleanup = () => {
|
|
unsubscribe()
|
|
resolve()
|
|
}
|
|
|
|
process.on('SIGINT', cleanup)
|
|
process.on('SIGTERM', cleanup)
|
|
})
|
|
|
|
await client.close()
|
|
}
|