할 일 목록
할 일 목록 (Todo list)
에이전트 상태에서 동기화된 실시간 할 일 목록으로 에이전트 진행 상황을 추적하세요
모든 에이전트 상호작용이 채팅인 것은 아닙니다. 때로는 에이전트가 다단계 계획을 실행 중이며, 진행 상황을 보여주는 가장 좋은 방법은 실시간으로 갱신되는 **할 일 목록(todo list)**입니다. deep agent 할 일 목록 패턴은 에이전트의 상태에서 todos 배열을 직접 읽어, 에이전트가 계획을 실행하는 동안 각 항목을 현재 상태와 함께 렌더링합니다. 채팅에 사용하는 것과 동일한 useStream 훅 위에 구축된 진행 대시보드입니다. 에이전트 상태가 메시지 버블뿐 아니라 어떤 UI도 구동할 수 있음을 보여줍니다.
작동 방식 (How it works)
Deep agents는 TodoListMiddleware를 선택하면 todos 상태 채널을 노출할 수 있습니다. 이 미들웨어는 write_todos 도구를 추가하고 에이전트가 계획을 실행하며 작업 진행 상황을 유지합니다. 에이전트가 실행되면서 각 todo의 상태를 "pending"에서 "in_progress"로, 다시 "completed"로 업데이트합니다. useStream 훅은 stream.values.todos를 통해 이 상태를 노출하며, 여러분의 UI는 이를 반응형으로 렌더링합니다.
흐름은 다음과 같습니다:
- 사용자가 요청을 제출합니다
- 에이전트가 계획을 만들고 상태에
todos를 채웁니다 - 에이전트가 각 todo를 실행하며
pending→in_progress→completed로 전환합니다 - 에이전트가 진행됨에 따라
stream.values.todos가 실시간으로 갱신됩니다 - 여러분의 UI가 현재 상태로 할 일 목록을 다시 렌더링합니다
useStream 설정 (Setting up useStream)
에이전트에서 TodoListMiddleware를 활성화하세요.
const agent = await createDeepAgent({ model: "google-genai:gemini-3.6-flash", middleware: [todoListMiddleware()], });
```ts OpenAI theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = await createDeepAgent({
model: "openai:gpt-5.5",
middleware: [todoListMiddleware()],
});
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = await createDeepAgent({
model: "anthropic:claude-sonnet-5",
middleware: [todoListMiddleware()],
});
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = await createDeepAgent({
model: "openrouter:z-ai/glm-5.2",
middleware: [todoListMiddleware()],
});
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = await createDeepAgent({
model: "fireworks:accounts/fireworks/models/glm-5p2",
middleware: [todoListMiddleware()],
});
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = await createDeepAgent({
model: "baseten:zai-org/GLM-5.2",
middleware: [todoListMiddleware()],
});
import { createDeepAgent } from "deepagents";
import { todoListMiddleware } from "langchain";
const agent = await createDeepAgent({
model: "ollama:north-mini-code-1.0",
middleware: [todoListMiddleware()],
});
그런 다음 useStream을 그 에이전트를 가리키도록 설정하고, stream.values에서 todos를 읽으세요.
const AGENT_URL = "http://localhost:2024";
export function TodoAgent() {
const stream = useStream
const todos = stream.values?.todos ?? [];
return (
<div>
<TodoList todos={todos} />
{stream.messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
</div>
);
}
```vue Vue theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { computed } from "vue";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_todo_list",
});
const todos = computed(() => stream.values.value?.todos ?? []);
</script>
<template>
<div>
<TodoList :todos="todos" />
<Message
v-for="msg in stream.messages.value"
:key="msg.id"
:message="msg"
/>
</div>
</template>
<script lang="ts">
import { useStream } from "@langchain/svelte";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_todo_list",
});
const todos = $derived(stream.values?.todos ?? []);
</script>
<div>
<TodoList {todos} />
{#each stream.messages as msg (msg.id)}
<Message message={msg} />
{/each}
</div>
import { Component, computed } from "@angular/core";
import { injectStream } from "@langchain/angular";
const AGENT_URL = "http://localhost:2024";
@Component({
selector: "app-todo-agent",
template: `
<div>
<app-todo-list [todos]="todos()" />
@for (msg of stream.messages(); track msg.id) {
<app-message [message]="msg" />
}
</div>
`,
})
export class TodoAgentComponent {
stream = injectStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_todo_list",
});
todos = computed(() => this.stream.values()?.todos ?? []);
}
TodoList 컴포넌트 만들기 (Building the TodoList component)
할 일 목록은 각 항목을 상태 아이콘, 색상 코딩, 현재 상태를 반영하는 시각적 스타일과 함께 렌더링합니다:
function TodoList({ todos }: { todos: Todo[] }) {
const completed = todos.filter((t) => t.status === "completed").length;
const percentage = todos.length
? Math.round((completed / todos.length) * 100)
: 0;
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">Agent Progress</h2>
<span className="text-sm text-gray-500">
{completed}/{todos.length} tasks
</span>
</div>
<ProgressBar percentage={percentage} />
<ul className="mt-4 space-y-2">
{todos.map((todo, i) => (
<TodoItem key={i} todo={todo} />
))}
</ul>
</div>
);
}
진행률 표시줄 (Progress bar)
시각적 진행률 표시줄은 사용자에게 전체 완료 상황을 한눈에 제공합니다:
function ProgressBar({ percentage }: { percentage: number }) {
return (
<div className="space-y-1">
<div className="flex items-center justify-between text-xs text-gray-500">
<span>Progress</span>
<span>{percentage}%</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full bg-green-500 transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
개별 할 일 항목 (Individual todo items)
각 항목은 상태 아이콘, 색상 코딩된 텍스트, 완료된 작업에 대한 취소선 스타일을 받습니다:
function TodoItem({ todo }: { todo: Todo }) {
const config = {
pending: {
icon: "○",
textClass: "text-gray-600",
bgClass: "bg-gray-50",
iconClass: "text-gray-400",
},
in_progress: {
icon: "◉",
textClass: "text-amber-800",
bgClass: "bg-amber-50 border-amber-200",
iconClass: "text-amber-500 animate-pulse",
},
completed: {
icon: "✓",
textClass: "text-green-800 line-through",
bgClass: "bg-green-50 border-green-200",
iconClass: "text-green-500",
},
};
const style = config[todo.status];
return (
<li
className={`flex items-start gap-3 rounded-md border px-3 py-2 ${style.bgClass}`}
>
<span className={`mt-0.5 text-lg leading-none ${style.iconClass}`}>
{style.icon}
</span>
<span className={`text-sm ${style.textClass}`}>{todo.content}</span>
</li>
);
}
in_progress 아이콘은 animate-pulse를 사용해 현재 진행 중인 작업에 주목을 끕니다.
진행률 계산 (Calculating progress)
진행 지표를 todos 배열에서 직접 도출하세요:
const todos = stream.values?.todos ?? [];
const completed = todos.filter((t) => t.status === "completed").length;
const inProgress = todos.filter((t) => t.status === "in_progress").length;
const pending = todos.filter((t) => t.status === "pending").length;
const percentage = todos.length
? Math.round((completed / todos.length) * 100)
: 0;
이 값들은 에이전트가 상태를 수정함에 따라 반응형으로 갱신되어, 진행률 표시줄과 카운터를 동기화 상태로 유지합니다.
채팅 메시지와 결합 (Combining with chat messages)
할 일 목록은 일반 채팅 인터페이스와 함께 작동합니다. 실용적인 레이아웃은 할 일 목록을 지속적인 사이드바 또는 헤더 패널로 표시하고, 채팅 메시지를 아래에 둡니다:
function TodoAgentLayout() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_todo_list",
});
const todos = stream.values?.todos ?? [];
return (
<div className="flex h-screen flex-col">
{todos.length > 0 && (
<div className="border-b bg-gray-50 p-4">
<TodoList todos={todos} />
</div>
)}
<main className="flex-1 overflow-y-auto p-6">
<div className="mx-auto max-w-2xl space-y-4">
{stream.messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
</div>
</main>
<ChatInput
onSubmit={(text) =>
stream.submit({ messages: [{ type: "human", content: text }] })
}
isLoading={stream.isLoading}
/>
</div>
);
}
사용 사례 (Use cases)
할 일 목록 패턴은 에이전트가 구조화된 계획을 실행하는 모든 시나리오에 적합합니다:
- 프로젝트 계획: 에이전트가 프로젝트를 작업으로 나누고 순차적으로 처리합니다
- 리서치 워크플로우: 각 리서치 질문이 에이전트가 조사하고 완료하는 todo가 됩니다
- 데이터 처리: 수집, 검증, 변환, 내보내기 같은 단계가 각각 자신의 todo를 가집니다
- 온보딩 흐름: 에이전트가 설정 단계를 진행하며 각 서비스를 구성할 때마다 하나씩 체크합니다
- 보고서 생성: 보고서의 각 섹션이 todo가 됩니다: 데이터 수집, 추세 분석, 요약 작성, 출력 형식화
빈 상태와 로딩 상태 처리 (Handling empty and loading states)
에이전트가 계획을 만들기 전의 초기 상태를 처리하세요:
function TodoList({ todos, isLoading }: { todos: Todo[]; isLoading: boolean }) {
if (todos.length === 0 && !isLoading) {
return null;
}
if (todos.length === 0 && isLoading) {
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="flex items-center gap-2 text-sm text-gray-500">
<span className="animate-spin">⟳</span>
Agent is creating a plan...
</div>
</div>
);
}
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
{/* ... full todo list rendering */}
</div>
);
}
모범 사례 (Best practices)
- 할 일 목록을 눈에 띄게 표시하세요. 계획 기반 에이전트의 기본 진행 지표입니다. 접힌 부분 아래에 매장하지 마세요.
- 상태 전환을 애니메이션하세요. 부드러운 전환은 에이전트를 더 반응형으로 느끼게 합니다. 배경색, 텍스트 장식, 불투명도에 CSS 전환을 사용하세요.
in_progress항목 하나만 강조하세요. 에이전트는 보통 한 번에 한 작업을 처리합니다. 여러 항목이in_progress로 표시되면 UI가 시끄러워집니다. 첫 번째 항목만 펄스(pulse)하는 것을 고려하세요.- 완료된 항목은 접거나 흐리게 하세요. 목록이 길어지면 완료된 항목은 덜 관련 있어집니다. 시각적 비중을 줄여 사용자가 여전히 진행 중인 작업에 집중하게 하세요.
- 진행률 백분율을 표시하세요. "67% 완료" 같은 단일 숫자는 멀리서도 즉시 이해할 수 있습니다.
- 할 일 목록을 동기화 상태로 유지하세요.
stream.values가 반응형으로 갱신되므로 할 일 목록은 자동으로 최신 상태를 유지합니다. 수동 폴링이나 새로고침 로직을 추가하지 마세요.
관련 LangChain 가이드 (Related LangChain guides)
할 일 목록 패턴은 구조화된 에이전트 상태를 렌더링하는 더 넓은 LangChain 접근 방식의 특수화입니다. 이 가이드들은 계획 기반 에이전트와 잘 어울리는 관련 기법을 다룹니다:
더 알아보기 (Learn more)
- 이 문서를 MCP로 연결하면 Claude, VSCode 등에서 실시간 답변을 받을 수 있어요.
- GitHub에서 이 페이지 편집하기 또는 이슈 제출하기.