{"meta":{"title":"The agent loop","intro":"How the Copilot CLI processes a user message end-to-end: from prompt to session.idle.","product":"GitHub Copilot","breadcrumbs":[{"href":"/en/enterprise-cloud@latest/copilot","title":"GitHub Copilot"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos","title":"How-tos"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk","title":"Copilot SDK"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features","title":"Features"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/agent-loop","title":"Agent Loop"}],"documentType":"article"},"body":"# The agent loop\n\nHow the Copilot CLI processes a user message end-to-end: from prompt to session.idle.\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\n## Architecture\n\n![Diagram: Graph diagram showing the described process.](/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-0.png)\n\nThe **SDK** is a transport layer—it sends your prompt to the **Copilot CLI** over JSON-RPC and surfaces events back to your app. The **CLI** is the orchestrator that runs the agentic tool-use loop, making one or more LLM API calls until the task is done.\n\n## The tool-use loop\n\nWhen you call `session.send({ prompt })`, the CLI enters a loop:\n\n![Diagram: Flowchart showing the described process.](/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-1.png)\n\nThe model sees the **full conversation history** on each call—system prompt, user message, and all prior tool calls and results.\n\n**Key insight:** Each iteration of this loop is exactly one LLM API call, visible as one `assistant.turn_start` / `assistant.turn_end` pair in the event log. There are no hidden calls.\n\n## Turns—what they are\n\nA **turn** is a single LLM API call and its consequences:\n\n1. The CLI sends the conversation history to the LLM\n2. The LLM responds (possibly with tool requests)\n3. If tools were requested, the CLI executes them\n4. `assistant.turn_end` is emitted\n\nA single user message typically results in **multiple turns**. For example, a question like \"how does X work in this codebase?\" might produce:\n\n| Turn | What the model does                            | toolRequests?    |\n| ---- | ---------------------------------------------- | ---------------- |\n| 1    | Calls `grep` and `glob` to search the codebase | ✅ Yes            |\n| 2    | Reads specific files based on search results   | ✅ Yes            |\n| 3    | Reads more files for deeper context            | ✅ Yes            |\n| 4    | Produces the final text answer                 | ❌ No → loop ends |\n\nThe model decides on each turn whether to request more tools or produce a final answer. Each call sees the **full accumulated context** (all prior tool calls and results), so it can make an informed decision about whether it has enough information.\n\n## Event flow for a multi-turn interaction\n\n![Diagram: Flowchart showing the described process.](/assets/images/help/copilot/copilot-sdk/features-agent-loop-diagram-2.png)\n\n## Who triggers each turn?\n\n| Actor           | Responsibility                                                                            |\n| --------------- | ----------------------------------------------------------------------------------------- |\n| **Your app**    | Sends the initial prompt via `session.send()`                                             |\n| **Copilot CLI** | Runs the tool-use loop—executes tools and feeds results back to the LLM for the next turn |\n| **LLM**         | Decides whether to request tools (continue looping) or produce a final response (stop)    |\n| **SDK**         | Passes events through; does not control the loop                                          |\n\nThe CLI is purely mechanical: \"model asked for tools → execute → call model again.\" The **model** is the decision-maker for when to stop.\n\n## `session.idle` vs `session.task_complete`\n\nThese are two different completion signals with very different guarantees:\n\n### `session.idle`\n\n* **Always emitted** when the tool-use loop ends\n* **Ephemeral**: not persisted to disk, not replayed on session resume\n* Means: \"the agent has stopped processing and is ready for the next message\"\n* **Use this** as your reliable \"done\" signal\n\nThe SDK's `sendAndWait()` method waits for this event:\n\n```typescript\n// Blocks until session.idle fires\nconst response = await session.sendAndWait({ prompt: \"Fix the bug\" });\n```\n\n### `session.task_complete`\n\n* **Optionally emitted**: requires the model to explicitly signal it\n* **Persisted**: saved to the session event log on disk\n* Means: \"the agent considers the overall task fulfilled\"\n* Carries an optional `summary` field\n\n```typescript\nsession.on(\"session.task_complete\", (event) => {\n    console.log(\"Task done:\", event.data.summary);\n});\n```\n\n### Autopilot mode: the CLI nudges for `task_complete`\n\nIn **autopilot mode** (headless/autonomous operation), the CLI actively tracks whether the model has called `task_complete`. If the tool-use loop ends without it, the CLI injects a synthetic user message nudging the model:\n\n> *\"You have not yet marked the task as complete using the task\\_complete tool. If you were planning, stop planning and start implementing. You aren't done until you have fully completed the task.\"*\n\nThis effectively restarts the tool-use loop—the model sees the nudge as a new user message and continues working. The nudge also instructs the model **not** to call `task_complete` prematurely:\n\n* Don't call it if you have open questions—make decisions and keep working\n* Don't call it if you hit an error—try to resolve it\n* Don't call it if there are remaining steps—complete them first\n\nThis creates a **two-level completion mechanism** in autopilot:\n\n1. The model calls `task_complete` with a summary → CLI emits `session.task_complete` → done\n2. The model stops without calling it → CLI nudges → model continues or calls `task_complete`\n\n### Why `task_complete` might not appear\n\nIn **interactive mode** (normal chat), the CLI does not nudge for `task_complete`. The model may skip it entirely. Common reasons:\n\n* **Conversational Q\\&A**: The model answers a question and simply stops—there's no discrete \"task\" to complete\n* **Model discretion**: The model produces a final text response without calling the task-complete signal\n* **Interrupted sessions**: The session ends before the model reaches a completion point\n\nThe CLI emits `session.idle` regardless, because it's a mechanical signal (the loop ended), not a semantic one (the model thinks it's done).\n\n### Which should you use?\n\n| Use case                                  | Signal                                |\n| ----------------------------------------- | ------------------------------------- |\n| \"Wait for the agent to finish processing\" | `session.idle` ✅                      |\n| \"Know when a coding task is done\"         | `session.task_complete` (best-effort) |\n| \"Timeout/error handling\"                  | `session.idle` + `session.error` ✅    |\n\n## Counting LLM calls\n\nThe number of `assistant.turn_start` / `assistant.turn_end` pairs in the event log equals the total number of LLM API calls made. There are no hidden calls for planning, evaluation, or completion checking.\n\nTo inspect turn count for a session:\n\n```bash\n# Count turns in a session's event log\ngrep -c \"assistant.turn_start\" ~/.copilot/session-state/<sessionId>/events.jsonl\n```\n\n## Further reading\n\n* [Streaming session events](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/streaming-events): Full field-level reference for every event type\n* [Session resume and persistence](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/session-persistence): How sessions are saved and resumed\n* [Working with hooks](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/hooks): Intercepting events in the loop (permissions, tools)"}