Skip to main content

Workflow Utilities

还没翻译译自英文(ai-sdk.dev)本页 42 段都还没译

@ai-sdk/workflow-harness provides helpers for running HarnessAgent turns inside a workflow.

The package provides a serializable state machine and runners for time-sliced and semantic agent step turns. You call the appropriate runner from your own 'use workflow' and 'use step' functions.

The core harness and workflow files are framework-independent. The HTTP handlers shown below use Next.js as one example; adapt them to your runtime and Workflow SDK integration. If you use Next.js, ensure you have configured your project for Workflow before following the examples.

Installation

pnpm add @ai-sdk/workflow-harness workflow
npm install @ai-sdk/workflow-harness workflow
yarn add @ai-sdk/workflow-harness workflow
bun add @ai-sdk/workflow-harness workflow

In addition to the workflow specific packages, install the core harness package, a harness adapter, and a sandbox adapter as shown in HarnessAgent.

Configuring the Harness Agent

The agent can be configured in the usual way, for the most part. When using semantic agent steps, set stopWhen to isStepCount(1) so one call to stream() completes one agent step. Omit it when using time slices.

import { HarnessAgent } from '@ai-sdk/harness/agent';
import { claudeCode } from '@ai-sdk/harness-claude-code';
import { isStepCount } from 'ai';

export const agent = new HarnessAgent({
  harness: claudeCode,
  instructions: 'You are a helpful coding assistant.',
  /*
   * Only needed for semantic agent-step workflows.
   * Omit this for time-sliced workflows.
   */
  stopWhen: isStepCount(1),
});

Using Semantic Agent Steps

Semantic agent steps persist the harness turn after each agent step. Configure the shared agent with the highlighted stopWhen option shown above, then call runHarnessAgentStep() from a Workflow step.

Defining the Agent Step

Keep the Workflow step in its own module and import the agent dynamically inside the step body. This keeps the agent, sandbox adapter, and other Node.js dependencies out of the workflow bundle.

import {
  runHarnessAgentStep,
  type HarnessWorkflowState,
} from '@ai-sdk/workflow-harness';

export async function agentStep(
  state: HarnessWorkflowState,
): Promise<HarnessWorkflowState> {
  'use step';

  const { agent } = await import('./agent');
  const {
    createVercelNetworkSandboxSession,
    resumeVercelNetworkSandboxSession,
  } = await import('@ai-sdk/sandbox-vercel');
  const sandboxId = `harness-${state.sessionId}`;
  const isFirstStep = state.resumeFrom == null && state.continueFrom == null;
  const sandboxSession = isFirstStep
    ? await createVercelNetworkSandboxSession({
        sandboxId,
        runtime: 'node24',
        ports: [4000],
        template: await agent.getSandboxTemplate(),
      })
    : await resumeVercelNetworkSandboxSession({ sandboxId });

  return runHarnessAgentStep({
    agent,
    state,
    sandboxSession,
  });
}

Defining the Semantic Agent Step Workflow

Create the workflow state and keep scheduling agentStep() while the agent has more work.

import { agentStep } from './agent-step';
import {
  createHarnessWorkflowState,
  finalizeHarnessWorkflow,
  type HarnessWorkflowInput,
} from '@ai-sdk/workflow-harness';

export async function agentWorkflow(input: {
  messages: NonNullable<HarnessWorkflowInput['messages']>;
  sessionId: string;
}) {
  'use workflow';

  let state = createHarnessWorkflowState(input);

  do {
    state = await agentStep(state);
  } while (state.status === 'ready_for_next_step');

  return finalizeHarnessWorkflow(state);
}

Note

This example covers the workflow execution foundation only. For multi-turn conversations, you must persist the harness session's resumeFrom state between workflow runs. See Resume Persistence.

Each ready_for_next_step result carries continueFrom, which lets the next Workflow step continue the same unfinished turn. When the turn finishes, finalizeHarnessWorkflow() returns its result or throws if the workflow failed.

Starting the Semantic Agent Step Workflow

Start the workflow from server-side code. This Next.js route converts the AI SDK UI messages, starts agentWorkflow(), and returns the workflow's AI SDK UI message stream. Passing the converted messages lets HarnessAgent distinguish a new user turn from tool approval and tool result continuations.

import { agentWorkflow } from '../../../harness-workflow/workflow';
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  type UIMessage,
  type UIMessageChunk,
} from 'ai';
import { start } from 'workflow/api';

