Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .changeset/fix-workflow-error-reporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"agents": patch
---

Fix: `throw new Error()` in AgentWorkflow now triggers `onWorkflowError` on the Agent

Previously, throwing an error inside a workflow's `run()` method would halt the workflow but never notify the Agent via `onWorkflowError`. Only explicit `step.reportError()` calls triggered the callback, but those did not halt the workflow.

Now, unhandled errors in `run()` are automatically caught and reported to the Agent before re-throwing. A double-notification guard (`_errorReported` flag) ensures that if `step.reportError()` was already called before the throw, the auto-report is skipped.
44 changes: 44 additions & 0 deletions packages/agents/src/tests/agents/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { WorkflowStatus, WorkflowInfo } from "../../workflows.ts";
type WorkflowEnv = {
TEST_WORKFLOW: Workflow;
SIMPLE_WORKFLOW: Workflow;
THROW_IN_RUN_WORKFLOW: Workflow;
REPORT_ERROR_THEN_THROW_WORKFLOW: Workflow;
REPORT_ERROR_ONLY_WORKFLOW: Workflow;
THROW_NON_ERROR_WORKFLOW: Workflow;
};

// Test Agent for Workflow integration
Expand Down Expand Up @@ -261,4 +265,44 @@ export class TestWorkflowAgent extends Agent<WorkflowEnv> {
async getCloudflareWorkflowStatus(workflowId: string) {
return this.getWorkflowStatus("TEST_WORKFLOW", workflowId);
}

// Start a throw-in-run workflow
async runThrowInRunWorkflowTest(
workflowId: string,
params: { message: string }
): Promise<string> {
return this.runWorkflow("THROW_IN_RUN_WORKFLOW", params, {
id: workflowId
});
}

// Start a report-error-then-throw workflow
async runReportErrorThenThrowWorkflowTest(
workflowId: string,
params: { message: string }
): Promise<string> {
return this.runWorkflow("REPORT_ERROR_THEN_THROW_WORKFLOW", params, {
id: workflowId
});
}

// Start a report-error-only workflow
async runReportErrorOnlyWorkflowTest(
workflowId: string,
params: { message: string }
): Promise<string> {
return this.runWorkflow("REPORT_ERROR_ONLY_WORKFLOW", params, {
id: workflowId
});
}

// Start a throw-non-error workflow
async runThrowNonErrorWorkflowTest(
workflowId: string,
params: { value: string }
): Promise<string> {
return this.runWorkflow("THROW_NON_ERROR_WORKFLOW", params, {
id: workflowId
});
}
}
74 changes: 74 additions & 0 deletions packages/agents/src/tests/test-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,77 @@ export class SimpleTestWorkflow extends AgentWorkflow<
return result;
}
}

/**
* Test workflow that throws directly in run() (outside any step.do).
* Used to verify that unhandled errors trigger onWorkflowError on the Agent.
*/
export class ThrowInRunWorkflow extends AgentWorkflow<
TestWorkflowAgent,
{ message: string }
> {
async run(
event: AgentWorkflowEvent<{ message: string }>,
_step: AgentWorkflowStep
) {
throw new Error(event.payload.message);
}
}

/**
* Test workflow that calls step.reportError() and then throws.
* Used to verify that onWorkflowError is NOT called twice (no double notification).
*/
export class ReportErrorThenThrowWorkflow extends AgentWorkflow<
TestWorkflowAgent,
{ message: string }
> {
async run(
event: AgentWorkflowEvent<{ message: string }>,
step: AgentWorkflowStep
) {
await step.reportError(event.payload.message);
throw new Error(event.payload.message);
}
}

/**
* Test workflow that calls step.reportError() without throwing.
* Used to verify backward compatibility: reportError alone still works
* and workflow continues executing.
*/
export class ReportErrorOnlyWorkflow extends AgentWorkflow<
TestWorkflowAgent,
{ message: string }
> {
async run(
event: AgentWorkflowEvent<{ message: string }>,
step: AgentWorkflowStep
) {
await step.reportError(event.payload.message);

const result = await step.do("continue-work", async () => {
return { continued: true };
});

await step.reportComplete(result);
return result;
}
}

/**
* Test workflow that throws a non-Error value (string).
* Used to verify that String(err) path works in _autoReportError.
*/
export class ThrowNonErrorWorkflow extends AgentWorkflow<
TestWorkflowAgent,
{ value: string }
> {
async run(
event: AgentWorkflowEvent<{ value: string }>,
_step: AgentWorkflowStep
) {
// oxlint-disable-next-line no-throw-literal
throw event.payload.value;
}
}
13 changes: 12 additions & 1 deletion packages/agents/src/tests/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ export {
export type { TestState } from "./agents";

// Re-export test workflows for wrangler
export { TestProcessingWorkflow, SimpleTestWorkflow } from "./test-workflow";
export {
TestProcessingWorkflow,
SimpleTestWorkflow,
ThrowInRunWorkflow,
ReportErrorThenThrowWorkflow,
ReportErrorOnlyWorkflow,
ThrowNonErrorWorkflow
} from "./test-workflow";

// ── Env type ─────────────────────────────────────────────────────────
// Uses import-type to reference agent classes without creating runtime
Expand Down Expand Up @@ -102,6 +109,10 @@ export type Env = {
// Workflow bindings for integration testing
TEST_WORKFLOW: Workflow;
SIMPLE_WORKFLOW: Workflow;
THROW_IN_RUN_WORKFLOW: Workflow;
REPORT_ERROR_THEN_THROW_WORKFLOW: Workflow;
REPORT_ERROR_ONLY_WORKFLOW: Workflow;
THROW_NON_ERROR_WORKFLOW: Workflow;
};

// ── Fetch handler ────────────────────────────────────────────────────
Expand Down
Loading
Loading