Files
pi_harness/packages/agent/learn/agent_workflow.md
T
ton bc56546b49
CI / build-check-test (push) Has been cancelled
update
2026-07-25 17:20:19 +07:00

38 KiB

Agent Workflow: Message Handling and Response Generation

Overview

This document explains the complete step-by-step flow of how the agent processes a user message and generates a response, from the moment a user asks "what product do you have in stock" to when the agent responds with an answer.


Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────────────┐
│                              USER INPUT                                              │
│  "what product do you have in stock?"                                               │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                           1. AGENT.PROMPT() ENTRY                                   │
│  File: packages/agent/src/agent.ts:339                                              │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ • Validate no active run (throws if busy)                                    │  │
│  │ • normalizePromptInput() converts string to AgentMessage[]                   │  │
│  │ • runPromptMessages() launches execution with lifecycle events              │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                     2. AGENT LIFECYCLE INITIALIZATION                                │
│  File: packages/agent/src/agent.ts:398-412                                          │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ Events Emitted:                                                               │  │
│  │ • agent_start                                                                 │  │
│  │ • turn_start                                                                  │  │
│  │ • message_start / message_end (for each prompt message)                      │  │
│  │                                                                                │  │
│  │ Context Snapshot Created:                                                     │  │
│  │ • systemPrompt                                                                │  │
│  │ • messages (copy)                                                             │  │
│  │ • tools (copy)                                                                │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                      3. AGENT LOOP STARTS                                            │
│  File: packages/agent/src/agent-loop.ts:95                                          │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ runPromptMessages() → runAgentLoop()                                          │  │
│  │ • Prompts added to context.messages                                           │  │
│  │ • Lifecycle events emitted for prompts                                        │  │
│  │ • Calls runLoop() (main processing loop)                                      │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                4. LLM CALL BOUNDARY - MESSAGE TRANSFORMATION                        │
│  File: packages/agent/src/agent-loop.ts:281-372 (streamAssistantResponse)          │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ Step 4.1: Context Transform (optional)                                        │  │
│  │   transformContext(messages) → transformed messages                           │  │
│  │   (Used for context pruning/injection)                                        │  │
│  │                                                                                │  │
│  │ Step 4.2: LLM Conversion                                                      │  │
│  │   convertToLlm(messages) → Message[]                                          │  │
│  │   - Filters non-LLM messages (bashExecution, branchSummary, etc.)            │  │
│  │   - Converts: user → user, assistant → assistant, toolResult → toolResult    │  │
│  │                                                                                │  │
│  │ Step 4.3: Build LLM Context                                                   │  │
│  │   {                                                                             │  │
│  │     systemPrompt: context.systemPrompt,                                       │  │
│  │     messages: llmMessages,                                                    │  │
│  │     tools: context.tools                                                       │  │
│  │   }                                                                             │  │
│  │                                                                                │  │
│  │ Step 4.4: Resolve API Key                                                     │  │
│  │   getApiKey(model.provider) → apiKey                                          │  │
│  │                                                                                │  │
│  │ Step 4.5: Stream Function Call                                                │  │
│  │   streamFunction(model, llmContext, options)                                  │  │
│  │   - Default: Models.streamSimple() from @earendil-works/pi-ai                │  │
│  │   - Makes actual LLM API call                                                │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                     5. LLM RESPONSE STREAMING                                       │
│  File: packages/agent/src/agent-loop.ts:317-371                                     │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ Stream yields events:                                                         │  │
│  │ • start          → creates partial AssistantMessage                          │  │
│  │ • text_start     → streaming text begins                                      │  │
│  │ • text_delta     → text chunks arrive                                         │  │
│  │ • toolcall_start → tool call block begins                                    │  │
│  │ • toolcall_delta → tool call arguments arrive                                │  │
│  │ • toolcall_end   → tool call block complete                                   │  │
│  │ • text_end       → text block complete                                        │  │
│  │ • done           → final message complete                                     │  │
│  │                                                                                │  │
│  │ State Updates:                                                                │  │
│  │ • Partial message pushed to context.messages                                 │  │
│  │ • message_start event emitted                                                │  │
│  │ • message_update events emitted as text/tools stream in                      │  │
│  │ • Final message committed to context.messages                                │  │
│  │ • message_end event emitted                                                  │  │
│  │                                                                                │  │
│  │ Stop Reasons:                                                                 │  │
│  │ • stop      - normal completion                                               │  │
│  │ • toolUse   - model requested tool calls                                      │  │
│  │ • length    - token limit reached                                             │  │
│  │ • error     - failure                                                         │  │
│  │ • aborted   - operation aborted                                               │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                  6. TOOL CALL PARSING AND EXECUTION                                  │
│  File: packages/agent/src/agent-loop.ts:408-554                                     │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ Step 6.1: Extract Tool Calls                                                  │  │
│  │   toolCalls = message.content.filter(c => c.type === "toolCall")            │  │
│  │                                                                                │  │
│  │ Step 6.2: Determine Execution Mode                                           │  │
│  │   - Check config.toolExecution: "parallel" or "sequential"                   │  │
│  │   - Check individual tool executionMode setting                              │  │
│  │   - Decides how to execute tool batch                                        │  │
│  │                                                                                │  │
│  │ Step 6.3: For Each Tool Call                                                  │  │
│  │   ┌──────────────────────────────────────────────────────────────────────┐   │  │
│  │   │ 1. Tool Lookup                                                      │   │  │
│  │   │    tool = context.tools.find(t => t.name === toolCall.name)         │   │  │
│  │   │                                                                     │   │  │
│  │   │ 2. Argument Preparation                                             │   │  │
│  │   │    prepared = tool.prepareArguments?(toolCall.arguments)            │   │  │
│  │   │                                                                     │   │  │
│  │   │ 3. Argument Validation                                              │   │  │
│  │   │    validateToolArguments(tool, preparedToolCall)                    │   │  │
│  │   │                                                                     │   │  │
│  │   │ 4. Before Tool Hook                                                 │   │  │
│  │   │    beforeToolCall({ assistantMessage, toolCall, args, context })    │   │  │
│  │   │    - Can block execution by returning { block: true, reason }      │   │  │
│  │   │                                                                     │   │  │
│  │   │ 5. Execution                                                       │   │  │
│  │   │    execute(toolCallId, params, signal, onUpdate, context)          │   │  │
│  │   │                                                                     │   │  │
│  │   │    - Parallel Mode: Tools execute concurrently                      │   │  │
│  │   │    - Sequential Mode: Tools execute one-by-one                     │   │  │
│  │   └──────────────────────────────────────────────────────────────────────┘   │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                7. TOOL EXECUTION EXAMPLE - READ TOOL                                │
│  File: packages/agent/src/harness/tools/read.ts                                     │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ User asks: "what product do you have in stock?"                              │  │
│  │                                                                                │  │
│  │ Agent decides to read catalog file "products.json"                           │  │
│  │                                                                                │  │
│  │ Input Arguments:                                                              │  │
│  │ {                                                                             │  │
│  │   "path": "products.json",                                                   │  │
│  │   "offset": 1,                                                               │  │
│  │   "limit": 100                                                               │  │
│  │ }                                                                             │  │
│  │                                                                                │  │
│  │ Execution Steps:                                                              │  │
│  │ 1. resolveReadToolPath(env, path, signal) → absolutePath                     │  │
│  │ 2. env.readBinaryFile(absolutePath, signal) → bytes                         │  │
│  │ 3. Detect mimeType (check if image)                                          │  │
│  │ 4. For text files:                                                           │  │
│  │    - Decode UTF-8 → textContent                                             │  │
│  │    - Split by lines → allLines                                             │  │
│  │    - Apply offset/limit slicing                                            │  │
│  │    - Truncate if exceeds DEFAULT_MAX_BYTES or DEFAULT_MAX_LINES           │  │
│  │    - Add truncation notice to output                                       │  │
│  │ 5. Return result:                                                            │  │
│  │    {                                                                         │  │
│  │      content: [{ type: "text", text: output }],                             │  │
│  │      details: { truncation: ... }                                           │  │
│  │    }                                                                         │  │
│  │                                                                                │  │
│  │ Output Example:                                                               │  │
│  │ "Showing lines 1-50 of 150. [Showing 50 lines of 150. Use offset=51 to      │  │
│  │ continue.]"                                                                   │  │
│  │                                                                              │  │
│  │ Tool Result:                                                                  │  │
│  │ {                                                                             │  │
│  │   "role": "toolResult",                                                       │  │
│  │   "toolCallId": "tool_abc123",                                               │  │
│  │   "toolName": "read",                                                         │  │
│  │   "content": [{ "type": "text", "text": "..." }],                            │  │
│  │   "isError": false,                                                           │  │
│  │   "timestamp": 1721721600000                                                  │  │
│  │ }                                                                             │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                  8. TOOL RESULT HANDLING                                            │
│  File: packages/agent/src/agent-loop.ts:556-792                                     │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ Step 8.1: After Tool Hook                                                     │  │
│  │   afterToolCall({ assistantMessage, toolCall, args, result, isError, ctx })  │  │
│  │   - Can override result content, details, usage, terminate hint             │  │
│  │                                                                                │  │
│  │ Step 8.2: Create Tool Result Message                                          │  │
│  │   {                                                                             │  │
│  │     role: "toolResult",                                                        │  │
│  │     toolCallId: toolCall.id,                                                   │  │
│  │     toolName: toolCall.name,                                                   │  │
│  │     content: result.content ?? [],                                             │  │
│  │     details: result.details,                                                   │  │
│  │     usage: result.usage,                                                       │  │
│  │     isError: false,                                                            │  │
│  │     timestamp: Date.now()                                                      │  │
│  │   }                                                                             │  │
│  │                                                                                │  │
│  │ Step 8.3: Emit Events                                                         │  │
│  │ • tool_execution_start                                                        │  │
│  │ • tool_execution_end                                                          │  │
│  │ • message_start (toolResult message)                                          │  │
│  │ • message_end (toolResult message)                                            │  │
│  │                                                                                │  │
│  │ Step 8.4: Update Context                                                      │  │
│  │ • Push tool result message to currentContext.messages                        │  │
│  │ • Push to newMessages array                                                   │  │
│  │                                                                                │  │
│  │ Step 8.5: Batch Termination Check                                             │  │
│  │   shouldTerminateToolBatch(finalizedCalls)                                   │  │
│  │   - Returns true if ALL tools set terminate: true                           │  │
│  │   - If true, agent may stop after this batch                                 │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                   9. NEXT TURN PREPARATION                                          │
│  File: packages/agent/src/agent-loop.ts:224-257                                     │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ Step 9.1: Turn End Event                                                      │  │
│  │   turn_end emitted with message and toolResults                              │  │
│  │                                                                                │  │
│  │ Step 9.2: prepareNextTurn Hook                                                │  │
│  │   prepareNextTurn({ message, toolResults, context, newMessages })            │  │
│  │   - Can return updated context, model, or thinking level                    │  │
│  │   - Used for dynamic context management                                      │  │
│  │                                                                                │  │
│  │ Step 9.3: Queue Polling                                                       │  │
│  │   getSteeringMessages() → inject messages for immediate processing           │  │
│  │   getFollowUpMessages() → check for queued follow-up messages                │  │
│  │                                                                                │  │
│  │ Step 9.4: Loop Decision                                                       │  │
│  │   • If steering messages exist → process them, continue loop                │  │
│  │   • If follow-up messages exist → process them, continue loop               │  │
│  │   • If tool calls remain in message → continue inner loop                   │  │
│  │   • If no messages → emit agent_end, exit loop                              │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘
                                            │
                                            ▼