export async function POST(request: Request) {
  const body: {
    id?: string;
    messages: UIMessage[];
  } = await request.json();

  if (!body.id) {
    return new Response('Missing chat ID', { status: 400 });
  }

  const messages = await convertToModelMessages(body.messages);
  const run = await start(agentWorkflow, [
    {
      messages,
      sessionId: body.id,
    },
  ]);

  return createUIMessageStreamResponse({
    stream: run.readable as ReadableStream<UIMessageChunk>,
  });
}

The workflow state sessionId identifies the harness session. The step derives sandboxId from it before first creating the sandbox and uses that same ID to reattach on later steps. The state does not serialize a live sandbox. Keep agent.ts, agent-step.ts, workflow.ts, and the route in separate modules so the workflow bundle does not include Node-heavy agent, sandbox, or framework dependencies.

Using Time Slices

Time slices persist a long-running harness turn at wall-clock boundaries. Omit the highlighted stopWhen option from the shared agent, then call runHarnessAgentTimeSlice() from a Workflow step. It uses a 750-second budget by default; pass timeSliceSeconds to choose a different budget.

Defining the Time Slice Step

As with semantic agent steps, keep the Workflow step in its own module and import the agent dynamically inside the step body.

import {
  runHarnessAgentTimeSlice,
  type HarnessWorkflowState,
} from '@ai-sdk/workflow-harness';

export async function timeSliceStep(
  state: HarnessWorkflowState,
): Promise<HarnessWorkflowState> {
  'use step';

  const { agent } = await import('./agent');
  const {
    createVercelNetworkSandboxSession,
    resumeVercelNetworkSandboxSession,
  } = await import('@ai-sdk/sandbox-vercel');
  const sandboxId = `harness-${state.sessionId}`;
  const isFirstStep = state.resumeFrom == null && state.continueFrom == null;
  const sandboxSession = isFirstStep
    ? await createVercelNetworkSandboxSession({
        sandboxId,
        runtime: 'node24',
        ports: [4000],
        template: await agent.getSandboxTemplate(),
      })
    : await resumeVercelNetworkSandboxSession({ sandboxId });

  return runHarnessAgentTimeSlice({
    agent,
    state,
    sandboxSession,
  });
}

Defining the Time-Sliced Workflow

Create the workflow state and keep scheduling timeSliceStep() while the agent has more work.

import { timeSliceStep } from './time-slice-step';
import {
  createHarnessWorkflowState,
  finalizeHarnessWorkflow,
  type HarnessWorkflowInput,
} from '@ai-sdk/workflow-harness';

export async function timeSliceWorkflow(input: {
  messages: NonNullable<HarnessWorkflowInput['messages']>;
  sessionId: string;
}) {
  'use workflow';

  let state = createHarnessWorkflowState(input);

  do {
    state = await timeSliceStep(state);
  } while (state.status === 'ready_for_next_step');

  return finalizeHarnessWorkflow(state);
}

Note

This example covers the workflow execution foundation only. For multi-turn conversations, you must persist the harness session's resumeFrom state between workflow runs. See Resume Persistence.

Each ready_for_next_step result carries continueFrom, which lets the next Workflow step continue the same unfinished turn. When the turn finishes, finalizeHarnessWorkflow() returns its result or throws if the workflow failed.

Starting the Time-Sliced Workflow

Start the workflow from server-side code. As with semantic agent steps, pass the converted messages so HarnessAgent can distinguish a new user turn from tool approval and tool result continuations.

import { timeSliceWorkflow } from '../../../harness-workflow/workflow';
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  type UIMessage,
  type UIMessageChunk,
} from 'ai';
import { start } from 'workflow/api';

export async function POST(request: Request) {
  const body: {
    id?: string;
    messages: UIMessage[];
  } = await request.json();

  if (!body.id) {
    return new Response('Missing chat ID', { status: 400 });
  }

  const messages = await convertToModelMessages(body.messages);
  const run = await start(timeSliceWorkflow, [
    {
      messages,
      sessionId: body.id,
    },
  ]);

  return createUIMessageStreamResponse({
    stream: run.readable as ReadableStream<UIMessageChunk>,
  });
}

If you use Next.js, wrap your config with withWorkflow() so the Workflow SDK transforms 'use workflow' and 'use step' modules. Other frameworks have their own Workflow SDK setup.

