Skip to content
74 changes: 65 additions & 9 deletions packages/core/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ export class Agent<
: defaultReplanningCycleLimit;
}

private buildFallbackContextFromError(cacheError: unknown): string {
const executionContext = (cacheError as any)?.executionContext;
if (executionContext?.fallbackContext) {
return executionContext.fallbackContext;
}

const errorMessage =
cacheError instanceof Error ? cacheError.message : String(cacheError);

return [
'Previous cached workflow execution failed.',
`Error: ${errorMessage}`,
'Please retry with a different approach.',
].join('\n');
}

constructor(interfaceInstance: InterfaceType, opts?: AgentOpt) {
this.interface = interfaceInstance;

Expand Down Expand Up @@ -867,6 +883,10 @@ export class Agent<
isVlmUiTars || cacheable === false
? undefined
: this.taskCache?.matchPlanCache(taskPrompt);

// Use local variable for context to avoid modifying instance state
let contextForPlanning = this.aiActContext;

if (
matchedCache &&
this.taskCache?.isCacheResultUsed &&
Expand All @@ -879,8 +899,23 @@ export class Agent<
);

debug('matched cache, will call .runYaml to run the action');
const yaml = matchedCache.cacheContent.yamlWorkflow;
return this.runYaml(yaml);
const yamlContent = matchedCache.cacheContent.yamlWorkflow;
try {
return await this.runYaml(yamlContent);
} catch (cacheError) {
// Cache execution failed, fall back to normal AI planning
debug(
'cache execution failed, falling back to AI planning:',
cacheError instanceof Error ? cacheError.message : String(cacheError),
);

const fallbackContext = this.buildFallbackContextFromError(cacheError);

// Append failure context to original aiActContext using local variable
contextForPlanning = this.aiActContext
? `${this.aiActContext}\n\n--- Cache Execution Failed ---\n${fallbackContext}`
: fallbackContext;
}
}

// If cache matched but yamlWorkflow is empty, fall through to normal execution
Expand All @@ -895,7 +930,7 @@ export class Agent<
modelConfigForPlanning,
defaultIntentModelConfig,
includeBboxInPlanning,
this.aiActContext,
contextForPlanning,
cacheable,
replanningCycleLimit,
imagesIncludeCount,
Expand Down Expand Up @@ -1227,13 +1262,34 @@ export class Agent<
await player.run();

if (player.status === 'error') {
const errors = player.taskStatusList
.filter((task) => task.status === 'error')
.map((task) => {
return `task - ${task.name}: ${task.error?.message}`;
})
const { fallbackContext, completedTasks, failedTasks, pendingTasks } =
player.buildFailureContext();

// Build error message for logging
const totalTasks = player.taskStatusList.length;
const errors = failedTasks
.map(
(t) =>
`task ${t.index + 1}/${totalTasks} "${t.name}": ${t.error?.message}`,
)
.join('\n');
throw new Error(`Error(s) occurred in running yaml script:\n${errors}`);

const error = new Error(
`Error(s) occurred in running yaml script:\n${errors}`,
);

// Attach execution context (backward compatible + new fields)
(error as any).executionContext = {
successfulTasks: completedTasks.map((t) => t.name),
failedTasks: failedTasks.map((t) => ({ name: t.name, error: t.error })),
totalTasks,
fallbackContext,
completedTasks,
failedTasksDetailed: failedTasks,
pendingTasks,
};

throw error;
}