┌─────────────────────────────────────────────────────────────────────────────────────┐
│                 10. AGENT RESPONSE GENERATION                                       │
│  File: packages/agent/src/agent-loop.ts:58-163                                      │
│  ┌──────────────────────────────────────────────────────────────────────────────┐  │
│  │ The agent loop continues until:                                              │  │
│  │ • No tool calls remain in assistant messages                                 │  │
│  │ • No steering/follow-up messages queued                                      │  │
│  │ • shouldStopAfterTurn() returns true (if configured)                        │  │
│  │                                                                                │  │
│  │ Final Response Generation:                                                    │  │
│  │ 1. LLM streams text content blocks                                           │  │
│  │ 2. Message committed to context                                              │  │
│  │ 3. turn_end emitted                                                          │  │
│  │ 4. agent_end emitted with all new messages                                    │  │
│  │ 5. Agent returns to idle state                                               │  │
│  │                                                                                │  │
│  │ Final Agent Response:                                                         │  │
│  │ "We have 15 products in stock:"                                              │  │
│  │ • Product A - $29.99                                                         │  │
│  │ • Product B - $49.99                                                         │  │
│  │ • Product C - $19.99                                                         │  │
│  │ (and 2 more products)                                                        │  │
│  └──────────────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────────────┘


Complete Event Flow Diagram

