Workflow 유틸리티
Workflow 유틸리티 (Workflow Utilities)
@ai-sdk/workflow-harness 는 workflow 안에서 HarnessAgent 턴을 실행하기 위한 헬퍼를 제공해요.
이 패키지는 time-sliced 및 시맨틱 에이전트 단계 턴을 위한 직렬화 가능한 상태 머신과 러너를 제공해요. 여러분의 'use workflow' 및 'use step' 함수에서 적절한 러너를 호출하세요.
핵심 harness와 workflow 파일은 프레임워크 독립적이에요. 아래의 HTTP 핸들러는 예제로 Next.js를 사용하며, 여러분의 런타임과 Workflow SDK 통합에 맞게 조정하세요. Next.js를 사용한다면 예제를 따르기 전에 프로젝트를 Workflow에 맞게 설정 했는지 확인하세요.
출처: 문서
본문
설치 (Installation)
workflow 특화 패키지에 더해 HarnessAgent 에 표시된 대로 핵심 harness 패키지, harness 어댑터, sandbox 어댑터를 설치하세요.
Harness 에이전트 설정 (Configuring the Harness Agent)
에이전트는 대부분 일반적인 방식으로 설정할 수 있어요. 시맨틱 에이전트 단계를 사용할 때는 stopWhen 을 isStepCount(1) 로 설정해 한 번의 stream() 호출이 하나의 에이전트 단계를 완료하게 하세요. 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)
시맨틱 에이전트 단계는 각 에이전트 단계 후 harness 턴을 지속해요. 위에서 하이라이트된 stopWhen 옵션으로 공유 에이전트를 설정한 다음 Workflow 단계에서 runHarnessAgentStep() 을 호출하세요.
에이전트 단계 정의 (Defining the Agent Step)
Workflow 단계를 자체 모듈에 두고 단계 본문 안에서 에이전트를 동적으로 import 하세요. 이렇게 하면 에이전트, sandbox 어댑터, 기타 Node.js 의존성이 workflow 번들 밖에 유지돼요.
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,
});
}
시맨틱 에이전트 단계 Workflow 정의 (Defining the Semantic Agent Step Workflow)
workflow 상태를 만들고 에이전트가 더 할 일이 있는 동안 agentStep() 을 계속 스케줄링하세요.
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);
}
각 ready_for_next_step 결과는 continueFrom 을 담아, 다음 Workflow 단계가 동일한 미완료 턴을 계속하게 해줘요. 턴이 끝나면 finalizeHarnessWorkflow() 는 결과를 반환하거나 workflow가 실패하면 throw해요.
시맨틱 에이전트 단계 Workflow 시작하기 (Starting the Semantic Agent Step Workflow)
서버 측 코드에서 workflow를 시작하세요. 이 Next.js 라우트는 AI SDK UI 메시지를 변환하고 agentWorkflow() 를 시작한 후 workflow의 AI SDK UI 메시지 스트림을 반환해요. 변환된 메시지를 전달하면 HarnessAgent 가 새 사용자 턴을 tool 승인 및 tool 결과 연속과 구분할 수 있어요.
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>,
});
}
workflow 상태 sessionId 는 harness 세션을 식별해요. 단계는 sandbox를 처음 만들기 전에 그것에서 sandboxId 를 파생하고 이후 단계에서 같은 ID를 재부착에 사용해요. 상태는 라이브 sandbox를 직렬화하지 않아요. agent.ts, agent-step.ts, workflow.ts, 라우트를 별도 모듈로 두어 workflow 번들에 Node 중심의 에이전트, sandbox, 프레임워크 의존성이 포함되지 않게 하세요.
Time Slices 사용하기 (Using Time Slices)
Time slices는 벽시계(wall-clock) 경계에서 장기 실행 harness 턴을 지속해요. 공유 에이전트에서 하이라이트된 stopWhen 옵션을 생략한 다음 Workflow 단계에서 runHarnessAgentTimeSlice() 를 호출하세요. 기본적으로 750초 예산을 사용하며, timeSliceSeconds 를 전달해 다른 예산을 고를 수 있어요.
Time Slice 단계 정의 (Defining the Time Slice Step)
시맨틱 에이전트 단계와 마찬가지로 Workflow 단계를 자체 모듈에 두고 단계 본문 안에서 에이전트를 동적으로 import 하세요.
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,
});
}
Time-Sliced Workflow 정의 (Defining the Time-Sliced Workflow)
workflow 상태를 만들고 에이전트가 더 할 일이 있는 동안 timeSliceStep() 을 계속 스케줄링하세요.
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);
}
각 ready_for_next_step 결과는 continueFrom 을 담아, 다음 Workflow 단계가 동일한 미완료 턴을 계속하게 해줘요. 턴이 끝나면 finalizeHarnessWorkflow() 는 결과를 반환하거나 workflow가 실패하면 throw해요.
Time-Sliced Workflow 시작하기 (Starting the Time-Sliced Workflow)
서버 측 코드에서 workflow를 시작하세요. 시맨틱 에이전트 단계와 마찬가지로 변환된 메시지를 전달해 HarnessAgent 가 새 사용자 턴을 tool 승인 및 tool 결과 연속과 구분하게 하세요.
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>,
});
}
workflow 상태 sessionId 는 harness 세션을 식별해요. 단계는 그것에서 sandboxId 를 파생해요. 그렇지 않으면 단계를 가로질러 재부착하려면 sandboxSession.id 를 별도로 지속해야 해요. agent.ts, time-slice-step.ts, workflow.ts, 라우트를 별도 모듈로 두어 workflow 번들에 Node 중심의 에이전트, sandbox, 프레임워크 의존성이 포함되지 않게 하세요.
destroyOnFinish 는 harness 세션을 파괴하지만 제공된 sandbox는 절대 파괴하지 않아요. 이후 단계가 재부착할 필요가 없을 때 호출자가 네트워크 세션에서 명시적으로 sandboxSession.destroy() 를 호출할 수 있어요. 이후 단계가 재개해야 하는 time-slice 경계에서는 절대 파괴하지 마세요.
재개 지속 (Resume Persistence)
Workflow는 각 단계가 반환하는 HarnessWorkflowState(다른 것은 제쳐두고 continueFrom 포함)를 현재 workflow 실행 동안 자동으로 지속해요. 네이티브 harness 세션을 별개의 사용자 턴 workflow 실행에 걸쳐 계속하려면 sessionId 별로 불투명한 resumeFrom 상태를 지속하세요.
저장 구현은 시맨틱 에이전트 단계와 time slices에서 동일해요. 이 예제는 파일시스템 접근이 workflow 함수 자체 밖에 있어야 하므로 Workflow 단계를 사용해요. 프로덕션에서는 로컬 파일 대신 내구성 있는 저장소를 사용하세요.
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));
}
workflow 상태를 만들기 전에 이전 resumeFrom 상태를 로드하고, 실행 루프 후 업데이트된 값을 지속하세요. 통합 지점은 두 workflow 접근 모두 동일해요.
시맨틱 에이전트 단계 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);
}