할 일 목록

할 일 목록 (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는 이를 반응형으로 렌더링합니다.

작업 계획은 선택 사항입니다. [`TodoListMiddleware`](https://reference.langchain.com/javascript/langchain/index/todoListMiddleware)가 없으면 `stream.values.todos`는 존재하지 않습니다. [작업 계획](/oss/javascript/deepagents/overview#task-planning)을 참고하세요.

흐름은 다음과 같습니다:

  1. 사용자가 요청을 제출합니다
  2. 에이전트가 계획을 만들고 상태에 todos를 채웁니다
  3. 에이전트가 각 todo를 실행하며 pendingin_progresscompleted로 전환합니다
  4. 에이전트가 진행됨에 따라 stream.values.todos가 실시간으로 갱신됩니다
  5. 여러분의 UI가 현재 상태로 할 일 목록을 다시 렌더링합니다

useStream 설정 (Setting up useStream)

에이전트에서 TodoListMiddleware를 활성화하세요.

```ts Google theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}} import { createDeepAgent } from "deepagents"; import { todoListMiddleware } from "langchain";

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를 읽으세요.

코드 예제는 타입 안전한 스트림 상태를 위해 `useStream`를 사용합니다. [Python](/oss/python/langchain/frontend/overview#type-inference) 또는 [JavaScript](/oss/javascript/langchain/frontend/overview#type-inference) 백엔드의 타입 추론을 참고하세요. ```tsx React theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}} import { useStream } from "@langchain/react";

const AGENT_URL = "http://localhost:2024";

export function TodoAgent() { const stream = useStream({ apiUrl: AGENT_URL, assistantId: "deep_agent_todo_list", });

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>
  );
}
할 일 목록은 `todos.length > 0`일 때만 표시하세요. 에이전트가 계획을 만들기 전에는 표시할 것이 없습니다. 빈 컴포넌트를 표시하면 공간만 낭비됩니다.

사용 사례 (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 접근 방식의 특수화입니다. 이 가이드들은 계획 기반 에이전트와 잘 어울리는 관련 기법을 다룹니다:

todos뿐 아니라 모든 구조화된 에이전트 상태를 일반 텍스트 대신 커스텀 UI 컴포넌트로 렌더링하세요. 페이지 새로고침이나 탭 전환 후에도 진행 상황을 잃지 않고 실행 중인 계획에 다시 연결하세요. 에이전트가 현재 계획을 처리하는 동안 후속 작업을 큐에 넣으세요. 에이전트가 사용자 승인 또는 입력이 필요할 때 계획 실행을 일시 중지하고 재개하세요.

더 알아보기 (Learn more)