feat: wait returns on permission request and improve CLI logging
This commit is contained in:
@@ -84,14 +84,8 @@ export function createCli(): Command {
|
|||||||
.option('--tail <n>', 'Show last n entries')
|
.option('--tail <n>', 'Show last n entries')
|
||||||
.option('--filter <type>', 'Filter by event type (tools, text, errors, permissions)')
|
.option('--filter <type>', 'Filter by event type (tools, text, errors, permissions)')
|
||||||
.option('--since <time>', 'Show logs since timestamp')
|
.option('--since <time>', 'Show logs since timestamp')
|
||||||
.option('--json', 'Output in JSON format (only when not following)')
|
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((id, options, command) => {
|
.action(runLogsCommand)
|
||||||
if (options.json && !options.follow) {
|
|
||||||
command.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runLogsCommand)(id, options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('stop')
|
.command('stop')
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ export function createAgentCommand(): Command {
|
|||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.option('-f, --follow', 'Follow log output (streaming)')
|
.option('-f, --follow', 'Follow log output (streaming)')
|
||||||
.option('--tail <n>', 'Show last n entries')
|
.option('--tail <n>', 'Show last n entries')
|
||||||
|
.option('--filter <type>', 'Filter by event type (tools, text, errors, permissions)')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runLogsCommand))
|
.action(runLogsCommand)
|
||||||
|
|
||||||
agent
|
agent
|
||||||
.command('stop')
|
.command('stop')
|
||||||
|
|||||||
@@ -3,37 +3,39 @@ import type { AgentSnapshotPayload } from '@paseo/server'
|
|||||||
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
|
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
|
||||||
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
|
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
|
||||||
|
|
||||||
/** Agent inspect data for display */
|
/** Agent inspect data for display (matches CLI spec format) */
|
||||||
interface AgentInspect {
|
interface AgentInspect {
|
||||||
id: string
|
Id: string
|
||||||
name: string
|
Name: string
|
||||||
provider: string
|
Provider: string
|
||||||
model: string
|
Model: string
|
||||||
status: string
|
Status: string
|
||||||
mode: string
|
Mode: string
|
||||||
cwd: string
|
Cwd: string
|
||||||
createdAt: string
|
CreatedAt: string
|
||||||
archivedAt: string | null
|
UpdatedAt: string
|
||||||
lastUsage: {
|
LastUsage: {
|
||||||
inputTokens: number
|
InputTokens: number
|
||||||
outputTokens: number
|
OutputTokens: number
|
||||||
cachedInputTokens: number
|
CachedTokens: number
|
||||||
totalCostUsd: number
|
CostUsd: number
|
||||||
} | null
|
} | null
|
||||||
capabilities: {
|
Capabilities: {
|
||||||
streaming: boolean
|
Streaming: boolean
|
||||||
persistence: boolean
|
Persistence: boolean
|
||||||
dynamicModes: boolean
|
DynamicModes: boolean
|
||||||
mcpServers: boolean
|
McpServers: boolean
|
||||||
} | null
|
} | null
|
||||||
availableModes: Array<{
|
AvailableModes: Array<{
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
}> | null
|
}> | null
|
||||||
pendingPermissions: Array<{
|
PendingPermissions: Array<{
|
||||||
id: string
|
id: string
|
||||||
tool: string
|
tool: string
|
||||||
}>
|
}>
|
||||||
|
Worktree: string | null
|
||||||
|
ParentAgentId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Key-value row for table display */
|
/** Key-value row for table display */
|
||||||
@@ -86,94 +88,94 @@ function formatCost(costUsd: number): string {
|
|||||||
function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
|
function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
|
||||||
const lastUsage = snapshot.lastUsage
|
const lastUsage = snapshot.lastUsage
|
||||||
? {
|
? {
|
||||||
inputTokens: snapshot.lastUsage.inputTokens ?? 0,
|
InputTokens: snapshot.lastUsage.inputTokens ?? 0,
|
||||||
outputTokens: snapshot.lastUsage.outputTokens ?? 0,
|
OutputTokens: snapshot.lastUsage.outputTokens ?? 0,
|
||||||
cachedInputTokens: snapshot.lastUsage.cachedInputTokens ?? 0,
|
CachedTokens: snapshot.lastUsage.cachedInputTokens ?? 0,
|
||||||
totalCostUsd: snapshot.lastUsage.totalCostUsd ?? 0,
|
CostUsd: snapshot.lastUsage.totalCostUsd ?? 0,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const capabilities = snapshot.capabilities
|
const capabilities = snapshot.capabilities
|
||||||
? {
|
? {
|
||||||
streaming: snapshot.capabilities.supportsStreaming ?? false,
|
Streaming: snapshot.capabilities.supportsStreaming ?? false,
|
||||||
persistence: snapshot.capabilities.supportsSessionPersistence ?? false,
|
Persistence: snapshot.capabilities.supportsSessionPersistence ?? false,
|
||||||
dynamicModes: snapshot.capabilities.supportsDynamicModes ?? false,
|
DynamicModes: snapshot.capabilities.supportsDynamicModes ?? false,
|
||||||
mcpServers: snapshot.capabilities.supportsMcpServers ?? false,
|
McpServers: snapshot.capabilities.supportsMcpServers ?? false,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
// Extract worktree and parentAgentId from labels if they exist
|
||||||
|
const worktree = snapshot.labels?.['paseo.worktree'] ?? null
|
||||||
|
const parentAgentId = snapshot.labels?.['paseo.parent-agent-id'] ?? null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: snapshot.id,
|
Id: snapshot.id,
|
||||||
name: snapshot.title ?? '-',
|
Name: snapshot.title ?? '-',
|
||||||
provider: snapshot.provider,
|
Provider: snapshot.provider,
|
||||||
model: snapshot.model ?? '-',
|
Model: snapshot.model ?? '-',
|
||||||
status: snapshot.status,
|
Status: snapshot.status,
|
||||||
mode: snapshot.currentModeId ?? 'default',
|
Mode: snapshot.currentModeId ?? 'default',
|
||||||
cwd: snapshot.cwd,
|
Cwd: snapshot.cwd,
|
||||||
createdAt: snapshot.createdAt,
|
CreatedAt: snapshot.createdAt,
|
||||||
archivedAt: snapshot.archivedAt ?? null,
|
UpdatedAt: snapshot.updatedAt,
|
||||||
lastUsage,
|
LastUsage: lastUsage,
|
||||||
capabilities,
|
Capabilities: capabilities,
|
||||||
availableModes: snapshot.availableModes
|
AvailableModes: snapshot.availableModes
|
||||||
? snapshot.availableModes.map((m) => ({ id: m.id, label: m.label }))
|
? snapshot.availableModes.map((m) => ({ id: m.id, label: m.label }))
|
||||||
: null,
|
: null,
|
||||||
pendingPermissions: (snapshot.pendingPermissions ?? []).map((p) => ({
|
PendingPermissions: (snapshot.pendingPermissions ?? []).map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
tool: p.name ?? 'unknown',
|
tool: p.name ?? 'unknown',
|
||||||
})),
|
})),
|
||||||
|
Worktree: worktree,
|
||||||
|
ParentAgentId: parentAgentId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert agent to key-value rows for table display */
|
/** Convert agent to key-value rows for table display */
|
||||||
function toInspectRows(agent: AgentInspect): InspectRow[] {
|
function toInspectRows(agent: AgentInspect): InspectRow[] {
|
||||||
const rows: InspectRow[] = [
|
const rows: InspectRow[] = [
|
||||||
{ key: 'Id', value: agent.id },
|
{ key: 'Id', value: agent.Id },
|
||||||
{ key: 'Name', value: agent.name },
|
{ key: 'Name', value: agent.Name },
|
||||||
{ key: 'Provider', value: agent.provider },
|
{ key: 'Provider', value: agent.Provider },
|
||||||
{ key: 'Model', value: agent.model },
|
{ key: 'Model', value: agent.Model },
|
||||||
{ key: 'Status', value: agent.status },
|
{ key: 'Status', value: agent.Status },
|
||||||
{ key: 'Mode', value: agent.mode },
|
{ key: 'Mode', value: agent.Mode },
|
||||||
{ key: 'Cwd', value: shortenPath(agent.cwd) },
|
{ key: 'Cwd', value: shortenPath(agent.Cwd) },
|
||||||
{ key: 'CreatedAt', value: agent.createdAt },
|
{ key: 'CreatedAt', value: agent.CreatedAt },
|
||||||
|
{ key: 'UpdatedAt', value: agent.UpdatedAt },
|
||||||
]
|
]
|
||||||
|
|
||||||
if (agent.archivedAt) {
|
if (agent.LastUsage) {
|
||||||
rows.push({ key: 'ArchivedAt', value: agent.archivedAt })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agent.lastUsage) {
|
|
||||||
rows.push({
|
rows.push({
|
||||||
key: 'LastUsage',
|
key: 'LastUsage',
|
||||||
value: `${agent.lastUsage.inputTokens} in, ${agent.lastUsage.outputTokens} out, ${formatCost(agent.lastUsage.totalCostUsd)}`,
|
value: `InputTokens: ${agent.LastUsage.InputTokens}, OutputTokens: ${agent.LastUsage.OutputTokens}, CachedTokens: ${agent.LastUsage.CachedTokens}, CostUsd: ${formatCost(agent.LastUsage.CostUsd)}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.capabilities) {
|
if (agent.Capabilities) {
|
||||||
const caps = agent.capabilities
|
rows.push({
|
||||||
const capsList = [
|
key: 'Capabilities',
|
||||||
caps.streaming ? 'Streaming' : null,
|
value: `Streaming: ${agent.Capabilities.Streaming}, Persistence: ${agent.Capabilities.Persistence}, DynamicModes: ${agent.Capabilities.DynamicModes}, McpServers: ${agent.Capabilities.McpServers}`,
|
||||||
caps.persistence ? 'Persistence' : null,
|
})
|
||||||
caps.dynamicModes ? 'DynamicModes' : null,
|
|
||||||
caps.mcpServers ? 'McpServers' : null,
|
|
||||||
].filter(Boolean)
|
|
||||||
rows.push({ key: 'Capabilities', value: capsList.join(', ') || 'none' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.availableModes && agent.availableModes.length > 0) {
|
if (agent.AvailableModes && agent.AvailableModes.length > 0) {
|
||||||
rows.push({
|
rows.push({
|
||||||
key: 'AvailableModes',
|
key: 'AvailableModes',
|
||||||
value: agent.availableModes.map((m) => m.id).join(', '),
|
value: agent.AvailableModes.map((m) => `${m.id} (${m.label})`).join(', '),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agent.pendingPermissions.length > 0) {
|
rows.push({
|
||||||
rows.push({
|
key: 'PendingPermissions',
|
||||||
key: 'PendingPermissions',
|
value: agent.PendingPermissions.length > 0
|
||||||
value: agent.pendingPermissions.length.toString(),
|
? agent.PendingPermissions.map(p => `${p.id} (${p.tool})`).join(', ')
|
||||||
})
|
: '[]'
|
||||||
} else {
|
})
|
||||||
rows.push({ key: 'PendingPermissions', value: '[]' })
|
|
||||||
}
|
rows.push({ key: 'Worktree', value: agent.Worktree ?? 'null' })
|
||||||
|
rows.push({ key: 'ParentAgentId', value: agent.ParentAgentId ?? 'null' })
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
import type { Command } from 'commander'
|
import type { Command } from 'commander'
|
||||||
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
|
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
|
||||||
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
|
import type { CommandOptions } from '../../output/index.js'
|
||||||
import type {
|
import type {
|
||||||
DaemonClientV2,
|
DaemonClientV2,
|
||||||
AgentStreamMessage,
|
AgentStreamMessage,
|
||||||
AgentStreamSnapshotMessage,
|
AgentStreamSnapshotMessage,
|
||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
} from '@paseo/server'
|
} from '@paseo/server'
|
||||||
|
import { curateAgentActivity } from '@paseo/server'
|
||||||
/** Timeline item for display */
|
|
||||||
export interface LogEntry {
|
|
||||||
timestamp: string
|
|
||||||
type: string
|
|
||||||
summary: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Schema for logs output */
|
|
||||||
export const logsSchema: OutputSchema<LogEntry> = {
|
|
||||||
idField: 'timestamp',
|
|
||||||
columns: [
|
|
||||||
{ header: 'TIME', field: 'timestamp', width: 12 },
|
|
||||||
{ header: 'TYPE', field: 'type', width: 15 },
|
|
||||||
{ header: 'SUMMARY', field: 'summary', width: 60 },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AgentLogsOptions extends CommandOptions {
|
export interface AgentLogsOptions extends CommandOptions {
|
||||||
follow?: boolean
|
follow?: boolean
|
||||||
@@ -32,74 +16,9 @@ export interface AgentLogsOptions extends CommandOptions {
|
|||||||
since?: string
|
since?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgentLogsResult = ListResult<LogEntry>
|
// Logs command returns void - it outputs directly to console
|
||||||
|
export type AgentLogsResult = void
|
||||||
|
|
||||||
/** Format a timeline item into a log entry */
|
|
||||||
function formatTimelineItem(item: AgentTimelineItem): LogEntry {
|
|
||||||
const now = new Date().toISOString().slice(11, 19) // HH:MM:SS
|
|
||||||
|
|
||||||
switch (item.type) {
|
|
||||||
case 'user_message':
|
|
||||||
return {
|
|
||||||
timestamp: now,
|
|
||||||
type: 'user',
|
|
||||||
summary: truncate(item.text, 60),
|
|
||||||
}
|
|
||||||
case 'assistant_message':
|
|
||||||
return {
|
|
||||||
timestamp: now,
|
|
||||||
type: 'assistant',
|
|
||||||
summary: truncate(item.text, 60),
|
|
||||||
}
|
|
||||||
case 'reasoning':
|
|
||||||
return {
|
|
||||||
timestamp: now,
|
|
||||||
type: 'reasoning',
|
|
||||||
summary: truncate(item.text, 60),
|
|
||||||
}
|
|
||||||
case 'tool_call': {
|
|
||||||
const toolName = item.name
|
|
||||||
const status = item.status ?? ''
|
|
||||||
let inputSummary = ''
|
|
||||||
if (item.input && typeof item.input === 'object') {
|
|
||||||
const inp = item.input as Record<string, unknown>
|
|
||||||
// Common input fields for summarization
|
|
||||||
if (inp.command) {
|
|
||||||
inputSummary = truncate(String(inp.command), 40)
|
|
||||||
} else if (inp.file_path) {
|
|
||||||
inputSummary = truncate(String(inp.file_path), 40)
|
|
||||||
} else if (inp.pattern) {
|
|
||||||
inputSummary = truncate(String(inp.pattern), 40)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
timestamp: now,
|
|
||||||
type: `tool:${toolName}`,
|
|
||||||
summary: inputSummary ? `${status} ${inputSummary}`.trim() : status,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'todo': {
|
|
||||||
const completed = item.items.filter((i) => i.completed).length
|
|
||||||
return {
|
|
||||||
timestamp: now,
|
|
||||||
type: 'todo',
|
|
||||||
summary: `${completed}/${item.items.length} completed`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'error':
|
|
||||||
return {
|
|
||||||
timestamp: now,
|
|
||||||
type: 'error',
|
|
||||||
summary: truncate(item.message, 60),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncate(str: string, maxLen: number): string {
|
|
||||||
const cleaned = str.replace(/\n/g, ' ').trim()
|
|
||||||
if (cleaned.length <= maxLen) return cleaned
|
|
||||||
return cleaned.slice(0, maxLen - 3) + '...'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a timeline item matches the filter type
|
* Check if a timeline item matches the filter type
|
||||||
@@ -126,43 +45,6 @@ function matchesFilter(item: AgentTimelineItem, filter?: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a timestamp string and return a Date object
|
|
||||||
* Supports ISO format and relative times like "5m", "1h", "2d"
|
|
||||||
*/
|
|
||||||
function parseTimestamp(timeStr: string): Date | null {
|
|
||||||
// Try ISO format first
|
|
||||||
const isoDate = new Date(timeStr)
|
|
||||||
if (!isNaN(isoDate.getTime())) {
|
|
||||||
return isoDate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try relative time format (e.g., "5m", "1h", "2d")
|
|
||||||
const match = timeStr.match(/^(\d+)([smhd])$/)
|
|
||||||
if (match) {
|
|
||||||
const value = parseInt(match[1], 10)
|
|
||||||
const unit = match[2]
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case 's':
|
|
||||||
now.setSeconds(now.getSeconds() - value)
|
|
||||||
return now
|
|
||||||
case 'm':
|
|
||||||
now.setMinutes(now.getMinutes() - value)
|
|
||||||
return now
|
|
||||||
case 'h':
|
|
||||||
now.setHours(now.getHours() - value)
|
|
||||||
return now
|
|
||||||
case 'd':
|
|
||||||
now.setDate(now.getDate() - value)
|
|
||||||
return now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract timeline items from an agent_stream_snapshot message
|
* Extract timeline items from an agent_stream_snapshot message
|
||||||
*/
|
*/
|
||||||
@@ -194,12 +76,9 @@ export async function runLogsCommand(
|
|||||||
const host = getDaemonHost({ host: options.host as string | undefined })
|
const host = getDaemonHost({ host: options.host as string | undefined })
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
const error: CommandError = {
|
console.error('Error: Agent ID required')
|
||||||
code: 'MISSING_ARGUMENT',
|
console.error('Usage: paseo agent logs <id>')
|
||||||
message: 'Agent ID required',
|
process.exit(1)
|
||||||
details: 'Usage: paseo agent logs <id>',
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let client: DaemonClientV2
|
let client: DaemonClientV2
|
||||||
@@ -207,12 +86,9 @@ export async function runLogsCommand(
|
|||||||
client = await connectToDaemon({ host: options.host as string | undefined })
|
client = await connectToDaemon({ host: options.host as string | undefined })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
const error: CommandError = {
|
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`)
|
||||||
code: 'DAEMON_NOT_RUNNING',
|
console.error('Start the daemon with: paseo daemon start')
|
||||||
message: `Cannot connect to daemon at ${host}: ${message}`,
|
process.exit(1)
|
||||||
details: 'Start the daemon with: paseo daemon start',
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -226,22 +102,19 @@ export async function runLogsCommand(
|
|||||||
const resolvedId = resolveAgentId(id, agents)
|
const resolvedId = resolveAgentId(id, agents)
|
||||||
|
|
||||||
if (!resolvedId) {
|
if (!resolvedId) {
|
||||||
const error: CommandError = {
|
console.error(`Error: No agent found matching: ${id}`)
|
||||||
code: 'AGENT_NOT_FOUND',
|
console.error('Use `paseo ls` to list available agents')
|
||||||
message: `No agent found matching: ${id}`,
|
await client.close()
|
||||||
details: 'Use `paseo ls` to list available agents',
|
process.exit(1)
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For follow mode, we stream events continuously
|
// For follow mode, we stream events continuously
|
||||||
if (options.follow) {
|
if (options.follow) {
|
||||||
return await runFollowMode(client, resolvedId, options)
|
await runFollowMode(client, resolvedId, options)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-follow mode, initialize the agent to get timeline snapshot
|
// For non-follow mode, initialize the agent to get timeline snapshot
|
||||||
const logEntries: LogEntry[] = []
|
|
||||||
|
|
||||||
// Set up handler for timeline events before initializing
|
// Set up handler for timeline events before initializing
|
||||||
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
||||||
const timeout = setTimeout(() => resolve([]), 10000)
|
const timeout = setTimeout(() => resolve([]), 10000)
|
||||||
@@ -265,7 +138,7 @@ export async function runLogsCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get timeline from snapshot
|
// Get timeline from snapshot
|
||||||
const timelineItems = await snapshotPromise
|
let timelineItems = await snapshotPromise
|
||||||
|
|
||||||
// Also check message queue for any stream events
|
// Also check message queue for any stream events
|
||||||
const queue = client.getMessageQueue()
|
const queue = client.getMessageQueue()
|
||||||
@@ -281,64 +154,29 @@ export async function runLogsCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse since timestamp if provided
|
// Apply filter
|
||||||
const sinceDate = options.since ? parseTimestamp(options.since) : null
|
if (options.filter) {
|
||||||
if (options.since && !sinceDate) {
|
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter))
|
||||||
const error: CommandError = {
|
|
||||||
code: 'INVALID_TIMESTAMP',
|
|
||||||
message: `Invalid timestamp format: ${options.since}`,
|
|
||||||
details: 'Use ISO format (e.g., 2024-01-15T10:30:00) or relative time (e.g., 5m, 1h, 2d)',
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to log entries with filtering
|
// Apply tail limit
|
||||||
for (const item of timelineItems) {
|
if (options.tail) {
|
||||||
// Apply filter
|
const tailCount = parseInt(options.tail, 10)
|
||||||
if (!matchesFilter(item, options.filter)) {
|
if (!isNaN(tailCount) && tailCount > 0) {
|
||||||
continue
|
timelineItems = timelineItems.slice(-tailCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = formatTimelineItem(item)
|
|
||||||
|
|
||||||
// Apply since filter (note: we're using current time for all entries, this is a limitation)
|
|
||||||
// In a real implementation, timeline items should have their own timestamps
|
|
||||||
if (sinceDate) {
|
|
||||||
// Since we don't have actual timestamps on timeline items, we can't filter by time
|
|
||||||
// This would need to be implemented with proper timestamp support in the timeline items
|
|
||||||
}
|
|
||||||
|
|
||||||
logEntries.push(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
// Apply tail limit
|
// Use curateAgentActivity to format the transcript
|
||||||
let entries = logEntries
|
const transcript = curateAgentActivity(timelineItems)
|
||||||
if (options.tail) {
|
console.log(transcript)
|
||||||
const tailCount = parseInt(options.tail, 10)
|
|
||||||
if (!isNaN(tailCount) && tailCount > 0) {
|
|
||||||
entries = entries.slice(-tailCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'list',
|
|
||||||
data: entries,
|
|
||||||
schema: logsSchema,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await client.close().catch(() => {})
|
|
||||||
// Re-throw if already a CommandError
|
|
||||||
if (err && typeof err === 'object' && 'code' in err) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
const error: CommandError = {
|
console.error(`Error: Failed to get logs: ${message}`)
|
||||||
code: 'LOGS_FAILED',
|
await client.close().catch(() => {})
|
||||||
message: `Failed to get logs: ${message}`,
|
process.exit(1)
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,9 +187,7 @@ async function runFollowMode(
|
|||||||
client: DaemonClientV2,
|
client: DaemonClientV2,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
options: AgentLogsOptions
|
options: AgentLogsOptions
|
||||||
): Promise<AgentLogsResult> {
|
): Promise<void> {
|
||||||
const logEntries: LogEntry[] = []
|
|
||||||
|
|
||||||
// First, get existing timeline
|
// First, get existing timeline
|
||||||
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
||||||
const timeout = setTimeout(() => resolve([]), 10000)
|
const timeout = setTimeout(() => resolve([]), 10000)
|
||||||
@@ -375,24 +211,25 @@ async function runFollowMode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get existing timeline
|
// Get existing timeline
|
||||||
const existingItems = await snapshotPromise
|
let existingItems = await snapshotPromise
|
||||||
|
|
||||||
// Apply filter to existing items
|
// Apply filter to existing items
|
||||||
let itemsToShow = existingItems.filter((item) => matchesFilter(item, options.filter))
|
if (options.filter) {
|
||||||
|
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter))
|
||||||
|
}
|
||||||
|
|
||||||
// Apply tail to existing items
|
// Apply tail to existing items
|
||||||
if (options.tail) {
|
if (options.tail) {
|
||||||
const tailCount = parseInt(options.tail, 10)
|
const tailCount = parseInt(options.tail, 10)
|
||||||
if (!isNaN(tailCount) && tailCount > 0) {
|
if (!isNaN(tailCount) && tailCount > 0) {
|
||||||
itemsToShow = itemsToShow.slice(-tailCount)
|
existingItems = existingItems.slice(-tailCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print existing entries
|
// Print existing transcript
|
||||||
for (const item of itemsToShow) {
|
const existingTranscript = curateAgentActivity(existingItems)
|
||||||
const entry = formatTimelineItem(item)
|
if (existingTranscript !== 'No activity to display.') {
|
||||||
logEntries.push(entry)
|
console.log(existingTranscript)
|
||||||
printLogEntry(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to new events
|
// Subscribe to new events
|
||||||
@@ -406,12 +243,14 @@ async function runFollowMode(
|
|||||||
if (message.payload.event.type === 'timeline') {
|
if (message.payload.event.type === 'timeline') {
|
||||||
const item = message.payload.event.item
|
const item = message.payload.event.item
|
||||||
// Apply filter
|
// Apply filter
|
||||||
if (!matchesFilter(item, options.filter)) {
|
if (options.filter && !matchesFilter(item, options.filter)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const entry = formatTimelineItem(item)
|
// Print each timeline item as it arrives using the curator format
|
||||||
logEntries.push(entry)
|
const transcript = curateAgentActivity([item])
|
||||||
printLogEntry(entry)
|
if (transcript !== 'No activity to display.') {
|
||||||
|
console.log(transcript)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -427,17 +266,4 @@ async function runFollowMode(
|
|||||||
})
|
})
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'list',
|
|
||||||
data: logEntries,
|
|
||||||
schema: logsSchema,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printLogEntry(entry: LogEntry): void {
|
|
||||||
// Simple format for streaming output
|
|
||||||
const typeWidth = 15
|
|
||||||
const paddedType = entry.type.padEnd(typeWidth)
|
|
||||||
console.log(`${entry.timestamp} ${paddedType} ${entry.summary}`)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '.
|
|||||||
/** Result type for agent wait command */
|
/** Result type for agent wait command */
|
||||||
export interface AgentWaitResult {
|
export interface AgentWaitResult {
|
||||||
agentId: string
|
agentId: string
|
||||||
status: 'idle' | 'timeout'
|
status: 'idle' | 'timeout' | 'permission'
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +140,29 @@ export async function runWaitCommand(
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for agent to become idle
|
// Wait for agent to become idle OR request permission (whichever comes first)
|
||||||
try {
|
try {
|
||||||
await client.waitForAgentIdle(agentId, timeoutMs)
|
const idlePromise = client.waitForAgentIdle(agentId, timeoutMs).then(() => ({ type: 'idle' as const }))
|
||||||
|
const permissionPromise = client
|
||||||
|
.waitForPermission(agentId, timeoutMs)
|
||||||
|
.then((request) => ({ type: 'permission' as const, request }))
|
||||||
|
|
||||||
|
const result = await Promise.race([idlePromise, permissionPromise])
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
if (result.type === 'permission') {
|
||||||
|
return {
|
||||||
|
type: 'single',
|
||||||
|
data: {
|
||||||
|
agentId,
|
||||||
|
status: 'permission',
|
||||||
|
message: `Agent is waiting for permission: ${result.request.kind}`,
|
||||||
|
},
|
||||||
|
schema: agentWaitSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'single',
|
type: 'single',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -48,15 +48,7 @@ export async function runAllowCommand(
|
|||||||
): Promise<PermitAllowResult> {
|
): Promise<PermitAllowResult> {
|
||||||
const host = getDaemonHost({ host: options.host })
|
const host = getDaemonHost({ host: options.host })
|
||||||
|
|
||||||
// Validate arguments
|
// No validation needed - if no reqId provided, allow all by default
|
||||||
if (!options.all && !reqId) {
|
|
||||||
const error: CommandError = {
|
|
||||||
code: 'MISSING_ARGUMENT',
|
|
||||||
message: 'Request ID is required unless --all is specified',
|
|
||||||
details: 'Usage: paseo permit allow <agent> <req_id> or paseo permit allow <agent> --all',
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse input JSON if provided
|
// Parse input JSON if provided
|
||||||
let updatedInput: Record<string, unknown> | undefined
|
let updatedInput: Record<string, unknown> | undefined
|
||||||
@@ -129,7 +121,9 @@ export async function runAllowCommand(
|
|||||||
|
|
||||||
// Determine which permissions to allow
|
// Determine which permissions to allow
|
||||||
let permissionsToAllow: AgentPermissionRequest[]
|
let permissionsToAllow: AgentPermissionRequest[]
|
||||||
if (options.all) {
|
if (!reqId || options.all) {
|
||||||
|
// Default: allow all pending permissions if no req_id specified
|
||||||
|
// --all flag is kept as an explicit alias for clarity
|
||||||
permissionsToAllow = pendingPermissions
|
permissionsToAllow = pendingPermissions
|
||||||
} else {
|
} else {
|
||||||
// Find permission by ID prefix
|
// Find permission by ID prefix
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ export interface PermissionListItem {
|
|||||||
id: string
|
id: string
|
||||||
agentId: string
|
agentId: string
|
||||||
agentShortId: string
|
agentShortId: string
|
||||||
agentName: string
|
|
||||||
name: string
|
name: string
|
||||||
kind: string
|
|
||||||
title: string
|
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,21 +16,10 @@ export interface PermissionListItem {
|
|||||||
export const permitLsSchema: OutputSchema<PermissionListItem> = {
|
export const permitLsSchema: OutputSchema<PermissionListItem> = {
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
columns: [
|
columns: [
|
||||||
{ header: 'REQUEST ID', field: 'id', width: 12 },
|
{ header: 'AGENT', field: 'agentShortId', width: 12 },
|
||||||
{ header: 'AGENT', field: 'agentShortId', width: 10 },
|
{ header: 'REQ_ID', field: 'id', width: 12 },
|
||||||
{ header: 'NAME', field: 'agentName', width: 20 },
|
|
||||||
{ header: 'TOOL', field: 'name', width: 20 },
|
{ header: 'TOOL', field: 'name', width: 20 },
|
||||||
{
|
{ header: 'DESCRIPTION', field: 'description', width: 50 },
|
||||||
header: 'KIND',
|
|
||||||
field: 'kind',
|
|
||||||
width: 8,
|
|
||||||
color: (value) => {
|
|
||||||
if (value === 'tool') return 'blue'
|
|
||||||
if (value === 'plan') return 'magenta'
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ header: 'TITLE', field: 'title', width: 30 },
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +29,7 @@ function toListItem(agent: AgentSnapshotPayload, permission: AgentPermissionRequ
|
|||||||
id: permission.id.slice(0, 8),
|
id: permission.id.slice(0, 8),
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
agentShortId: agent.id.slice(0, 7),
|
agentShortId: agent.id.slice(0, 7),
|
||||||
agentName: agent.title ?? '-',
|
|
||||||
name: permission.name,
|
name: permission.name,
|
||||||
kind: permission.kind,
|
|
||||||
title: permission.title ?? '-',
|
|
||||||
description: permission.description ?? '-',
|
description: permission.description ?? '-',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1902,6 +1902,10 @@ export class DaemonClientV2 {
|
|||||||
sawRunningInQueue = true;
|
sawRunningInQueue = true;
|
||||||
queuedIdle = null; // Reset: any previous idle was before this run
|
queuedIdle = null; // Reset: any previous idle was before this run
|
||||||
}
|
}
|
||||||
|
// Return immediately if we have pending permissions (even if still running)
|
||||||
|
if (sawRunningInQueue && hasPendingPermissions) {
|
||||||
|
return msg.payload;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
sawRunningInQueue &&
|
sawRunningInQueue &&
|
||||||
(status === "idle" || status === "error") &&
|
(status === "idle" || status === "error") &&
|
||||||
@@ -1934,6 +1938,11 @@ export class DaemonClientV2 {
|
|||||||
if (status === "running" || hasPendingPermissions) {
|
if (status === "running" || hasPendingPermissions) {
|
||||||
sawRunning = true;
|
sawRunning = true;
|
||||||
}
|
}
|
||||||
|
// Return if we have pending permissions (even if still running)
|
||||||
|
// OR if agent is idle/error with no pending permissions (after having run)
|
||||||
|
if (sawRunning && hasPendingPermissions) {
|
||||||
|
return msg.payload;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
sawRunning &&
|
sawRunning &&
|
||||||
(status === "idle" || status === "error") &&
|
(status === "idle" || status === "error") &&
|
||||||
|
|||||||
@@ -384,7 +384,8 @@ export class AgentManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.registerSession(session, overrides, agentId);
|
// Preserve existing labels during refresh
|
||||||
|
return this.registerSession(session, overrides, agentId, { labels: existing.labels });
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeAgent(agentId: string): Promise<void> {
|
async closeAgent(agentId: string): Promise<void> {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Core rules:
|
|||||||
- In orchestrator mode, you accomplish tasks only by managing agents; do not perform the work yourself.
|
- In orchestrator mode, you accomplish tasks only by managing agents; do not perform the work yourself.
|
||||||
- Always prefix agent titles (e.g., "🎭 Feature Implementation", "🎭 Design Discussion").
|
- Always prefix agent titles (e.g., "🎭 Feature Implementation", "🎭 Design Discussion").
|
||||||
- Set cwd to the repository root and choose the most permissive mode available.
|
- Set cwd to the repository root and choose the most permissive mode available.
|
||||||
- If an agent control call fails, list agents before launching another; it may just be a wait timeout.
|
- If an agent control tool call fails, list agents before launching another; it may just be a wait timeout.
|
||||||
|
|
||||||
Context management:
|
Context management:
|
||||||
- Reuse an existing agent when the next step needs the same context (same files/module/folder or immediate follow-up like investigate → fix in the same area).
|
- Reuse an existing agent when the next step needs the same context (same files/module/folder or immediate follow-up like investigate → fix in the same area).
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { mkdtempSync, rmSync } from "fs";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import path from "path";
|
||||||
|
import {
|
||||||
|
createDaemonTestContext,
|
||||||
|
type DaemonTestContext,
|
||||||
|
} from "../test-utils/index.js";
|
||||||
|
|
||||||
|
function tmpCwd(): string {
|
||||||
|
return mkdtempSync(path.join(tmpdir(), "wait-perm-e2e-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for wait returning on permission request.
|
||||||
|
*
|
||||||
|
* The `paseo wait` command should return when:
|
||||||
|
* 1. Agent completes (goes idle)
|
||||||
|
* 2. Agent requests permission
|
||||||
|
*
|
||||||
|
* This test verifies that waitForAgentIdle correctly returns when
|
||||||
|
* an agent requests permission, not just when it goes idle.
|
||||||
|
*/
|
||||||
|
describe("wait returns on permission request", () => {
|
||||||
|
let ctx: DaemonTestContext;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ctx = await createDaemonTestContext();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await ctx.cleanup();
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
describe("Claude provider", () => {
|
||||||
|
test(
|
||||||
|
"waitForAgentIdle returns when permission is requested (not just when idle)",
|
||||||
|
async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const testFilePath = path.join(cwd, "test-wait-perm.txt");
|
||||||
|
|
||||||
|
// Create Claude agent with default mode (always ask for permissions)
|
||||||
|
const agent = await ctx.client.createAgent({
|
||||||
|
provider: "claude",
|
||||||
|
model: "haiku",
|
||||||
|
cwd,
|
||||||
|
title: "Wait Permission Test",
|
||||||
|
modeId: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(agent.id).toBeTruthy();
|
||||||
|
expect(agent.status).toBe("idle");
|
||||||
|
|
||||||
|
// Clear message queue before sending prompt
|
||||||
|
ctx.client.clearMessageQueue();
|
||||||
|
|
||||||
|
// Send a prompt that requires file write permission
|
||||||
|
const prompt = [
|
||||||
|
`You must use the Write tool to create a file at "${testFilePath}" with content "hello".`,
|
||||||
|
"Do not respond before attempting to write the file.",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
await ctx.client.sendMessage(agent.id, prompt);
|
||||||
|
|
||||||
|
// CRITICAL: This is the behavior we're testing
|
||||||
|
// waitForAgentIdle should return when permission is requested,
|
||||||
|
// NOT wait until the agent is fully idle (which would timeout or
|
||||||
|
// require us to approve the permission first)
|
||||||
|
const startTime = Date.now();
|
||||||
|
const state = await ctx.client.waitForAgentIdle(agent.id, 60000);
|
||||||
|
const waitDuration = Date.now() - startTime;
|
||||||
|
|
||||||
|
// If wait returns because of permission request, we should have:
|
||||||
|
// 1. Agent status still "running" (not idle yet - waiting for permission)
|
||||||
|
// 2. Pending permissions in the state
|
||||||
|
// 3. Wait should return quickly (under 30 seconds, not timeout)
|
||||||
|
|
||||||
|
// Check that we have a pending permission
|
||||||
|
const hasPendingPermission =
|
||||||
|
(state.pendingPermissions && state.pendingPermissions.length > 0);
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
console.log("Wait returned after", waitDuration, "ms");
|
||||||
|
console.log("Agent status:", state.status);
|
||||||
|
console.log("Pending permissions:", state.pendingPermissions?.length ?? 0);
|
||||||
|
|
||||||
|
// THE ASSERTION:
|
||||||
|
// If waitForAgentIdle correctly yields on permission request,
|
||||||
|
// we should either:
|
||||||
|
// - Get a state with pending permissions (status might be "running")
|
||||||
|
// - Or the state should be from right when permission was requested
|
||||||
|
//
|
||||||
|
// If waitForAgentIdle does NOT yield on permission request,
|
||||||
|
// this test will either:
|
||||||
|
// - Timeout (60s)
|
||||||
|
// - Return only after we never approve permission and agent errors/gives up
|
||||||
|
|
||||||
|
// This test will FAIL if waitForAgentIdle waits for full idle
|
||||||
|
// instead of returning on permission request
|
||||||
|
expect(hasPendingPermission).toBe(true);
|
||||||
|
|
||||||
|
// Also verify we can get the permission via waitForPermission
|
||||||
|
// (This should return immediately since permission is already pending)
|
||||||
|
const permission = await ctx.client.waitForPermission(agent.id, 5000);
|
||||||
|
expect(permission).toBeTruthy();
|
||||||
|
expect(permission.kind).toBe("tool");
|
||||||
|
|
||||||
|
// Clean up: deny the permission so agent can finish
|
||||||
|
await ctx.client.respondToPermission(agent.id, permission.id, {
|
||||||
|
behavior: "deny",
|
||||||
|
message: "Test complete",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for agent to finish processing the denial
|
||||||
|
await ctx.client.waitForAgentIdle(agent.id, 30000);
|
||||||
|
|
||||||
|
await ctx.client.deleteAgent(agent.id);
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
120000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,9 @@ export type {
|
|||||||
AgentTimelineItem,
|
AgentTimelineItem,
|
||||||
} from "./agent/agent-sdk-types.js";
|
} from "./agent/agent-sdk-types.js";
|
||||||
|
|
||||||
|
// Agent activity curator for CLI logs
|
||||||
|
export { curateAgentActivity } from "./agent/activity-curator.js";
|
||||||
|
|
||||||
// WebSocket message types for CLI streaming
|
// WebSocket message types for CLI streaming
|
||||||
export type {
|
export type {
|
||||||
AgentSnapshotPayload,
|
AgentSnapshotPayload,
|
||||||
|
|||||||
@@ -623,6 +623,7 @@ export class Session {
|
|||||||
attentionReason: record.attentionReason ?? null,
|
attentionReason: record.attentionReason ?? null,
|
||||||
attentionTimestamp: record.attentionTimestamp ?? null,
|
attentionTimestamp: record.attentionTimestamp ?? null,
|
||||||
archivedAt: record.archivedAt ?? null,
|
archivedAt: record.archivedAt ?? null,
|
||||||
|
labels: record.labels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,7 +655,7 @@ export class Session {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const config = buildSessionConfig(record);
|
const config = buildSessionConfig(record);
|
||||||
snapshot = await this.agentManager.createAgent(config, agentId);
|
snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.agentManager.primeAgentHistory(agentId);
|
await this.agentManager.primeAgentHistory(agentId);
|
||||||
|
|||||||
Reference in New Issue
Block a user