Skip to content

Commit 0847324

Browse files
committed
feat: add af agent run — non-streaming task execution for scripting
`af agent run --agent-id <id> --message "Do X" --json` sends a task and returns a structured JSON result with the response and thread_id. Designed for AI agents calling the CLI via Bash: - No stream parsing needed — just JSON in, JSON out - Returns thread_id for conversation continuity - Timeout + poll interval configurable - Falls back to polling thread status if stream text is empty
1 parent 70729e7 commit 0847324

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed

packages/cli/src/cli/main.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3613,6 +3613,106 @@ export function createProgram(): Command {
36133613
}
36143614
});
36153615

3616+
agentCmd
3617+
.command("run")
3618+
.description("Send a task to an agent and wait for the result. Non-streaming — ideal for scripting and tool calls.")
3619+
.requiredOption("--agent-id <id>", "Agent ID")
3620+
.requiredOption("--message <message>", "Message to send")
3621+
.option("--thread-id <id>", "Thread ID for conversation continuity")
3622+
.option("--timeout <seconds>", "Max seconds to wait for result", "120")
3623+
.option("--poll-interval <seconds>", "Seconds between status checks", "2")
3624+
.action(async (opts) => {
3625+
const client = buildClient(program.opts());
3626+
const timeoutMs = Number.parseInt(opts.timeout, 10) * 1000;
3627+
const pollMs = Number.parseInt(opts.pollInterval, 10) * 1000;
3628+
const threadId = opts.threadId ?? crypto.randomUUID();
3629+
3630+
try {
3631+
// 1. Fire: POST to stream, read thread_id from first line, don't block
3632+
if (!isJsonFlagEnabled()) {
3633+
console.error(`Sending task to agent ${opts.agentId}...`);
3634+
}
3635+
3636+
const streamBody = {
3637+
messages: [{ role: "user" as const, content: opts.message }],
3638+
};
3639+
3640+
const token = resolveToken(program.opts());
3641+
const stream = token
3642+
? await client.agents.stream(opts.agentId, streamBody)
3643+
: await client.agents.streamAnonymous(opts.agentId, streamBody);
3644+
3645+
// Consume stream to get thread info and full text
3646+
const text = await stream.text();
3647+
3648+
// Try to get thread_id from stream metadata
3649+
let resolvedThreadId = threadId;
3650+
try {
3651+
const rawParts = (stream as unknown as { _raw?: string })._raw;
3652+
if (typeof rawParts === "string") {
3653+
for (const line of rawParts.split("\n")) {
3654+
if (line.startsWith("2:")) {
3655+
const arr = JSON.parse(line.slice(2)) as Array<{ type: string; data: Record<string, unknown> }>;
3656+
const info = arr.find((e) => e.type === "thread_info");
3657+
if (info?.data?.thread_id) {
3658+
resolvedThreadId = info.data.thread_id as string;
3659+
}
3660+
}
3661+
}
3662+
}
3663+
} catch { /* ignore — use default threadId */ }
3664+
3665+
// 2. If we got text from stream, return it immediately
3666+
if (text && text.trim()) {
3667+
printResult({
3668+
schema: "agenticflow.agent.run.v1",
3669+
status: "completed",
3670+
agent_id: opts.agentId,
3671+
thread_id: resolvedThreadId,
3672+
response: text,
3673+
});
3674+
return;
3675+
}
3676+
3677+
// 3. Fallback: poll thread status then fetch messages
3678+
if (!isJsonFlagEnabled()) {
3679+
console.error(`Waiting for response (thread: ${resolvedThreadId})...`);
3680+
}
3681+
3682+
const start = Date.now();
3683+
while (Date.now() - start < timeoutMs) {
3684+
try {
3685+
const thread = (await client.agentThreads.get(resolvedThreadId)) as Record<string, unknown>;
3686+
const status = thread.status as string;
3687+
if (status === "processed" || status === "idle") {
3688+
const history = (await client.agentThreads.getMessages(resolvedThreadId)) as {
3689+
messages: Array<{ role: string; content: string }>;
3690+
};
3691+
const msgs = history.messages?.filter((m) => m.role === "assistant") ?? [];
3692+
const lastMsg = msgs.length > 0 ? msgs[msgs.length - 1].content : "";
3693+
printResult({
3694+
schema: "agenticflow.agent.run.v1",
3695+
status: "completed",
3696+
agent_id: opts.agentId,
3697+
thread_id: resolvedThreadId,
3698+
response: lastMsg,
3699+
});
3700+
return;
3701+
}
3702+
if (status === "failed") {
3703+
fail("agent_run_failed", `Agent run failed (thread: ${resolvedThreadId})`);
3704+
}
3705+
} catch { /* thread not ready yet */ }
3706+
await new Promise((r) => setTimeout(r, pollMs));
3707+
}
3708+
3709+
fail("agent_run_timeout", `Agent did not respond within ${opts.timeout}s`, `Thread: ${resolvedThreadId}. Check with: af agent-threads messages --thread-id ${resolvedThreadId}`);
3710+
} catch (err) {
3711+
const message = err instanceof Error ? err.message : String(err);
3712+
fail("agent_run_failed", message);
3713+
}
3714+
});
3715+
36163716
agentCmd
36173717
.command("upload-file")
36183718
.description("Upload a file for an agent.")

0 commit comments

Comments
 (0)