return {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export interface FreeFn {
}

export interface ScriptPlayerTaskStatus extends MidsceneYamlTask {
index: number;
status: ScriptPlayerStatusValue;
currentStep?: number;
totalSteps: number;
Expand Down
145 changes: 145 additions & 0 deletions packages/core/src/yaml/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,151 @@ export class ScriptPlayer<T extends MidsceneYamlScriptEnv> {
this.errorInSetup = error;
}

/**
* Build detailed failure context for AI fallback when cached workflow execution fails.
*
* This method constructs a human-readable failure summary with step-by-step breakdown,
* including completed tasks, failed tasks with error details, and pending tasks.
* The generated context is used to inform the AI during fallback execution so it can
* continue from the point of failure without repeating successful steps.
*
* @returns Object containing:
* - `fallbackContext`: Formatted string with detailed failure information for AI context
* - `completedTasks`: Array of successfully completed tasks with their indices and names
* - `failedTasks`: Array of failed tasks with indices, names, errors, and step information
* - `pendingTasks`: Array of tasks that were not executed due to earlier failure
*
* @example
* ```typescript
* // When a workflow fails at step 3:
* const context = player.buildFailureContext();
* // context.fallbackContext will contain:
* // "Previous cached workflow execution failed at step 3/5:
* //
* // Completed successfully:
* // ✓ Step 1/5: "Login to system"
* // ✓ Step 2/5: "Navigate to dashboard"
* //
* // Failed:
* // ✗ Step 3/5: "Click submit button"
* // Error: Element not found
* //
* // Remaining steps (not executed):
* // - Step 4/5: "Verify success message"
* // - Step 5/5: "Logout"
* //
* // Please continue from Step 3 and avoid repeating the successful steps."
* ```
*/
buildFailureContext(): {
fallbackContext: string;
completedTasks: { index: number; name: string }[];
failedTasks: {
index: number;
name: string;
error?: Error;
currentStep?: number;
totalSteps: number;
}[];
pendingTasks: { index: number; name: string }[];
} {
const totalTasks = this.taskStatusList.length;

const completedTasks = this.taskStatusList
.filter((t) => t.status === 'done')
.map((t) => ({ index: t.index, name: t.name }));

const failedTasks = this.taskStatusList
.filter((t) => t.status === 'error')
.map((t) => ({
index: t.index,
name: t.name,
error: t.error,
currentStep: t.currentStep,
totalSteps: t.totalSteps,
}));

const pendingTasks = this.taskStatusList
.filter((t) => t.status === 'init')
.map((t) => ({ index: t.index, name: t.name }));

const parts: string[] = [];

// Title
if (failedTasks.length > 0) {
parts.push(
`Previous cached workflow execution failed at step ${failedTasks[0].index + 1}/${totalTasks}:\n`,
);
} else {
// No specific task failed, but execution was interrupted
parts.push(
`Previous cached workflow execution was interrupted (${completedTasks.length}/${totalTasks} tasks completed).\n`,
);
}

// Completed
if (completedTasks.length > 0) {
parts.push(
'Completed successfully:',
...completedTasks.map(
(t) => ` ✓ Step ${t.index + 1}/${totalTasks}: "${t.name}"`,
),
'',
);
}

// Failed
if (failedTasks.length > 0) {
parts.push(
'Failed:',
...failedTasks.flatMap((t) => {
const stepInfo =
t.currentStep !== undefined
? ` (at substep ${t.currentStep + 1}/${t.totalSteps})`
: '';
return [
` ✗ Step ${t.index + 1}/${totalTasks}: "${t.name}"${stepInfo}`,
` Error: ${t.error?.message || 'Unknown error'}`,
];
}),
'',
);
}

// Pending
if (pendingTasks.length > 0) {
parts.push(
'Remaining steps (not executed):',
...pendingTasks.map(
(t) => ` - Step ${t.index + 1}/${totalTasks}: "${t.name}"`,
),
'',
);
}

// Guidance
if (failedTasks.length > 0) {
parts.push(
`Please continue from Step ${failedTasks[0].index + 1} and avoid repeating the successful steps.`,
);
} else if (pendingTasks.length > 0) {
// No failed tasks but there are pending tasks
parts.push(
`Please continue from Step ${pendingTasks[0].index + 1} and complete the remaining tasks.`,
);
} else {
// All tasks completed but execution still failed (unlikely edge case)
parts.push('Please retry the entire workflow with a different approach.');
}

return {
fallbackContext: parts.join('\n'),
completedTasks,
failedTasks,
pendingTasks,
};
}

private notifyCurrentTaskStatusChange(taskIndex?: number) {
const taskIndexToNotify =
typeof taskIndex === 'number' ? taskIndex : this.currentTaskIndex;
Expand Down
Loading