Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 312 additions & 0 deletions landing/index.html.bak

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ try {
import { installWorkflow } from "../installer/install.js";
import { uninstallAllWorkflows, uninstallWorkflow, checkActiveRuns } from "../installer/uninstall.js";
import { getWorkflowStatus, listRuns, stopWorkflow } from "../installer/status.js";
import { runWorkflow } from "../installer/run.js";
import { runWorkflow, dryRunWorkflow } from "../installer/run.js";
import { listBundledWorkflows } from "../installer/workflow-fetch.js";
import { readRecentLogs } from "../lib/logger.js";
import { getRecentEvents, getRunEvents, type AntfarmEvent } from "../installer/events.js";
Expand Down Expand Up @@ -93,7 +93,7 @@ function printUsage() {
"antfarm workflow install <name> Install a workflow",
"antfarm workflow uninstall <name> Uninstall a workflow (blocked if runs active)",
"antfarm workflow uninstall --all Uninstall all workflows (--force to override)",
"antfarm workflow run <name> <task> Start a workflow run",
"antfarm workflow run <name> <task> Start a workflow run (--dry-run to validate only)",
"antfarm workflow status <query> Check run status (task substring, run ID prefix)",
"antfarm workflow runs List all workflow runs",
"antfarm workflow resume <run-id> Resume a failed run from where it left off",
Expand Down Expand Up @@ -671,13 +671,23 @@ async function main() {

if (action === "run") {
let notifyUrl: string | undefined;
let dryRun = false;
const runArgs = args.slice(3);
const nuIdx = runArgs.indexOf("--notify-url");
if (nuIdx !== -1) {
notifyUrl = runArgs[nuIdx + 1];
runArgs.splice(nuIdx, 2);
}
const drIdx = runArgs.indexOf("--dry-run");
if (drIdx !== -1) {
dryRun = true;
runArgs.splice(drIdx, 1);
}
const taskTitle = runArgs.join(" ").trim();
if (dryRun) {
await dryRunWorkflow({ workflowId: target, taskTitle });
return;
}
if (!taskTitle) { process.stderr.write("Missing task title.\n"); printUsage(); process.exit(1); }
const run = await runWorkflow({ workflowId: target, taskTitle, notifyUrl });
process.stdout.write(
Expand Down
53 changes: 10 additions & 43 deletions src/installer/agent-cron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createAgentCronJob, deleteAgentCronJobs, listCronJobs, checkCronToolAva
import type { WorkflowSpec } from "./types.js";
import { resolveAntfarmCli } from "./paths.js";
import { getDb } from "../db.js";
import { readOpenClawConfig } from "./openclaw-config.js";

const DEFAULT_EVERY_MS = 300_000; // 5 minutes
const DEFAULT_AGENT_TIMEOUT_SECONDS = 30 * 60; // 30 minutes
Expand Down Expand Up @@ -89,46 +88,13 @@ The workflow cannot advance until you report. Your session ending without report
}

const DEFAULT_POLLING_TIMEOUT_SECONDS = 120;
const DEFAULT_POLLING_MODEL = "default";

function extractModel(value: unknown): string | undefined {
if (!value) return undefined;
if (typeof value === "string") return value;
if (typeof value === "object" && value !== null) {
const primary = (value as { primary?: unknown }).primary;
if (typeof primary === "string") return primary;
}
return undefined;
}

async function resolveAgentCronModel(agentId: string, requestedModel?: string): Promise<string | undefined> {
if (requestedModel && requestedModel !== "default") {
return requestedModel;
}

try {
const { config } = await readOpenClawConfig();
const agents = config.agents?.list;
if (Array.isArray(agents)) {
const entry = agents.find((a: any) => a?.id === agentId);
const configured = extractModel(entry?.model);
if (configured) return configured;
}

const defaults = config.agents?.defaults;
const fallback = extractModel(defaults?.model);
if (fallback) return fallback;
} catch {
// best-effort — fallback below
}

return requestedModel;
}
const DEFAULT_POLLING_MODEL = "minimax/MiniMax-M2.5";

export function buildPollingPrompt(workflowId: string, agentId: string, workModel?: string): string {
export function buildPollingPrompt(workflowId: string, agentId: string, workModel?: string, workTimeoutSeconds?: number): string {
const fullAgentId = `${workflowId}_${agentId}`;
const cli = resolveAntfarmCli();
const model = workModel ?? "default";
const model = workModel ?? "minimax/MiniMax-M2.5";
const timeoutSec = workTimeoutSeconds ?? DEFAULT_AGENT_TIMEOUT_SECONDS;
const workPrompt = buildWorkPrompt(workflowId, agentId);

return `Step 1 — Quick check for pending work (lightweight, no side effects):
Expand All @@ -147,6 +113,7 @@ If JSON is returned, parse it to extract stepId, runId, and input fields.
Then call sessions_spawn with these parameters:
- agentId: "${fullAgentId}"
- model: "${model}"
- runTimeoutSeconds: ${timeoutSec}
- task: The full work prompt below, followed by "\\n\\nCLAIMED STEP JSON:\\n" and the exact JSON output from step claim.

Full work prompt to include in the spawned task:
Expand All @@ -173,11 +140,11 @@ export async function setupAgentCrons(workflow: WorkflowSpec): Promise<void> {
const agentId = `${workflow.id}_${agent.id}`;

// Two-phase: Phase 1 uses cheap polling model + minimal prompt
const requestedPollingModel = agent.pollingModel ?? workflowPollingModel;
const pollingModel = await resolveAgentCronModel(agentId, requestedPollingModel);
const requestedWorkModel = agent.model ?? workflowPollingModel;
const workModel = await resolveAgentCronModel(agentId, requestedWorkModel);
const prompt = buildPollingPrompt(workflow.id, agent.id, workModel);
const pollingModel = agent.pollingModel ?? workflowPollingModel;
const workModel = agent.model; // Phase 2 model (passed to sessions_spawn via prompt)
// Work agent timeout: per-agent > workflow default > library default (30 min)
const workTimeoutSeconds = agent.timeoutSeconds ?? DEFAULT_AGENT_TIMEOUT_SECONDS;
const prompt = buildPollingPrompt(workflow.id, agent.id, workModel, workTimeoutSeconds);
const timeoutSeconds = workflowPollingTimeout;

const result = await createAgentCronJob({
Expand Down
121 changes: 118 additions & 3 deletions src/installer/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,121 @@ import { getDb, nextRunNumber } from "../db.js";
import { logger } from "../lib/logger.js";
import { ensureWorkflowCrons } from "./agent-cron.js";
import { emitEvent } from "./events.js";
import { resolveTemplate } from "./step-ops.js";

export interface DryRunResult {
workflowId: string;
workflowName: string;
task: string;
steps: DryRunStep[];
context: Record<string, string>;
}

export interface DryRunStep {
stepIndex: number;
stepId: string;
agentId: string;
type: "single" | "loop";
inputTemplate: string;
resolvedInput: string;
expects: string;
status: string;
}

export async function dryRunWorkflow(params: {
workflowId: string;
taskTitle: string;
}): Promise<DryRunResult> {
// 1. Validate workflow YAML
const workflowDir = resolveWorkflowDir(params.workflowId);
const workflow = await loadWorkflowSpec(workflowDir);

// 2. Build execution context with placeholder values
const placeholderContext: Record<string, string> = {
task: params.taskTitle,
run_id: "dry-run-00000000-0000-0000-0000-000000000000",
run_number: "0",
...workflow.context,
};

// Add placeholder values for any workflow context variables not provided
if (workflow.context) {
for (const [key, value] of Object.entries(workflow.context)) {
placeholderContext[key] = value;
}
}

// 3. Resolve all step input templates
const steps: DryRunStep[] = [];
for (let i = 0; i < workflow.steps.length; i++) {
const step = workflow.steps[i];
const agentId = workflow.id + "_" + step.agent;
const stepType = step.type ?? "single";

// Resolve the input template against our context
const resolvedInput = resolveTemplate(step.input, placeholderContext);

steps.push({
stepIndex: i,
stepId: step.id,
agentId,
type: stepType,
inputTemplate: step.input,
resolvedInput,
expects: step.expects,
status: i === 0 ? "pending" : "waiting",
});
}

// 4. Print execution plan
console.log("");
console.log("═══════════════════════════════════════════════════════════════");
console.log(" DRY-RUN EXECUTION PLAN");
console.log("═══════════════════════════════════════════════════════════════");
console.log("");
console.log("Workflow: " + (workflow.name ?? workflow.id) + " (" + workflow.id + ")");
console.log("Task: " + params.taskTitle);
console.log("Steps: " + steps.length);
console.log("");

console.log("─────────────────────────────────────────────────────────────────");
console.log("CONTEXT (placeholder values):");
console.log("─────────────────────────────────────────────────────────────────");
for (const [key, value] of Object.entries(placeholderContext)) {
console.log(" {{" + key + "}}: " + value);
}
console.log("");

console.log("─────────────────────────────────────────────────────────────────");
console.log("EXECUTION ORDER:");
console.log("─────────────────────────────────────────────────────────────────");
for (const step of steps) {
const statusIcon = step.status === "pending" ? "→" : "…";
const typeLabel = step.type === "loop" ? " [LOOP]" : "";
console.log(statusIcon + " Step " + (step.stepIndex + 1) + ": " + step.stepId + typeLabel);
console.log(" Agent: " + step.agentId);
const inputPreview = step.resolvedInput.slice(0, 100);
const inputSuffix = step.resolvedInput.length > 100 ? "..." : "";
console.log(" Input: " + inputPreview + inputSuffix);
console.log(" Expects: " + step.expects);
console.log("");
}

console.log("═══════════════════════════════════════════════════════════════");
console.log(" VALIDATION PASSED");
console.log("═══════════════════════════════════════════════════════════════");
console.log("Workflow YAML is valid. All templates resolved.");
console.log("No database entries created. No agents spawned.");
console.log("");

return {
workflowId: workflow.id,
workflowName: workflow.name ?? workflow.id,
task: params.taskTitle,
steps,
context: placeholderContext,
};
}

export async function runWorkflow(params: {
workflowId: string;
Expand Down Expand Up @@ -38,7 +153,7 @@ export async function runWorkflow(params: {
for (let i = 0; i < workflow.steps.length; i++) {
const step = workflow.steps[i];
const stepUuid = crypto.randomUUID();
const agentId = `${workflow.id}_${step.agent}`;
const agentId = workflow.id + "_" + step.agent;
const status = i === 0 ? "pending" : "waiting";
const maxRetries = step.max_retries ?? step.on_fail?.max_retries ?? 2;
const stepType = step.type ?? "single";
Expand All @@ -60,12 +175,12 @@ export async function runWorkflow(params: {
const db2 = getDb();
db2.prepare("UPDATE runs SET status = 'failed', updated_at = ? WHERE id = ?").run(new Date().toISOString(), runId);
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Cannot start workflow run: cron setup failed. ${message}`);
throw new Error("Cannot start workflow run: cron setup failed. " + message);
}

emitEvent({ ts: new Date().toISOString(), event: "run.started", runId, workflowId: workflow.id });

logger.info(`Run started: "${params.taskTitle}"`, {
logger.info("Run started: \"" + params.taskTitle + "\"", {
workflowId: workflow.id,
runId,
stepId: workflow.steps[0]?.id,
Expand Down
Loading