import { withWorkflow } from 'workflow/next';

const nextConfig = {};

export default withWorkflow(nextConfig);

The workflow state sessionId identifies the harness session. The step derives sandboxId from it; otherwise, you must persist sandboxSession.id separately to reattach across steps. Keep agent.ts, time-slice-step.ts, workflow.ts, and the route in separate modules so the workflow bundle does not include Node-heavy agent, sandbox, or framework dependencies.

destroyOnFinish destroys the harness session, but never the supplied sandbox. The caller can explicitly call sandboxSession.destroy() on the network session when no later step needs to reattach. Never destroy it at a time-slice boundary where a subsequent step must resume it.

Resume Persistence

Workflow automatically persists the HarnessWorkflowState returned by each step, including continueFrom, for the duration of the current workflow run. To continue the native harness session across separate user-turn workflow runs, persist the opaque resumeFrom state by sessionId.

The storage implementation is the same for semantic agent steps and time slices. This example uses Workflow steps because filesystem access must stay out of the workflow function itself. Use durable storage instead of local files in production.

import type { HarnessV1ResumeSessionState } from '@ai-sdk/harness';
import { safeParseJSON } from '@ai-sdk/provider-utils';

const RESUME_DIR = '.harness-sessions';

function fileName(sessionId: string): string {
  return `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
}

export async function loadResumeStep(
  sessionId: string,
): Promise<HarnessV1ResumeSessionState | undefined> {
  'use step';

  const { readFile } = await import('node:fs/promises');
  const { join } = await import('node:path');

  let text: string;
  try {
    text = await readFile(
      join(process.cwd(), RESUME_DIR, fileName(sessionId)),
      'utf8',
    );
  } catch {
    return undefined;
  }

  const parsed = await safeParseJSON({ text });

  return parsed.success
    ? (parsed.value as unknown as HarnessV1ResumeSessionState)
    : undefined;
}

export async function persistResumeStep({
  sessionId,
  resumeState,
}: {
  sessionId: string;
  resumeState: HarnessV1ResumeSessionState | undefined;
}): Promise<void> {
  'use step';

  if (!resumeState) return;

  const { mkdir, writeFile } = await import('node:fs/promises');
  const { join } = await import('node:path');
  const dir = join(process.cwd(), RESUME_DIR);

  await mkdir(dir, { recursive: true });
  await writeFile(join(dir, fileName(sessionId)), JSON.stringify(resumeState));
}

Load the previous resumeFrom state before creating the workflow state, then persist the updated value after the execution loop. The integration points are the same for both workflow approaches.

Semantic Agent Step Workflow

import { agentStep } from './agent-step';
import { loadResumeStep, persistResumeStep } from './resume-store';
import {
  createHarnessWorkflowState,
  finalizeHarnessWorkflow,
  type HarnessWorkflowInput,
} from '@ai-sdk/workflow-harness';

export async function agentWorkflow(input: {
  messages: NonNullable<HarnessWorkflowInput['messages']>;
  sessionId: string;
}) {
  'use workflow';

  const resumeFrom = await loadResumeStep(input.sessionId);
  let state = createHarnessWorkflowState({ ...input, resumeFrom });

  do {
    state = await agentStep(state);
  } while (state.status === 'ready_for_next_step');

  await persistResumeStep({
    sessionId: state.sessionId,
    resumeState: state.resumeFrom,
  });

  return finalizeHarnessWorkflow(state);
}

Time-Sliced Workflow

import { loadResumeStep, persistResumeStep } from './resume-store';
import { timeSliceStep } from './time-slice-step';
import {
  createHarnessWorkflowState,
  finalizeHarnessWorkflow,
  type HarnessWorkflowInput,
} from '@ai-sdk/workflow-harness';

export async function timeSliceWorkflow(input: {
  messages: NonNullable<HarnessWorkflowInput['messages']>;
  sessionId: string;
}) {
  'use workflow';

  const resumeFrom = await loadResumeStep(input.sessionId);
  let state = createHarnessWorkflowState({ ...input, resumeFrom });

  do {
    state = await timeSliceStep(state);
  } while (state.status === 'ready_for_next_step');

  await persistResumeStep({
    sessionId: state.sessionId,
    resumeState: state.resumeFrom,
  });

  return finalizeHarnessWorkflow(state);
}