UNPKG

agents

Version:

A home for your AI agents

343 lines (342 loc) 12 kB
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