Remove chat rooms and agent loops before storage migration (#3053)
* refactor(storage): remove chat rooms and agent loops This is the prerequisite cleanup for the storage backend migration. Legacy wire schemas remain temporarily so mixed-version peers can still parse old messages, while daemon callers now use query-shaped AgentStorage methods that can map directly to SQL later. * test(cli): remove retired chat and loop coverage
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import { Command, Option } from "commander";
|
||||
import { createAgentCommand } from "./commands/agent/index.js";
|
||||
import { createDaemonCommand } from "./commands/daemon/index.js";
|
||||
import { createChatCommand } from "./commands/chat/index.js";
|
||||
import { createLoopCommand } from "./commands/loop/index.js";
|
||||
import { createPermitCommand } from "./commands/permit/index.js";
|
||||
import { createProviderCommand } from "./commands/provider/index.js";
|
||||
import { createScheduleCommand } from "./commands/schedule/index.js";
|
||||
@@ -168,7 +166,6 @@ export function createCli(): Command {
|
||||
program.addCommand(createHubCommand());
|
||||
|
||||
// Chat commands
|
||||
program.addCommand(createChatCommand());
|
||||
|
||||
// Terminal commands
|
||||
program.addCommand(createTerminalCommand());
|
||||
@@ -176,9 +173,6 @@ export function createCli(): Command {
|
||||
// Workspace script commands
|
||||
program.addCommand(createScriptCommand());
|
||||
|
||||
// Loop commands
|
||||
program.addCommand(createLoopCommand());
|
||||
|
||||
// Schedule commands
|
||||
program.addCommand(createScheduleCommand());
|
||||
program.addCommand(createHeartbeatCommand());
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { SingleResult } from "../../output/index.js";
|
||||
import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js";
|
||||
import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js";
|
||||
|
||||
export interface ChatCreateOptions extends ChatCommandOptions {
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
export async function runCreateCommand(
|
||||
name: string,
|
||||
options: ChatCreateOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<ChatRoomRow>> {
|
||||
const { client } = await connectChatClient(options.host);
|
||||
try {
|
||||
const payload = await client.createChatRoom({
|
||||
name,
|
||||
purpose: options.purpose,
|
||||
});
|
||||
return {
|
||||
type: "single",
|
||||
data: toChatRoomRow(payload.room!),
|
||||
schema: chatRoomSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_CREATE_FAILED", "create chat room", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { SingleResult } from "../../output/index.js";
|
||||
import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js";
|
||||
import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js";
|
||||
|
||||
export async function runDeleteCommand(
|
||||
room: string,
|
||||
options: ChatCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<ChatRoomRow>> {
|
||||
const { client } = await connectChatClient(options.host);
|
||||
try {
|
||||
const payload = await client.deleteChatRoom({ room });
|
||||
return {
|
||||
type: "single",
|
||||
data: toChatRoomRow(payload.room!),
|
||||
schema: chatRoomSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_DELETE_FAILED", "delete chat room", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||
import { runCreateCommand } from "./create.js";
|
||||
import { runLsCommand } from "./ls.js";
|
||||
import { runInspectCommand } from "./inspect.js";
|
||||
import { runDeleteCommand } from "./delete.js";
|
||||
import { runPostCommand } from "./post.js";
|
||||
import { runReadCommand } from "./read.js";
|
||||
import { runWaitCommand } from "./wait.js";
|
||||
|
||||
export function createChatCommand(): Command {
|
||||
const chat = new Command("chat").description("Manage chat rooms for agent coordination");
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
chat
|
||||
.command("create")
|
||||
.description("Create a chat room")
|
||||
.argument("<name>", "Room name (must be unique)")
|
||||
.option("--purpose <text>", "Room purpose/description"),
|
||||
).action(withOutput(runCreateCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(chat.command("ls").description("List chat rooms")).action(
|
||||
withOutput(runLsCommand),
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
chat
|
||||
.command("inspect")
|
||||
.description("Inspect a chat room")
|
||||
.argument("<name-or-id>", "Room name or ID"),
|
||||
).action(withOutput(runInspectCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
chat
|
||||
.command("delete")
|
||||
.description("Delete a chat room")
|
||||
.argument("<name-or-id>", "Room name or ID"),
|
||||
).action(withOutput(runDeleteCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
chat
|
||||
.command("post")
|
||||
.description("Post a chat message")
|
||||
.argument("<name-or-id>", "Room name or ID")
|
||||
.argument("<message>", "Message body")
|
||||
.option("--reply-to <msg-id>", "Reply to a specific message ID"),
|
||||
).action(withOutput(runPostCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
chat
|
||||
.command("read")
|
||||
.description("Read chat messages")
|
||||
.argument("<name-or-id>", "Room name or ID")
|
||||
.option("--limit <n>", "Maximum number of messages to return")
|
||||
.option("--since <duration-or-timestamp>", "Filter by relative duration or ISO timestamp")
|
||||
.option("--agent <agent-id>", "Filter by author agent ID"),
|
||||
).action(withOutput(runReadCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
chat
|
||||
.command("wait")
|
||||
.description("Wait for new chat messages")
|
||||
.argument("<name-or-id>", "Room name or ID")
|
||||
.option("--timeout <duration>", "Maximum wait time"),
|
||||
).action(withOutput(runWaitCommand));
|
||||
|
||||
return chat;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { SingleResult } from "../../output/index.js";
|
||||
import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js";
|
||||
import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js";
|
||||
|
||||
export async function runInspectCommand(
|
||||
room: string,
|
||||
options: ChatCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<ChatRoomRow>> {
|
||||
const { client } = await connectChatClient(options.host);
|
||||
try {
|
||||
const payload = await client.inspectChatRoom({ room });
|
||||
return {
|
||||
type: "single",
|
||||
data: toChatRoomRow(payload.room!),
|
||||
schema: chatRoomSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_INSPECT_FAILED", "inspect chat room", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js";
|
||||
import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js";
|
||||
|
||||
export async function runLsCommand(
|
||||
options: ChatCommandOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<ChatRoomRow>> {
|
||||
const { client } = await connectChatClient(options.host);
|
||||
try {
|
||||
const payload = await client.listChatRooms();
|
||||
return {
|
||||
type: "list",
|
||||
data: payload.rooms.map(toChatRoomRow),
|
||||
schema: chatRoomSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_LIST_FAILED", "list chat rooms", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { SingleResult } from "../../output/index.js";
|
||||
import {
|
||||
attachAgentNamesToMessages,
|
||||
connectChatClient,
|
||||
resolveChatAuthorAgentId,
|
||||
toChatCommandError,
|
||||
type ChatCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./schema.js";
|
||||
|
||||
export interface ChatPostOptions extends ChatCommandOptions {
|
||||
replyTo?: string;
|
||||
}
|
||||
|
||||
export async function runPostCommand(
|
||||
room: string,
|
||||
body: string,
|
||||
options: ChatPostOptions,
|
||||
_command: Command,
|
||||
): Promise<SingleResult<ChatMessageRow>> {
|
||||
const { client } = await connectChatClient(options.host);
|
||||
try {
|
||||
const payload = await client.postChatMessage({
|
||||
room,
|
||||
body,
|
||||
authorAgentId: resolveChatAuthorAgentId(),
|
||||
replyToMessageId: options.replyTo,
|
||||
});
|
||||
const [message] = await attachAgentNamesToMessages(client, [
|
||||
toChatMessageRow(payload.message!),
|
||||
]);
|
||||
return {
|
||||
type: "single",
|
||||
data: message,
|
||||
schema: chatMessageSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_POST_FAILED", "post chat message", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import {
|
||||
attachAgentNamesToMessages,
|
||||
connectChatClient,
|
||||
parseSinceValue,
|
||||
toChatCommandError,
|
||||
type ChatCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./schema.js";
|
||||
|
||||
export interface ChatReadOptions extends ChatCommandOptions {
|
||||
limit?: string;
|
||||
since?: string;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
function parseLimit(value?: string): number | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 0) {
|
||||
throw {
|
||||
code: "INVALID_LIMIT",
|
||||
message: "Invalid --limit value",
|
||||
details: "Use a non-negative integer.",
|
||||
};
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function runReadCommand(
|
||||
room: string,
|
||||
options: ChatReadOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<ChatMessageRow>> {
|
||||
const { client } = await connectChatClient(options.host);
|
||||
try {
|
||||
const payload = await client.readChatMessages({
|
||||
room,
|
||||
limit: parseLimit(options.limit),
|
||||
since: parseSinceValue(options.since),
|
||||
authorAgentId: options.agent,
|
||||
});
|
||||
const messages = await attachAgentNamesToMessages(
|
||||
client,
|
||||
payload.messages.map(toChatMessageRow),
|
||||
);
|
||||
return {
|
||||
type: "list",
|
||||
data: messages,
|
||||
schema: chatMessageSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_READ_FAILED", "read chat messages", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import type { OutputSchema } from "../../output/index.js";
|
||||
import type { AnyCommandResult, OutputOptions } from "../../output/index.js";
|
||||
|
||||
export interface ChatRoomRow {
|
||||
name: string;
|
||||
id: string;
|
||||
purpose: string;
|
||||
messages: number;
|
||||
lastMessageAt: string;
|
||||
}
|
||||
|
||||
export interface ChatMessageRow {
|
||||
id: string;
|
||||
author: string;
|
||||
authorName: string | null;
|
||||
createdAt: string;
|
||||
replyTo: string;
|
||||
mentionAgentIds: string[];
|
||||
mentionLabels: string[];
|
||||
body: string;
|
||||
}
|
||||
|
||||
export const chatRoomSchema: OutputSchema<ChatRoomRow> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "NAME", field: "name", width: 22 },
|
||||
{ header: "ID", field: "id", width: 36 },
|
||||
{ header: "PURPOSE", field: "purpose", width: 30 },
|
||||
{ header: "MESSAGES", field: "messages", width: 10, align: "right" },
|
||||
{ header: "LAST MESSAGE", field: "lastMessageAt", width: 24 },
|
||||
],
|
||||
};
|
||||
|
||||
export const chatMessageSchema: OutputSchema<ChatMessageRow> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "ID", field: "id", width: 36 },
|
||||
{ header: "AUTHOR", field: "author", width: 16 },
|
||||
{ header: "AUTHOR NAME", field: (row) => row.authorName ?? "-", width: 20 },
|
||||
{ header: "CREATED", field: "createdAt", width: 24 },
|
||||
{ header: "REPLY TO", field: "replyTo", width: 36 },
|
||||
{ header: "MENTIONS", field: (row) => row.mentionLabels.join(", ") || "-", width: 24 },
|
||||
{ header: "MESSAGE", field: "body", width: 60 },
|
||||
],
|
||||
renderHuman: renderChatTranscript,
|
||||
};
|
||||
|
||||
function renderChatTranscript(
|
||||
result: AnyCommandResult<ChatMessageRow>,
|
||||
_options: OutputOptions,
|
||||
): string {
|
||||
const data = result.type === "list" ? result.data : [result.data];
|
||||
if (data.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return data.map(renderChatMessageBlock).join("\n\n");
|
||||
}
|
||||
|
||||
function renderChatMessageBlock(message: ChatMessageRow): string {
|
||||
const authorLabel = message.authorName
|
||||
? `${message.authorName} (${message.author})`
|
||||
: message.author;
|
||||
const lines = [
|
||||
`┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`,
|
||||
];
|
||||
|
||||
if (message.replyTo !== "-") {
|
||||
lines.push(`│ reply-to: msg ${message.replyTo}`);
|
||||
}
|
||||
if (message.mentionLabels.length > 0) {
|
||||
lines.push(`│ mentions: ${message.mentionLabels.join(", ")}`);
|
||||
}
|
||||
|
||||
lines.push("│");
|
||||
|
||||
const bodyLines = message.body.split(/\r?\n/);
|
||||
for (const line of bodyLines) {
|
||||
lines.push(`│ ${line}`);
|
||||
}
|
||||
|
||||
lines.push("│");
|
||||
lines.push("└─");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatTimestamp(input: string): string {
|
||||
const date = new Date(input);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return input;
|
||||
}
|
||||
return date.toISOString().replace("T", " ").replace(".000Z", "Z");
|
||||
}
|
||||
|
||||
export function toChatRoomRow(room: {
|
||||
id: string;
|
||||
name: string;
|
||||
purpose: string | null;
|
||||
messageCount: number;
|
||||
lastMessageAt: string | null;
|
||||
}): ChatRoomRow {
|
||||
return {
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
purpose: room.purpose ?? "-",
|
||||
messages: room.messageCount,
|
||||
lastMessageAt: room.lastMessageAt ?? "-",
|
||||
};
|
||||
}
|
||||
|
||||
export function toChatMessageRow(message: {
|
||||
id: string;
|
||||
authorAgentId: string;
|
||||
createdAt: string;
|
||||
replyToMessageId: string | null;
|
||||
mentionAgentIds: string[];
|
||||
body: string;
|
||||
}): ChatMessageRow {
|
||||
return {
|
||||
id: message.id,
|
||||
author: message.authorAgentId,
|
||||
authorName: null,
|
||||
createdAt: message.createdAt,
|
||||
replyTo: message.replyToMessageId ?? "-",
|
||||
mentionAgentIds: message.mentionAgentIds,
|
||||
mentionLabels: message.mentionAgentIds,
|
||||
body: message.body,
|
||||
};
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import { parseDuration } from "../../utils/duration.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
import type { ChatMessageRow } from "./schema.js";
|
||||
|
||||
export interface ChatCommandOptions extends CommandOptions {
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export async function connectChatClient(host?: string) {
|
||||
const daemonHost = getDaemonHost({ host });
|
||||
try {
|
||||
const client = await connectToDaemon({ host });
|
||||
return { client, daemonHost };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const error: CommandError = {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${daemonHost}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function attachAgentNamesToMessages(
|
||||
client: Awaited<ReturnType<typeof connectToDaemon>>,
|
||||
messages: ChatMessageRow[],
|
||||
options: { timeout?: number; bestEffort?: boolean } = {},
|
||||
): Promise<ChatMessageRow[]> {
|
||||
const agentIds = new Set<string>();
|
||||
for (const message of messages) {
|
||||
agentIds.add(message.author);
|
||||
for (const mentionId of message.mentionAgentIds) {
|
||||
agentIds.add(mentionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (agentIds.size === 0) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
let payload: Awaited<ReturnType<typeof client.fetchAgents>>;
|
||||
try {
|
||||
payload = await client.fetchAgents({
|
||||
filter: { includeArchived: true },
|
||||
...(typeof options.timeout === "number" ? { timeout: options.timeout } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
if (options.bestEffort) {
|
||||
return messages;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const agentNames = new Map<string, string>();
|
||||
for (const entry of payload.entries) {
|
||||
const title = entry.agent.title?.trim();
|
||||
if (title) {
|
||||
agentNames.set(entry.agent.id, title);
|
||||
}
|
||||
}
|
||||
|
||||
return messages.map((message) => ({
|
||||
...message,
|
||||
authorName: agentNames.get(message.author) ?? null,
|
||||
mentionLabels: message.mentionAgentIds.map((agentId) => {
|
||||
const name = agentNames.get(agentId);
|
||||
return name ? `${name} (${agentId})` : agentId;
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
export function toChatCommandError(code: string, action: string, err: unknown): CommandError {
|
||||
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
||||
return err as CommandError;
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const rpcCode =
|
||||
typeof err === "object" && err !== null && "code" in err && typeof err.code === "string"
|
||||
? err.code
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
code: rpcCode ?? code,
|
||||
message: `Failed to ${action}: ${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSinceValue(input?: string): string | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const durationMs = parseDuration(input);
|
||||
return new Date(Date.now() - durationMs).toISOString();
|
||||
} catch {
|
||||
const timestamp = new Date(input);
|
||||
if (Number.isNaN(timestamp.getTime())) {
|
||||
const error: CommandError = {
|
||||
code: "INVALID_SINCE",
|
||||
message: "Invalid --since value",
|
||||
details: "Use a duration like 10m or an ISO timestamp.",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
return timestamp.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTimeoutMs(input?: string): number | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const timeoutMs = parseDuration(input);
|
||||
if (timeoutMs <= 0) {
|
||||
throw new Error("Timeout must be positive");
|
||||
}
|
||||
return timeoutMs;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const error: CommandError = {
|
||||
code: "INVALID_TIMEOUT",
|
||||
message: "Invalid timeout value",
|
||||
details: message,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveChatAuthorAgentId(): string {
|
||||
const agentId = process.env.PASEO_AGENT_ID?.trim();
|
||||
return agentId && agentId.length > 0 ? agentId : "manual";
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import {
|
||||
attachAgentNamesToMessages,
|
||||
connectChatClient,
|
||||
parseTimeoutMs,
|
||||
toChatCommandError,
|
||||
type ChatCommandOptions,
|
||||
} from "./shared.js";
|
||||
import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./schema.js";
|
||||
|
||||
export interface ChatWaitOptions extends ChatCommandOptions {
|
||||
timeout?: string;
|
||||
}
|
||||
|
||||
const CHAT_WAIT_PREFLIGHT_TIMEOUT_MS = 2000;
|
||||
|
||||
export async function runWaitCommand(
|
||||
room: string,
|
||||
options: ChatWaitOptions,
|
||||
_command: Command,
|
||||
): Promise<ListResult<ChatMessageRow>> {
|
||||
const timeoutMs = parseTimeoutMs(options.timeout);
|
||||
const { client } = await connectChatClient(options.host);
|
||||
const deadline = typeof timeoutMs === "number" ? Date.now() + timeoutMs : null;
|
||||
const hasExplicitTimeout = deadline !== null;
|
||||
const remainingTimeoutMs = () =>
|
||||
deadline === null ? undefined : Math.max(1, deadline - Date.now());
|
||||
try {
|
||||
const latest = await client.readChatMessages({
|
||||
room,
|
||||
limit: 1,
|
||||
...(hasExplicitTimeout
|
||||
? {
|
||||
timeout: Math.min(remainingTimeoutMs() ?? 1, CHAT_WAIT_PREFLIGHT_TIMEOUT_MS),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const afterMessageId = latest.messages[0]?.id;
|
||||
const payload = await client.waitForChatMessages({
|
||||
room,
|
||||
afterMessageId,
|
||||
timeoutMs: remainingTimeoutMs() ?? timeoutMs,
|
||||
});
|
||||
const messages = await attachAgentNamesToMessages(
|
||||
client,
|
||||
payload.messages.map(toChatMessageRow),
|
||||
hasExplicitTimeout
|
||||
? {
|
||||
timeout: remainingTimeoutMs(),
|
||||
bestEffort: true,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
return {
|
||||
type: "list",
|
||||
data: messages,
|
||||
schema: chatMessageSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
throw toChatCommandError("CHAT_WAIT_FAILED", "wait for chat messages", err);
|
||||
} finally {
|
||||
await client.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { addJsonAndDaemonHostOptions, addDaemonHostOption } from "../../utils/command-options.js";
|
||||
import { addLoopRunOptions, runLoopRunCommand } from "./run.js";
|
||||
import { addLoopLsOptions, runLoopLsCommand } from "./ls.js";
|
||||
import { addLoopInspectOptions, runLoopInspectCommand } from "./inspect.js";
|
||||
import { addLoopLogsOptions, runLoopLogsCommand } from "./logs.js";
|
||||
import { addLoopStopOptions, runLoopStopCommand } from "./stop.js";
|
||||
|
||||
export function createLoopCommand(): Command {
|
||||
const loop = new Command("loop").description("Run iterative worker loops");
|
||||
|
||||
addJsonAndDaemonHostOptions(addLoopRunOptions(loop.command("run"))).action(
|
||||
withOutput(runLoopRunCommand),
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(addLoopLsOptions(loop.command("ls"))).action(
|
||||
withOutput(runLoopLsCommand),
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(addLoopInspectOptions(loop.command("inspect"))).action(
|
||||
withOutput(runLoopInspectCommand),
|
||||
);
|
||||
|
||||
addDaemonHostOption(addLoopLogsOptions(loop.command("logs"))).action(runLoopLogsCommand);
|
||||
|
||||
addJsonAndDaemonHostOptions(addLoopStopOptions(loop.command("stop"))).action(
|
||||
withOutput(runLoopStopCommand),
|
||||
);
|
||||
|
||||
return loop;
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js";
|
||||
import type { LoopDaemonClient, LoopRecord } from "./types.js";
|
||||
|
||||
interface InspectRow {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface LoopInspectOptions extends CommandOptions {}
|
||||
|
||||
export function addLoopInspectOptions(command: Command): Command {
|
||||
return command.description("Show loop details and iteration history").argument("<id>", "Loop ID");
|
||||
}
|
||||
|
||||
function createInspectSchema(loop: LoopRecord): OutputSchema<InspectRow> {
|
||||
return {
|
||||
idField: "key",
|
||||
columns: [
|
||||
{ header: "KEY", field: "key", width: 18 },
|
||||
{ header: "VALUE", field: "value", width: 80 },
|
||||
],
|
||||
serialize: () => loop,
|
||||
};
|
||||
}
|
||||
|
||||
function toRows(loop: LoopRecord): InspectRow[] {
|
||||
return [
|
||||
{ key: "Id", value: loop.id },
|
||||
{ key: "Name", value: loop.name ?? "null" },
|
||||
{ key: "Status", value: loop.status },
|
||||
{ key: "Cwd", value: loop.cwd },
|
||||
{ key: "Provider", value: loop.provider },
|
||||
{ key: "Model", value: loop.model ?? "null" },
|
||||
{ key: "WorkerProvider", value: loop.workerProvider ?? "null" },
|
||||
{ key: "WorkerModel", value: loop.workerModel ?? "null" },
|
||||
{ key: "VerifierProvider", value: loop.verifierProvider ?? "null" },
|
||||
{ key: "VerifierModel", value: loop.verifierModel ?? "null" },
|
||||
{ key: "Prompt", value: loop.prompt },
|
||||
{ key: "VerifyPrompt", value: loop.verifyPrompt ?? "null" },
|
||||
{
|
||||
key: "VerifyChecks",
|
||||
value: loop.verifyChecks.length > 0 ? loop.verifyChecks.join(" | ") : "[]",
|
||||
},
|
||||
{ key: "Archive", value: String(loop.archive) },
|
||||
{ key: "SleepMs", value: String(loop.sleepMs) },
|
||||
{
|
||||
key: "MaxIterations",
|
||||
value: loop.maxIterations === null ? "null" : String(loop.maxIterations),
|
||||
},
|
||||
{ key: "MaxTimeMs", value: loop.maxTimeMs === null ? "null" : String(loop.maxTimeMs) },
|
||||
{ key: "CreatedAt", value: loop.createdAt },
|
||||
{ key: "UpdatedAt", value: loop.updatedAt },
|
||||
{ key: "CompletedAt", value: loop.completedAt ?? "null" },
|
||||
{
|
||||
key: "Iterations",
|
||||
value:
|
||||
loop.iterations.length === 0
|
||||
? "[]"
|
||||
: loop.iterations
|
||||
.map((iteration) => {
|
||||
const summary = [
|
||||
`#${iteration.index}`,
|
||||
iteration.status,
|
||||
iteration.workerAgentId ? `worker=${iteration.workerAgentId}` : null,
|
||||
iteration.verifierAgentId ? `verifier=${iteration.verifierAgentId}` : null,
|
||||
iteration.failureReason ? `reason=${iteration.failureReason}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return summary;
|
||||
})
|
||||
.join(" | "),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export type LoopInspectResult = ListResult<InspectRow>;
|
||||
|
||||
export async function runLoopInspectCommand(
|
||||
id: string,
|
||||
options: LoopInspectOptions,
|
||||
_command: Command,
|
||||
): Promise<LoopInspectResult> {
|
||||
const host = getDaemonHost({ host: options.host });
|
||||
let client;
|
||||
try {
|
||||
client = (await connectToDaemon({
|
||||
host: options.host,
|
||||
})) as unknown as LoopDaemonClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${host}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await client.loopInspect(id);
|
||||
await client.close();
|
||||
if (payload.error || !payload.loop) {
|
||||
throw new Error(payload.error ?? `Loop not found: ${id}`);
|
||||
}
|
||||
return {
|
||||
type: "list",
|
||||
data: toRows(payload.loop),
|
||||
schema: createInspectSchema(payload.loop),
|
||||
};
|
||||
} catch (error) {
|
||||
await client.close().catch(() => {});
|
||||
throw {
|
||||
code: "LOOP_INSPECT_FAILED",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
} satisfies CommandError;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
import type { LoopDaemonClient, LoopLogEntry } from "./types.js";
|
||||
|
||||
export interface LoopLogsOptions extends CommandOptions {
|
||||
pollInterval?: string;
|
||||
}
|
||||
|
||||
export function addLoopLogsOptions(command: Command): Command {
|
||||
return command
|
||||
.description("Stream loop logs")
|
||||
.argument("<id>", "Loop ID")
|
||||
.option("--poll-interval <ms>", "Polling interval in milliseconds", "1000");
|
||||
}
|
||||
|
||||
function parsePollInterval(value: string): number {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
throw {
|
||||
code: "INVALID_POLL_INTERVAL",
|
||||
message: "--poll-interval must be a positive integer",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function renderLogEntry(entry: LoopLogEntry): string {
|
||||
const prefix = [
|
||||
entry.timestamp,
|
||||
entry.source,
|
||||
entry.iteration === null ? null : `iteration=${entry.iteration}`,
|
||||
entry.level === "error" ? "ERROR" : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return `${prefix}\n${entry.text}`;
|
||||
}
|
||||
|
||||
export async function runLoopLogsCommand(
|
||||
id: string,
|
||||
options: LoopLogsOptions,
|
||||
_command: Command,
|
||||
): Promise<void> {
|
||||
const host = getDaemonHost({ host: options.host });
|
||||
const pollInterval = parsePollInterval(options.pollInterval ?? "1000");
|
||||
let client: LoopDaemonClient;
|
||||
try {
|
||||
client = (await connectToDaemon({
|
||||
host: options.host,
|
||||
})) as unknown as LoopDaemonClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`);
|
||||
console.error("Start the daemon with: paseo daemon start");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function streamLogs(cursor: number): Promise<void> {
|
||||
const payload = await client.loopLogs(id, cursor);
|
||||
if (payload.error || !payload.loop) {
|
||||
throw new Error(payload.error ?? `Loop not found: ${id}`);
|
||||
}
|
||||
for (const entry of payload.entries) {
|
||||
console.log(renderLogEntry(entry));
|
||||
}
|
||||
if (payload.loop.status !== "running") {
|
||||
await client.close();
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
return streamLogs(payload.nextCursor);
|
||||
}
|
||||
|
||||
try {
|
||||
await streamLogs(0);
|
||||
} catch (error) {
|
||||
await client.close().catch(() => {});
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Error: Failed to stream loop logs: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js";
|
||||
import type { LoopDaemonClient, LoopListItem } from "./types.js";
|
||||
|
||||
interface LoopListRow {
|
||||
id: string;
|
||||
name: string | null;
|
||||
status: string;
|
||||
cwd: string;
|
||||
updated: string;
|
||||
activeIteration: string;
|
||||
}
|
||||
|
||||
export interface LoopLsOptions extends CommandOptions {}
|
||||
|
||||
export const loopLsSchema: OutputSchema<LoopListRow> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "LOOP ID", field: "id", width: 10 },
|
||||
{ header: "NAME", field: "name", width: 20 },
|
||||
{ header: "STATUS", field: "status", width: 10 },
|
||||
{ header: "ITER", field: "activeIteration", width: 8 },
|
||||
{ header: "CWD", field: "cwd", width: 40 },
|
||||
{ header: "UPDATED", field: "updated", width: 24 },
|
||||
],
|
||||
};
|
||||
|
||||
export function addLoopLsOptions(command: Command): Command {
|
||||
return command.description("List loops");
|
||||
}
|
||||
|
||||
function toRow(loop: LoopListItem): LoopListRow {
|
||||
return {
|
||||
id: loop.id,
|
||||
name: loop.name,
|
||||
status: loop.status,
|
||||
cwd: loop.cwd,
|
||||
updated: loop.updatedAt,
|
||||
activeIteration: loop.activeIteration === null ? "-" : String(loop.activeIteration),
|
||||
};
|
||||
}
|
||||
|
||||
export type LoopLsResult = ListResult<LoopListRow>;
|
||||
|
||||
export async function runLoopLsCommand(
|
||||
options: LoopLsOptions,
|
||||
_command: Command,
|
||||
): Promise<LoopLsResult> {
|
||||
const host = getDaemonHost({ host: options.host });
|
||||
let client;
|
||||
try {
|
||||
client = (await connectToDaemon({
|
||||
host: options.host,
|
||||
})) as unknown as LoopDaemonClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${host}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await client.loopList();
|
||||
await client.close();
|
||||
if (payload.error) {
|
||||
throw new Error(payload.error);
|
||||
}
|
||||
return {
|
||||
type: "list",
|
||||
data: payload.loops.map(toRow),
|
||||
schema: loopLsSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
await client.close().catch(() => {});
|
||||
throw {
|
||||
code: "LOOP_LIST_FAILED",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
} satisfies CommandError;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildLoopRunInput } from "./run.js";
|
||||
import type { LoopRunOptions } from "./types.js";
|
||||
|
||||
describe("buildLoopRunInput", () => {
|
||||
it("should parse provider/model from provider string", () => {
|
||||
const options: LoopRunOptions = {
|
||||
provider: "opencode/hy3-preview-free",
|
||||
model: undefined,
|
||||
verifyProvider: undefined,
|
||||
verifyModel: undefined,
|
||||
verify: undefined,
|
||||
verifyCheck: [],
|
||||
archive: false,
|
||||
name: undefined,
|
||||
sleep: undefined,
|
||||
maxIterations: undefined,
|
||||
maxTime: undefined,
|
||||
} as LoopRunOptions;
|
||||
|
||||
const result = buildLoopRunInput("test prompt", options);
|
||||
|
||||
expect(result.provider).toBe("opencode");
|
||||
expect(result.model).toBe("hy3-preview-free");
|
||||
});
|
||||
|
||||
it("should use provider only when no model in string", () => {
|
||||
const options = {
|
||||
provider: "opencode",
|
||||
model: undefined,
|
||||
verifyProvider: undefined,
|
||||
verifyModel: undefined,
|
||||
verify: undefined,
|
||||
verifyCheck: [],
|
||||
archive: false,
|
||||
name: undefined,
|
||||
sleep: undefined,
|
||||
maxIterations: undefined,
|
||||
maxTime: undefined,
|
||||
} as LoopRunOptions;
|
||||
|
||||
const result = buildLoopRunInput("test prompt", options);
|
||||
|
||||
expect(result.provider).toBe("opencode");
|
||||
expect(result.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should pass verifierProvider with model parsing", () => {
|
||||
const options = {
|
||||
provider: undefined,
|
||||
model: undefined,
|
||||
verifyProvider: "codex/gpt-5.4",
|
||||
verifyModel: undefined,
|
||||
verify: undefined,
|
||||
verifyCheck: [],
|
||||
archive: false,
|
||||
name: undefined,
|
||||
sleep: undefined,
|
||||
maxIterations: undefined,
|
||||
maxTime: undefined,
|
||||
} as LoopRunOptions;
|
||||
|
||||
const result = buildLoopRunInput("test prompt", options);
|
||||
|
||||
expect(result.verifierProvider).toBe("codex");
|
||||
expect(result.verifierModel).toBe("gpt-5.4");
|
||||
});
|
||||
|
||||
it("should prefer explicit model over parsed model", () => {
|
||||
const options = {
|
||||
provider: "opencode/hy3-preview-free",
|
||||
model: "other-model",
|
||||
verifyProvider: undefined,
|
||||
verifyModel: undefined,
|
||||
verify: undefined,
|
||||
verifyCheck: [],
|
||||
archive: false,
|
||||
name: undefined,
|
||||
sleep: undefined,
|
||||
maxIterations: undefined,
|
||||
maxTime: undefined,
|
||||
} as LoopRunOptions;
|
||||
|
||||
const result = buildLoopRunInput("test prompt", options);
|
||||
|
||||
expect(result.provider).toBe("opencode");
|
||||
expect(result.model).toBe("other-model");
|
||||
});
|
||||
});
|
||||
@@ -1,208 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type {
|
||||
CommandOptions,
|
||||
CommandError,
|
||||
OutputSchema,
|
||||
SingleResult,
|
||||
} from "../../output/index.js";
|
||||
import { collectMultiple } from "../../utils/command-options.js";
|
||||
import { parseDuration } from "../../utils/duration.js";
|
||||
import { resolveProviderAndModel } from "../../utils/provider-model.js";
|
||||
import type { LoopDaemonClient, LoopRecord, LoopRunInput } from "./types.js";
|
||||
|
||||
export interface LoopRunRow {
|
||||
id: string;
|
||||
status: string;
|
||||
name: string | null;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export interface LoopRunOptions extends CommandOptions {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
mode?: string;
|
||||
verifyProvider?: string;
|
||||
verifyModel?: string;
|
||||
verifyMode?: string;
|
||||
verify?: string;
|
||||
verifyCheck?: string[];
|
||||
archive?: boolean;
|
||||
name?: string;
|
||||
sleep?: string;
|
||||
maxIterations?: string;
|
||||
maxTime?: string;
|
||||
}
|
||||
|
||||
export const loopRunSchema: OutputSchema<LoopRunRow> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "LOOP ID", field: "id", width: 10 },
|
||||
{ header: "STATUS", field: "status", width: 10 },
|
||||
{ header: "NAME", field: "name", width: 20 },
|
||||
{ header: "CWD", field: "cwd", width: 40 },
|
||||
],
|
||||
};
|
||||
|
||||
export function addLoopRunOptions(command: Command): Command {
|
||||
return command
|
||||
.description("Start a loop")
|
||||
.argument("<prompt>", "Prompt for each fresh worker iteration")
|
||||
.option("--provider <provider>", "Default provider for worker and verifier agents")
|
||||
.option("--model <model>", "Default model for worker and verifier agents")
|
||||
.option(
|
||||
"--mode <mode>",
|
||||
"Provider-specific mode for the worker agent (e.g. claude bypassPermissions, opencode build)",
|
||||
)
|
||||
.option("--verify-provider <provider>", "Provider for the verifier agent")
|
||||
.option("--verify-model <model>", "Model for the verifier agent")
|
||||
.option("--verify-mode <mode>", "Provider-specific mode for the verifier agent")
|
||||
.option("--verify <prompt>", "Verifier agent prompt")
|
||||
.option(
|
||||
"--verify-check <command>",
|
||||
"Shell command that must exit 0 (repeatable)",
|
||||
collectMultiple,
|
||||
[],
|
||||
)
|
||||
.option("--archive", "Archive worker and verifier agents after each iteration")
|
||||
.option("--name <name>", "Optional loop name")
|
||||
.option("--sleep <duration>", "Delay between iterations (for example: 30s, 5m)")
|
||||
.option("--max-iterations <n>", "Maximum number of iterations")
|
||||
.option("--max-time <duration>", "Maximum total runtime (for example: 1h, 30m)");
|
||||
}
|
||||
|
||||
function toRow(loop: LoopRecord): LoopRunRow {
|
||||
return {
|
||||
id: loop.id,
|
||||
status: loop.status,
|
||||
name: loop.name,
|
||||
cwd: loop.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
function parseMaxIterations(value: string | undefined): number | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
throw {
|
||||
code: "INVALID_MAX_ITERATIONS",
|
||||
message: "--max-iterations must be a positive integer",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// oxlint-disable complexity
|
||||
export function buildLoopRunInput(prompt: string, options: LoopRunOptions): LoopRunInput {
|
||||
const verifyPrompt = options.verify?.trim();
|
||||
if (options.verify !== undefined && !verifyPrompt) {
|
||||
throw {
|
||||
code: "INVALID_VERIFY_PROMPT",
|
||||
message: "--verify cannot be empty",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
|
||||
const result: LoopRunInput = {
|
||||
prompt,
|
||||
cwd: process.cwd(),
|
||||
};
|
||||
|
||||
// Resolve provider/model
|
||||
if (options.provider) {
|
||||
const { provider, model } = resolveProviderAndModel({ provider: options.provider });
|
||||
if (provider) result.provider = provider;
|
||||
// Explicit --model takes precedence over parsed model
|
||||
if (options.model?.trim()) {
|
||||
result.model = options.model.trim();
|
||||
} else if (model) {
|
||||
result.model = model;
|
||||
}
|
||||
} else if (options.model?.trim()) {
|
||||
result.model = options.model.trim();
|
||||
}
|
||||
|
||||
if (options.mode?.trim()) {
|
||||
result.modeId = options.mode.trim();
|
||||
}
|
||||
|
||||
// Resolve verifier provider/model
|
||||
if (options.verifyProvider) {
|
||||
const { provider, model } = resolveProviderAndModel({ provider: options.verifyProvider });
|
||||
if (provider) result.verifierProvider = provider;
|
||||
// Explicit --verify-model takes precedence over parsed model
|
||||
if (options.verifyModel?.trim()) {
|
||||
result.verifierModel = options.verifyModel.trim();
|
||||
} else if (model) {
|
||||
result.verifierModel = model;
|
||||
}
|
||||
} else if (options.verifyModel?.trim()) {
|
||||
result.verifierModel = options.verifyModel.trim();
|
||||
}
|
||||
|
||||
if (options.verifyMode?.trim()) {
|
||||
result.verifierModeId = options.verifyMode.trim();
|
||||
}
|
||||
|
||||
if (verifyPrompt) result.verifyPrompt = verifyPrompt;
|
||||
if (options.verifyCheck && options.verifyCheck.length > 0) {
|
||||
result.verifyChecks = options.verifyCheck;
|
||||
}
|
||||
if (options.archive) result.archive = true;
|
||||
if (options.name?.trim()) result.name = options.name.trim();
|
||||
if (options.sleep) result.sleepMs = parseDuration(options.sleep);
|
||||
if (options.maxIterations) {
|
||||
result.maxIterations = parseMaxIterations(options.maxIterations);
|
||||
}
|
||||
if (options.maxTime) result.maxTimeMs = parseDuration(options.maxTime);
|
||||
|
||||
return result;
|
||||
}
|
||||
// oxlint-enable complexity
|
||||
|
||||
export type LoopRunResult = SingleResult<LoopRunRow>;
|
||||
|
||||
export async function runLoopRunCommand(
|
||||
prompt: string,
|
||||
options: LoopRunOptions,
|
||||
_command: Command,
|
||||
): Promise<LoopRunResult> {
|
||||
const host = getDaemonHost({ host: options.host });
|
||||
const input = buildLoopRunInput(prompt, options);
|
||||
let client;
|
||||
try {
|
||||
client = (await connectToDaemon({
|
||||
host: options.host,
|
||||
})) as unknown as LoopDaemonClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${host}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await client.loopRun(input);
|
||||
await client.close();
|
||||
if (payload.error || !payload.loop) {
|
||||
throw new Error(payload.error ?? "Loop creation failed");
|
||||
}
|
||||
return {
|
||||
type: "single",
|
||||
data: toRow(payload.loop),
|
||||
schema: loopRunSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
await client.close().catch(() => {});
|
||||
if (error && typeof error === "object" && "code" in error) {
|
||||
throw error;
|
||||
}
|
||||
throw {
|
||||
code: "LOOP_RUN_FAILED",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
} satisfies CommandError;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Command } from "commander";
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import type {
|
||||
CommandOptions,
|
||||
CommandError,
|
||||
OutputSchema,
|
||||
SingleResult,
|
||||
} from "../../output/index.js";
|
||||
import type { LoopDaemonClient, LoopRecord } from "./types.js";
|
||||
|
||||
interface LoopStopRow {
|
||||
id: string;
|
||||
status: string;
|
||||
activeIteration: string;
|
||||
}
|
||||
|
||||
export interface LoopStopOptions extends CommandOptions {}
|
||||
|
||||
export const loopStopSchema: OutputSchema<LoopStopRow> = {
|
||||
idField: "id",
|
||||
columns: [
|
||||
{ header: "LOOP ID", field: "id", width: 10 },
|
||||
{ header: "STATUS", field: "status", width: 10 },
|
||||
{ header: "ITER", field: "activeIteration", width: 8 },
|
||||
],
|
||||
};
|
||||
|
||||
export function addLoopStopOptions(command: Command): Command {
|
||||
return command.description("Stop a running loop").argument("<id>", "Loop ID");
|
||||
}
|
||||
|
||||
function toRow(loop: LoopRecord): LoopStopRow {
|
||||
return {
|
||||
id: loop.id,
|
||||
status: loop.status,
|
||||
activeIteration: loop.activeIteration === null ? "-" : String(loop.activeIteration),
|
||||
};
|
||||
}
|
||||
|
||||
export type LoopStopResult = SingleResult<LoopStopRow>;
|
||||
|
||||
export async function runLoopStopCommand(
|
||||
id: string,
|
||||
options: LoopStopOptions,
|
||||
_command: Command,
|
||||
): Promise<LoopStopResult> {
|
||||
const host = getDaemonHost({ host: options.host });
|
||||
let client;
|
||||
try {
|
||||
client = (await connectToDaemon({
|
||||
host: options.host,
|
||||
})) as unknown as LoopDaemonClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw {
|
||||
code: "DAEMON_NOT_RUNNING",
|
||||
message: `Cannot connect to daemon at ${host}: ${message}`,
|
||||
details: "Start the daemon with: paseo daemon start",
|
||||
} satisfies CommandError;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await client.loopStop(id);
|
||||
await client.close();
|
||||
if (payload.error || !payload.loop) {
|
||||
throw new Error(payload.error ?? `Loop not found: ${id}`);
|
||||
}
|
||||
return {
|
||||
type: "single",
|
||||
data: toRow(payload.loop),
|
||||
schema: loopStopSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
await client.close().catch(() => {});
|
||||
throw {
|
||||
code: "LOOP_STOP_FAILED",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
} satisfies CommandError;
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
export type LoopStatus = "running" | "succeeded" | "failed" | "stopped";
|
||||
|
||||
export interface LoopLogEntry {
|
||||
seq: number;
|
||||
timestamp: string;
|
||||
iteration: number | null;
|
||||
source: "loop" | "worker" | "verifier" | "verify-check";
|
||||
level: "info" | "error";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface LoopVerifyCheckResult {
|
||||
command: string;
|
||||
exitCode: number;
|
||||
passed: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
startedAt: string;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface LoopVerifyPromptResult {
|
||||
passed: boolean;
|
||||
reason: string;
|
||||
verifierAgentId: string | null;
|
||||
startedAt: string;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface LoopIterationRecord {
|
||||
index: number;
|
||||
workerAgentId: string | null;
|
||||
workerStartedAt: string;
|
||||
workerCompletedAt: string | null;
|
||||
verifierAgentId: string | null;
|
||||
status: "running" | "succeeded" | "failed" | "stopped";
|
||||
workerOutcome: "completed" | "failed" | "canceled" | null;
|
||||
failureReason: string | null;
|
||||
verifyChecks: LoopVerifyCheckResult[];
|
||||
verifyPrompt: LoopVerifyPromptResult | null;
|
||||
}
|
||||
|
||||
export interface LoopRecord {
|
||||
id: string;
|
||||
name: string | null;
|
||||
prompt: string;
|
||||
cwd: string;
|
||||
provider: string;
|
||||
model: string | null;
|
||||
modeId: string | null;
|
||||
workerProvider: string | null;
|
||||
workerModel: string | null;
|
||||
verifierProvider: string | null;
|
||||
verifierModel: string | null;
|
||||
verifierModeId: string | null;
|
||||
verifyPrompt: string | null;
|
||||
verifyChecks: string[];
|
||||
archive: boolean;
|
||||
sleepMs: number;
|
||||
maxIterations: number | null;
|
||||
maxTimeMs: number | null;
|
||||
status: LoopStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
startedAt: string;
|
||||
completedAt: string | null;
|
||||
stopRequestedAt: string | null;
|
||||
iterations: LoopIterationRecord[];
|
||||
logs: LoopLogEntry[];
|
||||
nextLogSeq: number;
|
||||
activeIteration: number | null;
|
||||
activeWorkerAgentId: string | null;
|
||||
activeVerifierAgentId: string | null;
|
||||
}
|
||||
|
||||
export interface LoopListItem {
|
||||
id: string;
|
||||
name: string | null;
|
||||
status: LoopStatus;
|
||||
cwd: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
activeIteration: number | null;
|
||||
}
|
||||
|
||||
export interface LoopLogsResult {
|
||||
loop: LoopRecord;
|
||||
entries: LoopLogEntry[];
|
||||
nextCursor: number;
|
||||
}
|
||||
|
||||
export interface LoopRunPayload {
|
||||
requestId: string;
|
||||
loop: LoopRecord | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface LoopListPayload {
|
||||
requestId: string;
|
||||
loops: LoopListItem[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface LoopInspectPayload {
|
||||
requestId: string;
|
||||
loop: LoopRecord | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface LoopLogsPayload {
|
||||
requestId: string;
|
||||
loop: LoopRecord | null;
|
||||
entries: LoopLogEntry[];
|
||||
nextCursor: number;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface LoopStopPayload {
|
||||
requestId: string;
|
||||
loop: LoopRecord | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface LoopRunInput {
|
||||
prompt: string;
|
||||
cwd: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
modeId?: string;
|
||||
workerProvider?: string;
|
||||
workerModel?: string;
|
||||
verifierProvider?: string;
|
||||
verifierModel?: string;
|
||||
verifierModeId?: string;
|
||||
verifyPrompt?: string;
|
||||
verifyChecks?: string[];
|
||||
archive?: boolean;
|
||||
name?: string;
|
||||
sleepMs?: number;
|
||||
maxIterations?: number;
|
||||
maxTimeMs?: number;
|
||||
}
|
||||
|
||||
export interface LoopDaemonClient {
|
||||
loopRun(input: LoopRunInput): Promise<LoopRunPayload>;
|
||||
loopList(): Promise<LoopListPayload>;
|
||||
loopInspect(id: string): Promise<LoopInspectPayload>;
|
||||
loopLogs(id: string, afterSeq?: number): Promise<LoopLogsPayload>;
|
||||
loopStop(id: string): Promise<LoopStopPayload>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
Reference in New Issue
Block a user