agents
Version:
A home for your AI agents
343 lines (342 loc) • 12 kB
JavaScript
import { isInternalJsStubProp } from "./utils.js";
import { getAgentByName } from "./index.js";
import { WorkflowRejectedError } from "./workflow-types.js";
import { WorkflowEntrypoint } from "cloudflare:workers";
//#region src/workflows.ts
/**
* AgentWorkflow - Base class for Workflows that integrate with Agents
*
* Extends Cloudflare's WorkflowEntrypoint to provide seamless access to
* the Agent that started the workflow, enabling bidirectional communication.
*
* @example
* ```typescript
* import { AgentWorkflow } from 'agents/workflows';
* import type { MyAgent } from './agent';
*
* type TaskParams = { taskId: string; data: string };
*
* export class ProcessingWorkflow extends AgentWorkflow<MyAgent, TaskParams> {
* async run(event: AgentWorkflowEvent<TaskParams>, step: WorkflowStep) {
* // Access the originating Agent via typed RPC
* await this.agent.updateTaskStatus(event.payload.taskId, 'processing');
*
* const result = await step.do('process', async () => {
* // ... processing logic
* return { processed: true };
* });
*
* // Report progress to Agent (typed)
* await this.reportProgress({ step: 'process', status: 'complete', percent: 0.5 });
*
* // Broadcast to connected clients
* await this.broadcastToClients({ type: 'progress', data: result });
*
* return result;
* }
* }
* ```
*/
/**
* WeakSet to track which prototypes have been wrapped.
* This prevents re-wrapping on subsequent instantiations of the same class.
*/
const wrappedPrototypes = /* @__PURE__ */ new WeakSet();
/**
* Base class for Workflows that need access to their originating Agent.
*
* @template AgentType - The Agent class type (for typed RPC access)
* @template Params - User-defined params passed to the workflow (optional)
* @template ProgressType - Type for progress reporting (defaults to DefaultProgress)
* @template Env - Environment type (defaults to Cloudflare.Env)
*/
var AgentWorkflow = class extends WorkflowEntrypoint {
constructor(ctx, env) {
super(ctx, env);
this.__agentInitCalled = false;
this._errorReported = false;
const proto = Object.getPrototypeOf(this);
if (Object.hasOwn(proto, "run") && !wrappedPrototypes.has(proto)) {
const originalRun = proto.run;
proto.run = async function(event, step) {
if (!this.__agentInitCalled) {
const { __agentName, __agentBinding, __workflowName, __agentOrigin, ...userParams } = event.payload;
await this._initAgent(__agentName, __agentBinding, __workflowName, __agentOrigin, event.instanceId);
this.__agentInitCalled = true;
try {
const cleanedEvent = {
...event,
payload: userParams
};
const wrappedStep = this.extendStep(this._wrapStep(step), cleanedEvent);
return await this._runWithErrorReporting(originalRun, cleanedEvent, wrappedStep);
} finally {
this._disposeAgent();
}
}
return await this._runWithErrorReporting(originalRun, event, step);
};
wrappedPrototypes.add(proto);
}
}
/**
* Initialize the Agent stub from workflow params.
* Called automatically before run() executes.
*/
async _initAgent(agentName, agentBinding, workflowName, agentOrigin, instanceId) {
if (!workflowName || !agentOrigin && (!agentName || !agentBinding)) throw new Error("AgentWorkflow requires __agentName, __agentBinding, and __workflowName in params. Use agent.runWorkflow() to start workflows with proper agent context.");
this._workflowId = instanceId;
this._workflowName = workflowName;
this._errorReported = false;
if (agentOrigin && agentOrigin.version !== 1) throw new Error(`AgentWorkflow received an unsupported origin version (${agentOrigin.version}). Upgrade the "agents" package running this Workflow to match the Agent that started it.`);
if (agentOrigin?.kind === "facet") {
this._agent = await this._initFacetAgent(agentOrigin);
return;
}
const resolvedAgentName = agentOrigin?.kind === "agent" ? agentOrigin.name : agentName;
const resolvedAgentBinding = agentOrigin?.kind === "agent" ? agentOrigin.binding : agentBinding;
if (!resolvedAgentName || !resolvedAgentBinding) throw new Error("AgentWorkflow requires a valid Agent origin. Use agent.runWorkflow() to start workflows with proper agent context.");
const namespace = this.env[resolvedAgentBinding];
if (!namespace) throw new Error(`Agent binding '${resolvedAgentBinding}' not found in environment`);
this._agent = await getAgentByName(namespace, resolvedAgentName);
}
async _initFacetAgent(origin) {
const root = origin.path[0];
if (!root) throw new Error("AgentWorkflow facet origin requires a non-empty path");
const namespace = this.env[origin.rootBinding];
if (!namespace) throw new Error(`Agent binding '${origin.rootBinding}' not found in environment`);
const rootAgent = await getAgentByName(namespace, root.name);
return new Proxy({}, { get(_target, prop) {
if (isInternalJsStubProp(prop)) return void 0;
if (typeof prop !== "string") return void 0;
if (prop === "fetch") return () => {
throw new Error("AgentWorkflow.agent for sub-agent origins is an RPC-only stub — .fetch() is not supported. Use routeSubAgentRequest() or the /agents/{parent}/{name}/sub/{child}/{name} URL for external HTTP/WS routing.");
};
return async (...args) => rootAgent._cf_invokeAgentPath(origin.path, prop, args);
} });
}
/**
* Call user workflow code and report unhandled errors to the Agent.
*/
async _runWithErrorReporting(originalRun, event, step) {
try {
return await originalRun.call(this, event, step);
} catch (err) {
await this._autoReportError(err);
throw err;
}
}
/**
* Dispose the Agent stub owned by this workflow run.
*/
_disposeAgent() {
const agent = this._agent;
this._agent = void 0;
this.__agentInitCalled = false;
disposeIfPresent(agent);
}
/**
* Wrap WorkflowStep with durable Agent communication methods.
* Methods added to the wrapped step are idempotent and won't repeat on retry.
*
* Note: We add methods directly to the step object to preserve instanceof checks
* that Cloudflare's runtime may perform on the WorkflowStep class.
*/
_wrapStep(step) {
let stepCounter = 0;
const wrappedStep = step;
wrappedStep.reportComplete = async (result) => {
await step.do(`__agent_reportComplete_${stepCounter++}`, async () => {
await this.notifyAgent({
workflowName: this._workflowName,
workflowId: this._workflowId,
type: "complete",
result,
timestamp: Date.now()
});
});
};
wrappedStep.reportError = async (error) => {
const errorMessage = error instanceof Error ? error.message : error;
this._errorReported = true;
await step.do(`__agent_reportError_${stepCounter++}`, async () => {
await this.notifyAgent({
workflowName: this._workflowName,
workflowId: this._workflowId,
type: "error",
error: errorMessage,
timestamp: Date.now()
});
});
};
wrappedStep.sendEvent = async (event) => {
await step.do(`__agent_sendEvent_${stepCounter++}`, async () => {
await this.notifyAgent({
workflowName: this._workflowName,
workflowId: this._workflowId,
type: "event",
event,
timestamp: Date.now()
});
});
};
wrappedStep.updateAgentState = async (state) => {
await step.do(`__agent_updateState_${stepCounter++}`, async () => {
await this.agent._workflow_updateState("set", state);
});
};
wrappedStep.mergeAgentState = async (partialState) => {
await step.do(`__agent_mergeState_${stepCounter++}`, async () => {
await this.agent._workflow_updateState("merge", partialState);
});
};
wrappedStep.resetAgentState = async () => {
await step.do(`__agent_resetState_${stepCounter++}`, async () => {
await this.agent._workflow_updateState("reset");
});
};
return wrappedStep;
}
/**
* Extend the Agent-aware workflow step before user code receives it.
*
* Subclasses can override this to add framework-specific step helpers while
* preserving the underlying WorkflowStep object identity.
*/
extendStep(step, _event) {
return step;
}
/**
* Get the Agent stub for RPC calls.
* Provides typed access to the Agent's methods.
*
* @example
* ```typescript
* // Call any public method on the Agent
* await this.agent.updateStatus('processing');
* const data = await this.agent.getData();
* ```
*/
get agent() {
if (!this._agent) throw new Error("Agent not initialized. Ensure you're accessing this.agent inside run().");
return this._agent;
}
/**
* Get the workflow instance ID
*/
get workflowId() {
return this._workflowId;
}
/**
* Get the workflow binding name
*/
get workflowName() {
return this._workflowName;
}
/**
* Automatically report an unhandled error to the Agent.
* Skipped if reportError() was already called (prevents double notification).
* Best-effort: notification failures are swallowed so the original error propagates.
*
* @param err - The caught error
*/
async _autoReportError(err) {
if (this._errorReported) return;
this._errorReported = true;
const errorMessage = err instanceof Error ? err.message : String(err);
try {
await this.notifyAgent({
workflowName: this._workflowName,
workflowId: this._workflowId,
type: "error",
error: errorMessage,
timestamp: Date.now()
});
} catch (_notifyErr) {}
}
/**
* Send a notification to the Agent via RPC.
*
* @param callback - Callback payload to send
*/
async notifyAgent(callback) {
await this.agent._workflow_handleCallback(callback);
}
/**
* Report progress to the Agent with typed progress data.
* Triggers onWorkflowProgress() on the Agent.
*
* @param progress - Typed progress data
*
* @example
* ```typescript
* // Using default progress type
* await this.reportProgress({ step: 'fetch', status: 'running' });
* await this.reportProgress({ step: 'fetch', status: 'complete', percent: 0.5 });
*
* // With custom progress type
* await this.reportProgress({ stage: 'extract', recordsProcessed: 100 });
* ```
*/
async reportProgress(progress) {
await this.notifyAgent({
workflowName: this._workflowName,
workflowId: this._workflowId,
type: "progress",
progress,
timestamp: Date.now()
});
}
/**
* Broadcast a message to all connected WebSocket clients via the Agent.
* This is non-durable and may repeat on workflow retry.
*
* @param message - Message to broadcast (will be JSON-stringified)
*/
broadcastToClients(message) {
this.agent._workflow_broadcast(message);
}
/**
* Wait for approval from the Agent.
* Handles rejection by reporting error (durably) and throwing WorkflowRejectedError.
*
* @param step - AgentWorkflowStep object
* @param options - Wait options (timeout, eventType, stepName)
* @returns Approval payload (throws WorkflowRejectedError if rejected)
*
* @example
* ```typescript
* const approval = await this.waitForApproval(step, { timeout: '7 days' });
* // approval contains the payload from approveWorkflow()
* ```
*/
async waitForApproval(step, options) {
const stepName = options?.stepName ?? "wait-for-approval";
const eventType = options?.eventType ?? "approval";
const timeout = options?.timeout;
const event = await step.waitForEvent(stepName, {
type: eventType,
timeout
});
try {
const payload = event.payload;
if (!payload.approved) {
const reason = payload.reason;
await step.reportError(reason ?? "Workflow rejected");
throw new WorkflowRejectedError(reason, this._workflowId);
}
return payload.metadata;
} finally {
disposeIfPresent(event);
}
}
};
function isDisposableResource(value) {
return !!value && typeof value === "object" && Symbol.dispose in value && typeof value[Symbol.dispose] === "function";
}
function disposeIfPresent(value) {
if (isDisposableResource(value)) value[Symbol.dispose]();
}
//#endregion
export { AgentWorkflow, WorkflowRejectedError };
//# sourceMappingURL=workflows.js.map