135 lines
3.5 KiB
TypeScript
135 lines
3.5 KiB
TypeScript
/**
|
|
* Workflow orchestration engine for DroidClaw.
|
|
*
|
|
* Executes a sequence of sub-goals, each optionally scoped to a specific app.
|
|
* This is DroidClaw's equivalent of `analyze_and_act(sub_goal, app)`.
|
|
*
|
|
* Usage:
|
|
* bun run src/kernel.ts --workflow examples/workflows/productivity/logistics-workflow.json
|
|
*/
|
|
|
|
import { runAgent } from "./kernel.js";
|
|
import { runAdbCommand } from "./actions.js";
|
|
|
|
// ===========================================
|
|
// Types
|
|
// ===========================================
|
|
|
|
export interface WorkflowStep {
|
|
goal: string;
|
|
app?: string;
|
|
maxSteps?: number;
|
|
formData?: Record<string, string>;
|
|
}
|
|
|
|
export interface Workflow {
|
|
name: string;
|
|
steps: WorkflowStep[];
|
|
}
|
|
|
|
export interface StepResult {
|
|
goal: string;
|
|
app?: string;
|
|
success: boolean;
|
|
stepsUsed: number;
|
|
error?: string;
|
|
}
|
|
|
|
export interface WorkflowResult {
|
|
name: string;
|
|
steps: StepResult[];
|
|
success: boolean;
|
|
}
|
|
|
|
// ===========================================
|
|
// Workflow Engine
|
|
// ===========================================
|
|
|
|
const DEFAULT_STEP_LIMIT = 15;
|
|
const APP_LAUNCH_DELAY_MS = 2000;
|
|
|
|
/**
|
|
* Builds the effective goal string for a workflow step.
|
|
* Appends structured form data if present.
|
|
*/
|
|
function buildGoal(step: WorkflowStep): string {
|
|
let goal = step.goal;
|
|
|
|
if (step.formData && Object.keys(step.formData).length > 0) {
|
|
const lines = Object.entries(step.formData)
|
|
.map(([key, value]) => `- ${key}: ${value}`)
|
|
.join("\n");
|
|
goal += `\n\nFORM DATA TO FILL:\n${lines}\n\nFind each field on screen and enter the corresponding value.`;
|
|
}
|
|
|
|
return goal;
|
|
}
|
|
|
|
/**
|
|
* Switches to the specified app by launching it via monkey.
|
|
*/
|
|
function switchToApp(packageName: string): void {
|
|
console.log(`Switching to app: ${packageName}`);
|
|
runAdbCommand([
|
|
"shell", "monkey", "-p", packageName,
|
|
"-c", "android.intent.category.LAUNCHER", "1",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Executes a full workflow: a sequence of sub-goals with optional app switching.
|
|
*/
|
|
export async function runWorkflow(workflow: Workflow): Promise<WorkflowResult> {
|
|
console.log(`\n========================================`);
|
|
console.log(`Workflow: ${workflow.name}`);
|
|
console.log(`Steps: ${workflow.steps.length}`);
|
|
console.log(`========================================`);
|
|
|
|
const results: StepResult[] = [];
|
|
|
|
for (let i = 0; i < workflow.steps.length; i++) {
|
|
const step = workflow.steps[i];
|
|
const total = workflow.steps.length;
|
|
|
|
console.log(`\n--- Step ${i + 1}/${total}: ${step.goal} ---`);
|
|
|
|
// Switch to target app if specified
|
|
if (step.app) {
|
|
switchToApp(step.app);
|
|
await Bun.sleep(APP_LAUNCH_DELAY_MS);
|
|
}
|
|
|
|
// Build effective goal with form data
|
|
const effectiveGoal = buildGoal(step);
|
|
const maxSteps = step.maxSteps ?? DEFAULT_STEP_LIMIT;
|
|
|
|
// Execute the sub-goal
|
|
let result: StepResult;
|
|
try {
|
|
const agentResult = await runAgent(effectiveGoal, maxSteps);
|
|
result = {
|
|
goal: step.goal,
|
|
app: step.app,
|
|
success: agentResult.success,
|
|
stepsUsed: agentResult.stepsUsed,
|
|
};
|
|
} catch (err) {
|
|
result = {
|
|
goal: step.goal,
|
|
app: step.app,
|
|
success: false,
|
|
stepsUsed: 0,
|
|
error: (err as Error).message,
|
|
};
|
|
}
|
|
|
|
results.push(result);
|
|
|
|
const status = result.success ? "completed" : "failed";
|
|
console.log(`\nStep ${i + 1} ${status} (${result.stepsUsed} steps used)`);
|
|
}
|
|
|
|
const allSuccess = results.every((r) => r.success);
|
|
return { name: workflow.name, steps: results, success: allSuccess };
|
|
}
|