┌─────────────────────────────────────────────────────────────────────────────────────┐
│                    COMPLETE EVENT SEQUENCE FOR A TURN                              │
│                    WITH TOOL USE (Product Catalog Query)                           │
└─────────────────────────────────────────────────────────────────────────────────────┘

agent_start
    ↓
turn_start
    ↓
message_start  (user message: "what product do you have in stock?")
message_end    (user message)
    ↓
message_start  (assistant message - streaming from LLM)
message_update (text delta: "We have")
message_update (toolcall delta: {"name":"read","arguments":{...}})
message_update (toolcall end)
message_end    (assistant message with tool calls)
    ↓
tool_execution_start (tool call: read products.json)
tool_execution_end   (tool call: read complete)
message_start  (toolResult message)
message_end    (toolResult message)
    ↓
turn_end
    ↓
[Inner loop continues: send tool result to LLM]
    ↓
turn_start
    ↓
message_start  (assistant message - streaming from LLM)
message_update (text delta: "We have 15 products in stock:")
message_update (text delta: "• Product A - $29.99")
message_update (text delta: "• Product B - $49.99")
message_end    (final assistant message)
    ↓
turn_end
    ↓
agent_end

Key Files Summary

Component File Purpose
Agent class packages/agent/src/agent.ts Stateful wrapper, event emission, queue management
Agent loop packages/agent/src/agent-loop.ts Core loop, LLM calls, tool execution
Message types packages/agent/src/types.ts AgentMessage, AgentTool, AgentEvent definitions
Harness packages/agent/src/harness/agent-harness.ts Session integration, hooks, persistence
Read tool packages/agent/src/harness/tools/read.ts File reading implementation
Stream function packages/agent/src/stream-fn.ts Default stream function management
Types packages/agent/src/types.ts All type definitions

