Merge branch 'feature/cli-output-schema'
This commit is contained in:
@@ -67,6 +67,7 @@ export function createCli(): Command {
|
||||
.option('--cwd <path>', 'Working directory (default: current)')
|
||||
.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('--output-schema <schema>', 'Output JSON matching the provided schema file path or inline JSON schema')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runRunCommand))
|
||||
|
||||
@@ -43,6 +43,7 @@ export function createAgentCommand(): Command {
|
||||
.option('--cwd <path>', 'Working directory (default: current)')
|
||||
.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('--output-schema <schema>', 'Output JSON matching the provided schema file path or inline JSON schema')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runRunCommand))
|
||||
|
||||
@@ -9,7 +9,7 @@ import { lookup } from 'mime-types'
|
||||
/** Result type for agent run command */
|
||||
export interface AgentRunResult {
|
||||
agentId: string
|
||||
status: 'created' | 'running'
|
||||
status: 'created' | 'running' | 'completed' | 'timeout' | 'permission' | 'error'
|
||||
provider: string
|
||||
cwd: string
|
||||
title: string | null
|
||||
@@ -39,24 +39,178 @@ export interface AgentRunOptions extends CommandOptions {
|
||||
cwd?: string
|
||||
label?: string[]
|
||||
ui?: boolean
|
||||
outputSchema?: string
|
||||
}
|
||||
|
||||
function toRunResult(agent: AgentSnapshotPayload): AgentRunResult {
|
||||
function toRunResult(
|
||||
agent: AgentSnapshotPayload,
|
||||
statusOverride?: AgentRunResult['status']
|
||||
): AgentRunResult {
|
||||
return {
|
||||
agentId: agent.id,
|
||||
status: agent.status === 'running' ? 'running' : 'created',
|
||||
status: statusOverride ?? (agent.status === 'running' ? 'running' : 'created'),
|
||||
provider: agent.provider,
|
||||
cwd: agent.cwd,
|
||||
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(
|
||||
prompt: string,
|
||||
options: AgentRunOptions,
|
||||
_command: Command
|
||||
): Promise<SingleResult<AgentRunResult>> {
|
||||
const host = getDaemonHost({ host: options.host as string | undefined })
|
||||
const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined
|
||||
|
||||
// Validate prompt is provided
|
||||
if (!prompt || prompt.trim().length === 0) {
|
||||
@@ -78,6 +232,16 @@ export async function runRunCommand(
|
||||
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
|
||||
try {
|
||||
client = await connectToDaemon({ host: options.host as string | undefined })
|
||||
@@ -162,12 +326,75 @@ export async function runRunCommand(
|
||||
modeId: options.mode,
|
||||
model: options.model,
|
||||
initialPrompt: prompt,
|
||||
outputSchema,
|
||||
images,
|
||||
git,
|
||||
worktreeName: options.worktree,
|
||||
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()
|
||||
|
||||
return {
|
||||
@@ -177,6 +404,11 @@ export async function runRunCommand(
|
||||
}
|
||||
} catch (err) {
|
||||
await client.close().catch(() => {})
|
||||
|
||||
if (err && typeof err === 'object' && 'code' in err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const error: CommandError = {
|
||||
code: 'AGENT_CREATE_FAILED',
|
||||
|
||||
@@ -483,6 +483,7 @@ function printNextSteps(pairingUrl: string | null, paseoHome: string, richUi: bo
|
||||
'2. Web app: https://app.paseo.sh',
|
||||
'3. Desktop app: https://github.com/getpaseo/paseo/releases/latest',
|
||||
'4. Docs: https://paseo.sh/docs',
|
||||
'5. Example: paseo run --output-schema schema.json "extract fields"',
|
||||
]
|
||||
const quickReferenceLines = [
|
||||
'1. paseo --help',
|
||||
|
||||
@@ -31,6 +31,18 @@ function normalizeFormat(raw: unknown): OutputOptions['format'] {
|
||||
|
||||
/** Extract output options from command options */
|
||||
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 {
|
||||
format: options.json ? 'json' : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
||||
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
import assert from 'node:assert'
|
||||
import { $ } from 'zx'
|
||||
import { mkdtemp, rm } from 'fs/promises'
|
||||
import { mkdtemp, rm, writeFile } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
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)
|
||||
const port = 10000 + Math.floor(Math.random() * 50000)
|
||||
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 {
|
||||
// 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('--mode'), 'help should mention --mode 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('<prompt>'), 'help should mention prompt argument')
|
||||
console.log('✓ run --help shows options\n')
|
||||
@@ -138,9 +151,34 @@ try {
|
||||
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 =
|
||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -q run -d "test prompt"`.nothrow()
|
||||
const output = result.stdout + result.stderr
|
||||
@@ -149,9 +187,9 @@ try {
|
||||
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 =
|
||||
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
|
||||
@@ -160,9 +198,9 @@ try {
|
||||
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()
|
||||
assert.strictEqual(result.exitCode, 0, 'paseo --help should exit 0')
|
||||
assert(result.stdout.includes('run'), 'help should mention run command')
|
||||
|
||||
@@ -170,6 +170,7 @@ export type CreateAgentRequestOptions = {
|
||||
provider?: AgentProvider;
|
||||
cwd?: string;
|
||||
initialPrompt?: string;
|
||||
outputSchema?: Record<string, unknown>;
|
||||
images?: CreateAgentRequestMessage["images"];
|
||||
git?: GitSetupOptions;
|
||||
worktreeName?: string;
|
||||
@@ -226,6 +227,7 @@ export type WaitForFinishResult = {
|
||||
status: "idle" | "error" | "permission" | "timeout";
|
||||
final: AgentSnapshotPayload | null;
|
||||
error: string | null;
|
||||
lastMessage: string | null;
|
||||
};
|
||||
|
||||
type Waiter<T> = {
|
||||
@@ -986,6 +988,7 @@ export class DaemonClient {
|
||||
requestId,
|
||||
config,
|
||||
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
||||
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
||||
...(options.images && options.images.length > 0
|
||||
? { images: options.images }
|
||||
: {}),
|
||||
@@ -2287,6 +2290,7 @@ export class DaemonClient {
|
||||
status: payload.status,
|
||||
final: payload.final,
|
||||
error: payload.error,
|
||||
lastMessage: payload.lastMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ import type {
|
||||
AgentPermissionResponse,
|
||||
AgentPromptContentBlock,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
McpServerConfig,
|
||||
AgentSessionConfig,
|
||||
AgentStreamEvent,
|
||||
@@ -683,7 +684,8 @@ export class Session {
|
||||
*/
|
||||
private startAgentStream(
|
||||
agentId: string,
|
||||
prompt: AgentPromptInput
|
||||
prompt: AgentPromptInput,
|
||||
runOptions?: AgentRunOptions
|
||||
): { ok: true } | { ok: false; error: string } {
|
||||
this.sessionLogger.info(
|
||||
{ agentId },
|
||||
@@ -692,7 +694,7 @@ export class Session {
|
||||
|
||||
let iterator: AsyncGenerator<AgentStreamEvent>;
|
||||
try {
|
||||
iterator = this.agentManager.streamAgent(agentId, prompt);
|
||||
iterator = this.agentManager.streamAgent(agentId, prompt, runOptions);
|
||||
} catch (error) {
|
||||
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
||||
const message =
|
||||
@@ -2040,7 +2042,8 @@ export class Session {
|
||||
agentId: string,
|
||||
text: string,
|
||||
messageId?: string,
|
||||
images?: Array<{ data: string; mimeType: string }>
|
||||
images?: Array<{ data: string; mimeType: string }>,
|
||||
runOptions?: AgentRunOptions
|
||||
): Promise<void> {
|
||||
this.sessionLogger.info(
|
||||
{ agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 },
|
||||
@@ -2080,7 +2083,7 @@ export class Session {
|
||||
);
|
||||
}
|
||||
|
||||
this.startAgentStream(agentId, prompt);
|
||||
this.startAgentStream(agentId, prompt, runOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2128,7 +2131,7 @@ export class Session {
|
||||
private async handleCreateAgentRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "create_agent_request" }>
|
||||
): Promise<void> {
|
||||
const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg;
|
||||
const { config, worktreeName, requestId, initialPrompt, outputSchema, git, images, labels } = msg;
|
||||
this.sessionLogger.info(
|
||||
{ cwd: config.cwd, provider: config.provider, worktreeName },
|
||||
`Creating agent in ${config.cwd} (${config.provider})${
|
||||
@@ -2167,7 +2170,8 @@ export class Session {
|
||||
snapshot.id,
|
||||
trimmedPrompt,
|
||||
uuidv4(),
|
||||
images
|
||||
images,
|
||||
outputSchema ? { outputSchema } : undefined
|
||||
);
|
||||
} catch (promptError) {
|
||||
this.sessionLogger.error(
|
||||
@@ -4868,7 +4872,7 @@ export class Session {
|
||||
if (!resolved.ok) {
|
||||
this.emit({
|
||||
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;
|
||||
}
|
||||
@@ -4885,6 +4889,7 @@ export class Session {
|
||||
status: "error",
|
||||
final: null,
|
||||
error: `Agent not found: ${agentId}`,
|
||||
lastMessage: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -4898,7 +4903,7 @@ export class Session {
|
||||
: "idle";
|
||||
this.emit({
|
||||
type: "wait_for_finish_response",
|
||||
payload: { requestId, status, final, error: null },
|
||||
payload: { requestId, status, final, error: null, lastMessage: null },
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -4928,7 +4933,7 @@ export class Session {
|
||||
|
||||
this.emit({
|
||||
type: "wait_for_finish_response",
|
||||
payload: { requestId, status, final, error: null },
|
||||
payload: { requestId, status, final, error: null, lastMessage: result.lastMessage },
|
||||
});
|
||||
} catch (error) {
|
||||
const isAbort =
|
||||
@@ -4946,6 +4951,7 @@ export class Session {
|
||||
status: "error",
|
||||
final,
|
||||
error: message,
|
||||
lastMessage: null,
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -4957,7 +4963,7 @@ export class Session {
|
||||
}
|
||||
this.emit({
|
||||
type: "wait_for_finish_response",
|
||||
payload: { requestId, status: "timeout", final, error: null },
|
||||
payload: { requestId, status: "timeout", final, error: null, lastMessage: null },
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutHandle);
|
||||
|
||||
@@ -548,6 +548,7 @@ export const CreateAgentRequestMessageSchema = z.object({
|
||||
config: AgentSessionConfigSchema,
|
||||
worktreeName: z.string().optional(),
|
||||
initialPrompt: z.string().optional(),
|
||||
outputSchema: z.record(z.unknown()).optional(),
|
||||
images: z.array(z.object({
|
||||
data: z.string(), // base64 encoded image
|
||||
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
||||
@@ -1366,6 +1367,7 @@ export const WaitForFinishResponseMessageSchema = z.object({
|
||||
status: z.enum(["idle", "error", "permission", "timeout"]),
|
||||
final: AgentSnapshotPayloadSchema.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"
|
||||
paseo run --provider codex "refactor the API layer"
|
||||
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>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The <code className="font-mono">--worktree</code> flag creates the agent in an isolated git
|
||||
worktree, useful for parallel feature development.
|
||||
</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>
|
||||
|
||||
{/* Listing agents */}
|
||||
@@ -187,6 +198,21 @@ paseo daemon stop # Stop the daemon`}</pre>
|
||||
paseo run --detach "implement the API" --name api-agent
|
||||
paseo wait api-agent
|
||||
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>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
This pattern enables hierarchical task decomposition — a lead agent can break down work,
|
||||
|
||||
Reference in New Issue
Block a user