feat(cli): add --output-schema flag for structured agent output
This commit is contained in:
@@ -67,6 +67,7 @@ export function createCli(): Command {
|
|||||||
.option('--cwd <path>', 'Working directory (default: current)')
|
.option('--cwd <path>', 'Working directory (default: current)')
|
||||||
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
||||||
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
||||||
|
.option('--output-schema <schema>', 'Output JSON matching the provided schema file path or inline JSON schema')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runRunCommand))
|
.action(withOutput(runRunCommand))
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function createAgentCommand(): Command {
|
|||||||
.option('--cwd <path>', 'Working directory (default: current)')
|
.option('--cwd <path>', 'Working directory (default: current)')
|
||||||
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
||||||
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
||||||
|
.option('--output-schema <schema>', 'Output JSON matching the provided schema file path or inline JSON schema')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runRunCommand))
|
.action(withOutput(runRunCommand))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { lookup } from 'mime-types'
|
|||||||
/** Result type for agent run command */
|
/** Result type for agent run command */
|
||||||
export interface AgentRunResult {
|
export interface AgentRunResult {
|
||||||
agentId: string
|
agentId: string
|
||||||
status: 'created' | 'running'
|
status: 'created' | 'running' | 'completed' | 'timeout' | 'permission' | 'error'
|
||||||
provider: string
|
provider: string
|
||||||
cwd: string
|
cwd: string
|
||||||
title: string | null
|
title: string | null
|
||||||
@@ -39,24 +39,178 @@ export interface AgentRunOptions extends CommandOptions {
|
|||||||
cwd?: string
|
cwd?: string
|
||||||
label?: string[]
|
label?: string[]
|
||||||
ui?: boolean
|
ui?: boolean
|
||||||
|
outputSchema?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRunResult(agent: AgentSnapshotPayload): AgentRunResult {
|
function toRunResult(
|
||||||
|
agent: AgentSnapshotPayload,
|
||||||
|
statusOverride?: AgentRunResult['status']
|
||||||
|
): AgentRunResult {
|
||||||
return {
|
return {
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
status: agent.status === 'running' ? 'running' : 'created',
|
status: statusOverride ?? (agent.status === 'running' ? 'running' : 'created'),
|
||||||
provider: agent.provider,
|
provider: agent.provider,
|
||||||
cwd: agent.cwd,
|
cwd: agent.cwd,
|
||||||
title: agent.title,
|
title: agent.title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadOutputSchema(value: string): Record<string, unknown> {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'INVALID_OUTPUT_SCHEMA',
|
||||||
|
message: '--output-schema cannot be empty',
|
||||||
|
details: 'Provide a JSON schema file path or inline JSON object',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = trimmed
|
||||||
|
if (!trimmed.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
source = readFileSync(resolve(trimmed), 'utf8')
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'INVALID_OUTPUT_SCHEMA',
|
||||||
|
message: `Failed to read output schema file: ${trimmed}`,
|
||||||
|
details: message,
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(source)
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'INVALID_OUTPUT_SCHEMA',
|
||||||
|
message: 'Failed to parse output schema JSON',
|
||||||
|
details: message,
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'INVALID_OUTPUT_SCHEMA',
|
||||||
|
message: 'Output schema must be a JSON object',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFirstJsonObject(text: string): string | null {
|
||||||
|
const source = text.trim()
|
||||||
|
if (!source) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndexes: number[] = []
|
||||||
|
for (let i = 0; i < source.length; i += 1) {
|
||||||
|
if (source[i] === '{') {
|
||||||
|
startIndexes.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const start of startIndexes) {
|
||||||
|
let depth = 0
|
||||||
|
let inString = false
|
||||||
|
let escaped = false
|
||||||
|
|
||||||
|
for (let i = start; i < source.length; i += 1) {
|
||||||
|
const ch = source[i]!
|
||||||
|
|
||||||
|
if (inString) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === '\\') {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === '"') {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '"') {
|
||||||
|
inString = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === '{') {
|
||||||
|
depth += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1
|
||||||
|
if (depth === 0) {
|
||||||
|
const candidate = source.slice(start, i + 1).trim()
|
||||||
|
try {
|
||||||
|
JSON.parse(candidate)
|
||||||
|
return candidate
|
||||||
|
} catch {
|
||||||
|
// Keep scanning.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStructuredOutput(lastMessage: string): Record<string, unknown> {
|
||||||
|
const trimmed = lastMessage.trim()
|
||||||
|
const fenced = trimmed.match(/```(?:json)?\s*\n([\s\S]*?)\n```/)
|
||||||
|
const jsonText = fenced?.[1]?.trim() ?? extractFirstJsonObject(trimmed) ?? trimmed
|
||||||
|
|
||||||
|
let parsed: unknown
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(jsonText)
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'OUTPUT_SCHEMA_FAILED',
|
||||||
|
message: 'Agent response is not valid JSON',
|
||||||
|
details: message,
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'OUTPUT_SCHEMA_FAILED',
|
||||||
|
message: 'Agent response JSON must be an object',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
function structuredRunSchema(output: Record<string, unknown>): OutputSchema<AgentRunResult> {
|
||||||
|
return {
|
||||||
|
...agentRunSchema,
|
||||||
|
serialize: () => output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function runRunCommand(
|
export async function runRunCommand(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
options: AgentRunOptions,
|
options: AgentRunOptions,
|
||||||
_command: Command
|
_command: Command
|
||||||
): Promise<SingleResult<AgentRunResult>> {
|
): Promise<SingleResult<AgentRunResult>> {
|
||||||
const host = getDaemonHost({ host: options.host as string | undefined })
|
const host = getDaemonHost({ host: options.host as string | undefined })
|
||||||
|
const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined
|
||||||
|
|
||||||
// Validate prompt is provided
|
// Validate prompt is provided
|
||||||
if (!prompt || prompt.trim().length === 0) {
|
if (!prompt || prompt.trim().length === 0) {
|
||||||
@@ -78,6 +232,16 @@ export async function runRunCommand(
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --output-schema always runs in attached/wait mode
|
||||||
|
if (outputSchema && options.detach) {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'INVALID_OPTIONS',
|
||||||
|
message: '--output-schema cannot be used with --detach',
|
||||||
|
details: 'Structured output requires waiting for the agent to finish',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
let client
|
let client
|
||||||
try {
|
try {
|
||||||
client = await connectToDaemon({ host: options.host as string | undefined })
|
client = await connectToDaemon({ host: options.host as string | undefined })
|
||||||
@@ -162,12 +326,75 @@ export async function runRunCommand(
|
|||||||
modeId: options.mode,
|
modeId: options.mode,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
initialPrompt: prompt,
|
initialPrompt: prompt,
|
||||||
|
outputSchema,
|
||||||
images,
|
images,
|
||||||
git,
|
git,
|
||||||
worktreeName: options.worktree,
|
worktreeName: options.worktree,
|
||||||
labels: Object.keys(labels).length > 0 ? labels : undefined,
|
labels: Object.keys(labels).length > 0 ? labels : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (outputSchema) {
|
||||||
|
const state = await client.waitForFinish(agent.id, 10 * 60 * 1000)
|
||||||
|
|
||||||
|
if (state.status === 'timeout') {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'OUTPUT_SCHEMA_FAILED',
|
||||||
|
message: 'Timed out waiting for structured output',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'permission') {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'OUTPUT_SCHEMA_FAILED',
|
||||||
|
message: 'Agent is waiting for permission before producing structured output',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'error') {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'OUTPUT_SCHEMA_FAILED',
|
||||||
|
message: state.error ?? 'Agent failed before producing structured output',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage = state.lastMessage?.trim()
|
||||||
|
if (!lastMessage) {
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'OUTPUT_SCHEMA_FAILED',
|
||||||
|
message: 'Agent finished without a structured output message',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = parseStructuredOutput(lastMessage)
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'single',
|
||||||
|
data: toRunResult(agent, 'completed'),
|
||||||
|
schema: structuredRunSchema(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default run behavior is foreground: wait for completion unless --detach is set.
|
||||||
|
if (!options.detach) {
|
||||||
|
const state = await client.waitForFinish(agent.id, 10 * 60 * 1000)
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
const finalAgent = state.final ?? agent
|
||||||
|
const status: AgentRunResult['status'] =
|
||||||
|
state.status === 'idle' ? 'completed' : state.status
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'single',
|
||||||
|
data: toRunResult(finalAgent, status),
|
||||||
|
schema: agentRunSchema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -177,6 +404,11 @@ export async function runRunCommand(
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await client.close().catch(() => {})
|
await client.close().catch(() => {})
|
||||||
|
|
||||||
|
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 = {
|
const error: CommandError = {
|
||||||
code: 'AGENT_CREATE_FAILED',
|
code: 'AGENT_CREATE_FAILED',
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ function printNextSteps(pairingUrl: string | null, paseoHome: string, richUi: bo
|
|||||||
'2. Web app: https://app.paseo.sh',
|
'2. Web app: https://app.paseo.sh',
|
||||||
'3. Desktop app: https://github.com/getpaseo/paseo/releases/latest',
|
'3. Desktop app: https://github.com/getpaseo/paseo/releases/latest',
|
||||||
'4. Docs: https://paseo.sh/docs',
|
'4. Docs: https://paseo.sh/docs',
|
||||||
|
'5. Example: paseo run --output-schema schema.json "extract fields"',
|
||||||
]
|
]
|
||||||
const quickReferenceLines = [
|
const quickReferenceLines = [
|
||||||
'1. paseo --help',
|
'1. paseo --help',
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ function normalizeFormat(raw: unknown): OutputOptions['format'] {
|
|||||||
|
|
||||||
/** Extract output options from command options */
|
/** Extract output options from command options */
|
||||||
function extractOutputOptions(options: CommandOptions): OutputOptions {
|
function extractOutputOptions(options: CommandOptions): OutputOptions {
|
||||||
|
const hasStructuredOutputSchema =
|
||||||
|
typeof options.outputSchema === 'string' && options.outputSchema.trim().length > 0
|
||||||
|
|
||||||
|
if (hasStructuredOutputSchema) {
|
||||||
|
return {
|
||||||
|
format: 'json',
|
||||||
|
quiet: false,
|
||||||
|
noHeaders: options.headers === false,
|
||||||
|
noColor: options.color === false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
format: options.json ? 'json' : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
format: options.json ? 'json' : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
||||||
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
import assert from 'node:assert'
|
import assert from 'node:assert'
|
||||||
import { $ } from 'zx'
|
import { $ } from 'zx'
|
||||||
import { mkdtemp, rm } from 'fs/promises'
|
import { mkdtemp, rm, writeFile } from 'fs/promises'
|
||||||
import { tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
@@ -33,6 +33,18 @@ console.log('=== Run Command Tests ===\n')
|
|||||||
// Get random port that's definitely not in use (never 6767)
|
// Get random port that's definitely not in use (never 6767)
|
||||||
const port = 10000 + Math.floor(Math.random() * 50000)
|
const port = 10000 + Math.floor(Math.random() * 50000)
|
||||||
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
|
const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-'))
|
||||||
|
const schemaPath = join(paseoHome, 'run-output-schema.json')
|
||||||
|
await writeFile(
|
||||||
|
schemaPath,
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
status: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['status'],
|
||||||
|
additionalProperties: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Test 1: run --help shows options
|
// Test 1: run --help shows options
|
||||||
@@ -46,6 +58,7 @@ try {
|
|||||||
assert(result.stdout.includes('--provider'), 'help should mention --provider option')
|
assert(result.stdout.includes('--provider'), 'help should mention --provider option')
|
||||||
assert(result.stdout.includes('--mode'), 'help should mention --mode option')
|
assert(result.stdout.includes('--mode'), 'help should mention --mode option')
|
||||||
assert(result.stdout.includes('--cwd'), 'help should mention --cwd option')
|
assert(result.stdout.includes('--cwd'), 'help should mention --cwd option')
|
||||||
|
assert(result.stdout.includes('--output-schema'), 'help should mention --output-schema option')
|
||||||
assert(result.stdout.includes('--host'), 'help should mention --host option')
|
assert(result.stdout.includes('--host'), 'help should mention --host option')
|
||||||
assert(result.stdout.includes('<prompt>'), 'help should mention prompt argument')
|
assert(result.stdout.includes('<prompt>'), 'help should mention prompt argument')
|
||||||
console.log('✓ run --help shows options\n')
|
console.log('✓ run --help shows options\n')
|
||||||
@@ -138,9 +151,34 @@ try {
|
|||||||
console.log('✓ run --cwd flag is accepted\n')
|
console.log('✓ run --cwd flag is accepted\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 9: -q (quiet) flag is accepted with run
|
// Test 9: run --output-schema flag is accepted
|
||||||
{
|
{
|
||||||
console.log('Test 9: -q (quiet) flag is accepted with run')
|
console.log('Test 9: run --output-schema flag is accepted')
|
||||||
|
const result =
|
||||||
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run --output-schema ${schemaPath} "test prompt"`.nothrow()
|
||||||
|
const output = result.stdout + result.stderr
|
||||||
|
assert(!output.includes('unknown option'), 'should accept --output-schema flag')
|
||||||
|
assert(!output.includes('error: option'), 'should not have option parsing error')
|
||||||
|
console.log('✓ run --output-schema flag is accepted\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 10: run --output-schema cannot be used with --detach
|
||||||
|
{
|
||||||
|
console.log('Test 10: run --output-schema cannot be used with --detach')
|
||||||
|
const result =
|
||||||
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo run -d --output-schema ${schemaPath} "test prompt"`.nothrow()
|
||||||
|
assert.notStrictEqual(result.exitCode, 0, 'should fail with --detach and --output-schema')
|
||||||
|
const output = result.stdout + result.stderr
|
||||||
|
assert(
|
||||||
|
output.includes('--output-schema cannot be used with --detach'),
|
||||||
|
'error should explain detach incompatibility'
|
||||||
|
)
|
||||||
|
console.log('✓ run --output-schema cannot be used with --detach\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 11: -q (quiet) flag is accepted with run
|
||||||
|
{
|
||||||
|
console.log('Test 11: -q (quiet) flag is accepted with run')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d "test prompt"`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d "test prompt"`.nothrow()
|
||||||
const output = result.stdout + result.stderr
|
const output = result.stdout + result.stderr
|
||||||
@@ -149,9 +187,9 @@ try {
|
|||||||
console.log('✓ -q (quiet) flag is accepted with run\n')
|
console.log('✓ -q (quiet) flag is accepted with run\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 10: Combined flags work together
|
// Test 12: Combined flags work together
|
||||||
{
|
{
|
||||||
console.log('Test 10: Combined flags work together')
|
console.log('Test 12: Combined flags work together')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d --name "test-fixer" --provider claude --mode bypass --cwd /tmp "Fix the tests"`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d --name "test-fixer" --provider claude --mode bypass --cwd /tmp "Fix the tests"`.nothrow()
|
||||||
const output = result.stdout + result.stderr
|
const output = result.stdout + result.stderr
|
||||||
@@ -160,9 +198,9 @@ try {
|
|||||||
console.log('✓ Combined flags work together\n')
|
console.log('✓ Combined flags work together\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 11: paseo --help shows run command
|
// Test 13: paseo --help shows run command
|
||||||
{
|
{
|
||||||
console.log('Test 11: paseo --help shows run command')
|
console.log('Test 13: paseo --help shows run command')
|
||||||
const result = await $`npx paseo --help`.nothrow()
|
const result = await $`npx paseo --help`.nothrow()
|
||||||
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
|
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
|
||||||
assert(result.stdout.includes('run'), 'help should mention run command')
|
assert(result.stdout.includes('run'), 'help should mention run command')
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export type CreateAgentRequestOptions = {
|
|||||||
provider?: AgentProvider;
|
provider?: AgentProvider;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
initialPrompt?: string;
|
initialPrompt?: string;
|
||||||
|
outputSchema?: Record<string, unknown>;
|
||||||
images?: CreateAgentRequestMessage["images"];
|
images?: CreateAgentRequestMessage["images"];
|
||||||
git?: GitSetupOptions;
|
git?: GitSetupOptions;
|
||||||
worktreeName?: string;
|
worktreeName?: string;
|
||||||
@@ -224,6 +225,7 @@ export type WaitForFinishResult = {
|
|||||||
status: "idle" | "error" | "permission" | "timeout";
|
status: "idle" | "error" | "permission" | "timeout";
|
||||||
final: AgentSnapshotPayload | null;
|
final: AgentSnapshotPayload | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
lastMessage: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Waiter<T> = {
|
type Waiter<T> = {
|
||||||
@@ -984,6 +986,7 @@ export class DaemonClient {
|
|||||||
requestId,
|
requestId,
|
||||||
config,
|
config,
|
||||||
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
||||||
|
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
||||||
...(options.images && options.images.length > 0
|
...(options.images && options.images.length > 0
|
||||||
? { images: options.images }
|
? { images: options.images }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -2260,6 +2263,7 @@ export class DaemonClient {
|
|||||||
status: payload.status,
|
status: payload.status,
|
||||||
final: payload.final,
|
final: payload.final,
|
||||||
error: payload.error,
|
error: payload.error,
|
||||||
|
lastMessage: payload.lastMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import type {
|
|||||||
AgentPermissionResponse,
|
AgentPermissionResponse,
|
||||||
AgentPromptContentBlock,
|
AgentPromptContentBlock,
|
||||||
AgentPromptInput,
|
AgentPromptInput,
|
||||||
|
AgentRunOptions,
|
||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
@@ -683,7 +684,8 @@ export class Session {
|
|||||||
*/
|
*/
|
||||||
private startAgentStream(
|
private startAgentStream(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
prompt: AgentPromptInput
|
prompt: AgentPromptInput,
|
||||||
|
runOptions?: AgentRunOptions
|
||||||
): { ok: true } | { ok: false; error: string } {
|
): { ok: true } | { ok: false; error: string } {
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{ agentId },
|
{ agentId },
|
||||||
@@ -692,7 +694,7 @@ export class Session {
|
|||||||
|
|
||||||
let iterator: AsyncGenerator<AgentStreamEvent>;
|
let iterator: AsyncGenerator<AgentStreamEvent>;
|
||||||
try {
|
try {
|
||||||
iterator = this.agentManager.streamAgent(agentId, prompt);
|
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
||||||
const message =
|
const message =
|
||||||
@@ -2036,7 +2038,8 @@ export class Session {
|
|||||||
agentId: string,
|
agentId: string,
|
||||||
text: string,
|
text: string,
|
||||||
messageId?: string,
|
messageId?: string,
|
||||||
images?: Array<{ data: string; mimeType: string }>
|
images?: Array<{ data: string; mimeType: string }>,
|
||||||
|
runOptions?: AgentRunOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 },
|
{ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 },
|
||||||
@@ -2076,7 +2079,7 @@ export class Session {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startAgentStream(agentId, prompt);
|
this.startAgentStream(agentId, prompt, runOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2124,7 +2127,7 @@ export class Session {
|
|||||||
private async handleCreateAgentRequest(
|
private async handleCreateAgentRequest(
|
||||||
msg: Extract<SessionInboundMessage, { type: "create_agent_request" }>
|
msg: Extract<SessionInboundMessage, { type: "create_agent_request" }>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg;
|
const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = msg;
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
{ cwd: config.cwd, provider: config.provider, worktreeName },
|
{ cwd: config.cwd, provider: config.provider, worktreeName },
|
||||||
`Creating agent in ${config.cwd} (${config.provider})${
|
`Creating agent in ${config.cwd} (${config.provider})${
|
||||||
@@ -2163,7 +2166,8 @@ export class Session {
|
|||||||
snapshot.id,
|
snapshot.id,
|
||||||
trimmedPrompt,
|
trimmedPrompt,
|
||||||
uuidv4(),
|
uuidv4(),
|
||||||
images
|
images,
|
||||||
|
outputSchema ? { outputSchema } : undefined
|
||||||
);
|
);
|
||||||
} catch (promptError) {
|
} catch (promptError) {
|
||||||
this.sessionLogger.error(
|
this.sessionLogger.error(
|
||||||
@@ -4832,7 +4836,7 @@ export class Session {
|
|||||||
if (!resolved.ok) {
|
if (!resolved.ok) {
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "wait_for_finish_response",
|
type: "wait_for_finish_response",
|
||||||
payload: { requestId, status: "error", final: null, error: resolved.error },
|
payload: { requestId, status: "error", final: null, error: resolved.error, lastMessage: null },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4849,6 +4853,7 @@ export class Session {
|
|||||||
status: "error",
|
status: "error",
|
||||||
final: null,
|
final: null,
|
||||||
error: `Agent not found: ${agentId}`,
|
error: `Agent not found: ${agentId}`,
|
||||||
|
lastMessage: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -4862,7 +4867,7 @@ export class Session {
|
|||||||
: "idle";
|
: "idle";
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "wait_for_finish_response",
|
type: "wait_for_finish_response",
|
||||||
payload: { requestId, status, final, error: null },
|
payload: { requestId, status, final, error: null, lastMessage: null },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4892,7 +4897,7 @@ export class Session {
|
|||||||
|
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "wait_for_finish_response",
|
type: "wait_for_finish_response",
|
||||||
payload: { requestId, status, final, error: null },
|
payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const isAbort =
|
const isAbort =
|
||||||
@@ -4910,6 +4915,7 @@ export class Session {
|
|||||||
status: "error",
|
status: "error",
|
||||||
final,
|
final,
|
||||||
error: message,
|
error: message,
|
||||||
|
lastMessage: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -4921,7 +4927,7 @@ export class Session {
|
|||||||
}
|
}
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "wait_for_finish_response",
|
type: "wait_for_finish_response",
|
||||||
payload: { requestId, status: "timeout", final, error: null },
|
payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutHandle);
|
clearTimeout(timeoutHandle);
|
||||||
|
|||||||
@@ -548,6 +548,7 @@ export const CreateAgentRequestMessageSchema = z.object({
|
|||||||
config: AgentSessionConfigSchema,
|
config: AgentSessionConfigSchema,
|
||||||
worktreeName: z.string().optional(),
|
worktreeName: z.string().optional(),
|
||||||
initialPrompt: z.string().optional(),
|
initialPrompt: z.string().optional(),
|
||||||
|
outputSchema: z.record(z.unknown()).optional(),
|
||||||
images: z.array(z.object({
|
images: z.array(z.object({
|
||||||
data: z.string(), // base64 encoded image
|
data: z.string(), // base64 encoded image
|
||||||
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
||||||
@@ -1360,6 +1361,7 @@ export const WaitForFinishResponseMessageSchema = z.object({
|
|||||||
status: z.enum(["idle", "error", "permission", "timeout"]),
|
status: z.enum(["idle", "error", "permission", "timeout"]),
|
||||||
final: AgentSnapshotPayloadSchema.nullable(),
|
final: AgentSnapshotPayloadSchema.nullable(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
|
lastMessage: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,12 +64,23 @@ paseo stop <id> # Stop an agent`}</pre>
|
|||||||
<pre className="text-white/80">{`paseo run "implement user authentication"
|
<pre className="text-white/80">{`paseo run "implement user authentication"
|
||||||
paseo run --provider codex "refactor the API layer"
|
paseo run --provider codex "refactor the API layer"
|
||||||
paseo run --detach "run the full test suite" # background
|
paseo run --detach "run the full test suite" # background
|
||||||
paseo run --worktree feature-x "implement feature X"`}</pre>
|
paseo run --worktree feature-x "implement feature X"
|
||||||
|
paseo run --output-schema schema.json "extract release notes"
|
||||||
|
paseo run --output-schema '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}' "summarize release notes"`}</pre>
|
||||||
</Code>
|
</Code>
|
||||||
<p className="text-white/60 leading-relaxed">
|
<p className="text-white/60 leading-relaxed">
|
||||||
The <code className="font-mono">--worktree</code> flag creates the agent in an isolated git
|
The <code className="font-mono">--worktree</code> flag creates the agent in an isolated git
|
||||||
worktree, useful for parallel feature development.
|
worktree, useful for parallel feature development.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-white/60 leading-relaxed">
|
||||||
|
Use <code className="font-mono">--output-schema</code> to return only matching JSON output.
|
||||||
|
You can pass a schema file path or an inline JSON schema object.
|
||||||
|
This mode cannot be used with <code className="font-mono">--detach</code>.
|
||||||
|
</p>
|
||||||
|
<p className="text-white/60 leading-relaxed">
|
||||||
|
By default, <code className="font-mono">paseo run</code> waits for completion. Use{' '}
|
||||||
|
<code className="font-mono">--detach</code> to run in the background.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Listing agents */}
|
{/* Listing agents */}
|
||||||
@@ -187,6 +198,21 @@ paseo daemon stop # Stop the daemon`}</pre>
|
|||||||
paseo run --detach "implement the API" --name api-agent
|
paseo run --detach "implement the API" --name api-agent
|
||||||
paseo wait api-agent
|
paseo wait api-agent
|
||||||
paseo logs api-agent --tail 5`}</pre>
|
paseo logs api-agent --tail 5`}</pre>
|
||||||
|
</Code>
|
||||||
|
<p className="text-white/60 leading-relaxed">
|
||||||
|
Simple implement + verify loop:
|
||||||
|
</p>
|
||||||
|
<Code>
|
||||||
|
<pre className="text-white/80">{`# Requires jq
|
||||||
|
while true; do
|
||||||
|
paseo run --provider codex "make the tests pass" >/dev/null
|
||||||
|
|
||||||
|
verdict=$(paseo run --provider claude --output-schema '{"type":"object","properties":{"criteria_met":{"type":"boolean"}},"required":["criteria_met"],"additionalProperties":false}' "ensure tests all pass")
|
||||||
|
if echo "$verdict" | jq -e '.criteria_met == true' >/dev/null; then
|
||||||
|
echo "criteria met"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done`}</pre>
|
||||||
</Code>
|
</Code>
|
||||||
<p className="text-white/60 leading-relaxed">
|
<p className="text-white/60 leading-relaxed">
|
||||||
This pattern enables hierarchical task decomposition — a lead agent can break down work,
|
This pattern enables hierarchical task decomposition — a lead agent can break down work,
|
||||||
|
|||||||
Reference in New Issue
Block a user