branch:
approval-agent.ts
7422 bytesRaw
import { callable } from "agents";
import { PlaygroundAgent as Agent } from "../../shared/playground-agent";
import type { WorkflowInfo } from "agents/workflows";

// No custom state needed - we use SDK's workflow tracking
// Title/description are stored in workflow metadata
export interface ApprovalAgentState {
  // Empty - all data comes from getWorkflows()
}

// Extended workflow info for the UI
export interface ApprovalRequest {
  id: string;
  title: string;
  description: string;
  status: "pending" | "approved" | "rejected";
  createdAt: string;
  resolvedAt?: string;
  reason?: string;
}

export class ApprovalAgent extends Agent<Env, ApprovalAgentState> {
  initialState: ApprovalAgentState = {};

  // ─────────────────────────────────────────────────────────────────────────────
  // Workflow lifecycle callbacks
  // ─────────────────────────────────────────────────────────────────────────────

  async onWorkflowProgress(
    _workflowName: string,
    workflowId: string,
    progress: { status: "pending" | "approved" | "rejected"; message: string }
  ): Promise<void> {
    this.broadcast(
      JSON.stringify({
        type: "approval_progress",
        requestId: workflowId,
        progress
      })
    );
  }

  async onWorkflowComplete(
    _workflowName: string,
    workflowId: string,
    result?: { approved: boolean }
  ): Promise<void> {
    this.broadcast(
      JSON.stringify({
        type: result?.approved ? "approval_approved" : "approval_rejected",
        requestId: workflowId
      })
    );
  }

  async onWorkflowError(
    _workflowName: string,
    workflowId: string,
    error: string
  ): Promise<void> {
    this.broadcast(
      JSON.stringify({
        type: "approval_error",
        requestId: workflowId,
        error
      })
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Helper to convert SDK WorkflowInfo to our ApprovalRequest format
  // ─────────────────────────────────────────────────────────────────────────────

  private _toApprovalRequest(w: WorkflowInfo): ApprovalRequest {
    const metadata = w.metadata as {
      title?: string;
      description?: string;
    } | null;

    // Map SDK status to our simpler status
    // "queued", "running", "waiting" are all "pending" from the user's perspective
    let status: "pending" | "approved" | "rejected" = "pending";
    if (w.status === "complete") {
      status = "approved";
    } else if (w.status === "errored" || w.status === "terminated") {
      status = "rejected";
    }

    return {
      id: w.workflowId,
      title: metadata?.title || "Untitled",
      description: metadata?.description || "",
      status,
      createdAt: w.createdAt.toISOString(),
      resolvedAt: w.completedAt?.toISOString(),
      reason: w.error?.message
    };
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Callable methods
  // ─────────────────────────────────────────────────────────────────────────────

  @callable({ description: "Submit a new approval request" })
  async requestApproval(
    title: string,
    description: string
  ): Promise<ApprovalRequest> {
    // Start the approval workflow, storing title/description in metadata
    const workflowId = await this.runWorkflow(
      "ApprovalWorkflow",
      { title, description },
      { metadata: { title, description } }
    );

    this.broadcast(
      JSON.stringify({
        type: "approval_requested",
        requestId: workflowId,
        title
      })
    );

    // Return the request info
    return {
      id: workflowId,
      title,
      description,
      status: "pending",
      createdAt: new Date().toISOString()
    };
  }

  @callable({ description: "Get all approval requests" })
  listRequests(): ApprovalRequest[] {
    const { workflows } = this.getWorkflows({
      workflowName: "ApprovalWorkflow"
    });
    return workflows.map((w) => this._toApprovalRequest(w));
  }

  @callable({ description: "Approve a pending request" })
  async approve(requestId: string): Promise<boolean> {
    // Check if workflow exists in tracking table
    const workflow = this.getWorkflow(requestId);
    if (!workflow) {
      return false;
    }

    // Don't approve already completed workflows
    if (
      workflow.status === "complete" ||
      workflow.status === "errored" ||
      workflow.status === "terminated"
    ) {
      return false;
    }

    // Resume the workflow with approval
    // Note: we don't check "waiting" status because the local tracking table
    // doesn't auto-sync with Cloudflare - the workflow could be waiting even
    // if our local status shows "queued" or "running"
    await this.approveWorkflow(requestId, {
      reason: "Approved via playground",
      metadata: { approvedBy: "demo-user" }
    });

    return true;
  }

  @callable({ description: "Reject a pending request" })
  async reject(requestId: string, reason?: string): Promise<boolean> {
    // Check if workflow exists in tracking table
    const workflow = this.getWorkflow(requestId);
    if (!workflow) {
      return false;
    }

    // Don't reject already completed workflows
    if (
      workflow.status === "complete" ||
      workflow.status === "errored" ||
      workflow.status === "terminated"
    ) {
      return false;
    }

    // Resume the workflow with rejection
    await this.rejectWorkflow(requestId, {
      reason: reason || "Rejected via playground"
    });

    return true;
  }

  @callable({ description: "Clear resolved approval requests" })
  clearApprovals(): number {
    const count = this.deleteWorkflows({
      workflowName: "ApprovalWorkflow",
      status: ["complete", "errored", "terminated"]
    });

    this.broadcast(
      JSON.stringify({
        type: "approvals_cleared",
        count
      })
    );

    return count;
  }

  @callable({ description: "Get approval stats" })
  getStats(): { pending: number; approved: number; rejected: number } {
    const { workflows } = this.getWorkflows({
      workflowName: "ApprovalWorkflow"
    });
    return {
      pending: workflows.filter(
        (w) =>
          w.status === "queued" ||
          w.status === "running" ||
          w.status === "waiting"
      ).length,
      approved: workflows.filter((w) => w.status === "complete").length,
      rejected: workflows.filter(
        (w) => w.status === "errored" || w.status === "terminated"
      ).length
    };
  }
}