Hook Points for Customization

The agent supports multiple extension points:

Hook Location Purpose
convertToLlm agent.ts:99 Transform messages before LLM call
transformContext agent.ts:100 Modify context (pruning, injection)
beforeToolCall agent.ts:105 Block or modify tool execution
afterToolCall agent.ts:106 Override tool results
prepareNextTurn agent.ts:107 Dynamic context/model updates
shouldStopAfterTurn agent-loop.ts Graceful termination
getSteeringMessages agent.ts:114 Inject messages mid-turn
getFollowUpMessages agent.ts:115 Queue follow-up messages

Tool Execution Flow

Tool Call Received from LLM
    ↓
1. Tool Lookup (find by name)
    ↓
2. prepareArguments? (transform if defined)
    ↓
3. validateToolArguments (JSON Schema)
    ↓
4. beforeToolCall hook (can block)
    ↓
5. execute (parallel or sequential)
    ↓
6. onUpdate (stream partial results)
    ↓
7. afterToolCall hook (can override)
    ↓
8. Create toolResult message
    ↓
9. Emit events (start, end)
    ↓
10. Add to context.messages

Example: "What product do you have in stock?"

Step-by-Step Execution:

  1. User sends message

    "what product do you have in stock?"
    
  2. Agent normalizes input

    [{ 
      role: "user", 
      content: [{ type: "text", text: "what product do you have in stock?" }],
      timestamp: Date.now()
    }]
    
  3. LLM processes and decides to use read tool

    {
      "role": "assistant",
      "content": [{
        "type": "toolCall",
        "name": "read",
        "arguments": {
          "path": "products.json",
          "offset": 1,
          "limit": 50
        },
        "id": "tool_abc123"
      }]
    }
    
  4. Tool execution

    • Read products.json (150 lines total)
    • Return lines 1-50 with truncation notice
    • Add to context as toolResult
  5. LLM generates final response

    We have 15 products in stock:
    
    • Product A - $29.99
    • Product B - $49.99
    • Product C - $19.99
    • Product D - $99.99
    • Product E - $149.99
    (and 10 more products)
    
    Use offset=51 to continue viewing.
    
  6. Agent emits final response to user


Summary

The agent workflow follows a clear pattern:

  1. Message Input → Normalize and validate
  2. Context Setup → Create snapshot with system prompt, messages, tools
  3. LLM Call → Transform messages, resolve API key, stream response
  4. Tool Detection → Check for tool calls in assistant message
  5. Tool Execution → Validate, hook, execute, stream updates
  6. Result Handling → Create toolResult message, emit events
  7. Next Turn → Check for steering/follow-up messages, prepare context
  8. Response Generation → Continue until no more tool calls needed
  9. Completion → Emit final response to user

The entire flow is event-driven, allowing for real-time updates and hook-based customization at every step.