Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Agentic loop implementation #65

Description

@Unshure

Agentic Loop Implementation

Create an async iterator agent loop that coordinates execution between model providers and tools. The event loop manages the conversation flow by streaming model responses, executing tools when needed, and continuing until completion.

Implementation Requirements

Based on repository analysis and clarification discussions, this task involves implementing a core agent loop function without the full Agent class (which will come later).

Architecture Overview

The agent loop follows a simple top-level loop pattern that calls out to different steps:

  1. Model invocation - Send messages to model provider and stream responses
  2. Tool execution - Execute tools when model requests them
  3. Loop continuation - Return to model invocation with tool results

Reference: Strands Agent Loop Documentation

Technical Specifications

1. Agent Loop Function (src/agent_loop.ts)

Create a new file src/agent_loop.ts with:

async function* agent_loop(
  messages: Message[],
  toolRegistry: ToolRegistry,
  systemPrompt: SystemPrompt | undefined,
  modelProvider: Model<BaseModelConfig>
): AsyncGenerator<AgentStreamEvent, Message[], never>

Key behaviors:

  • Returns an async generator that yields AgentStreamEvent objects
  • Returns final list of messages when complete
  • Does NOT create an Agent class (that comes later)
  • Uses existing ToolRegistry class from src/tools/registry.ts

2. Event Types (src/agent/streaming.ts or in agent_loop.ts)

Create AgentStreamEvent type that includes:

  • All ModelStreamEvent types (passthrough from model provider)
  • Additional agent-specific events:
    • BeforeModelEvent - Before invoking model
    • AfterModelEvent - After model completes
    • BeforeToolsEvent - Before executing tools
    • AfterToolsEvent - After tools complete
    • BeforeInvocationEvent - Start of agent loop iteration
    • AfterInvocationEvent - End of agent loop iteration

3. Message Handling (Transactional)

Critical requirement: Do NOT add user messages to the messages array until the model provider returns its first event.

  • Wait for first event from model provider
  • Only then add the user message to the messages array
  • If model provider throws error before first event, do NOT add message
  • Return the complete messages array at the end

Reference: PR #57 contains guidance on this pattern (ContentBlock construction)

4. Model Provider Invocation

  • Call modelProvider.stream(messages, options) with:
    • Current messages array
    • toolSpecs extracted from ToolRegistry using toolRegistry.list().map(t => t.toolSpec)
    • systemPrompt if provided
  • Stream all ModelStreamEvent objects to the caller (yield each one)
  • Accumulate content blocks from streaming events to construct response message

5. ContentBlock Construction

  • Use patterns from PR Issue #56: Collect Model Streamed Events Implementation #57 as guidance
  • Accumulate streaming deltas to build complete ContentBlocks:
    • textDeltaTextBlock
    • toolUseStart + toolUseInputDeltaToolUseBlock
    • reasoningDeltaReasoningBlock
  • Construct assistant message with all content blocks at end of stream

6. Stop Reason Detection & Tool Execution

When stopReason === 'toolUse':

  • Extract all ToolUseBlock objects from the assistant message
  • Execute tools sequentially (one at a time, not in parallel)
  • For each tool:
    1. Look up tool in registry: toolRegistry.get(toolName)
    2. Execute: tool.stream({ toolUse, invocationState: {} })
    3. Yield all ToolStreamEvent objects in real-time
    4. Collect the final ToolResult from the generator
    5. Create a ToolResultBlock and add to messages array
  • After all tools executed, create user message with all ToolResultBlocks
  • Continue the loop (invoke model again)

When stopReason !== 'toolUse':

  • Yield a final AfterInvocationEvent
  • Return the complete messages array
  • Terminate the loop

7. Error Handling

Model provider errors (before first event):

  • Throw immediately
  • Do NOT add user message to messages array

Model provider errors (after first event):

  • Propagate the error
  • Messages array state is preserved up to error point

Tool execution errors:

  • If tool throws exception, re-raise it (potentially with additional context)
  • Note: Well-behaved tools should NOT throw - they should return ToolResult with status: 'error'

MaxTokens stop reason:

  • Throw an error (this is an unrecoverable state)
  • Should throw MaxTokensReachedException or similar

8. Loop Termination

The loop terminates when:

  • stopReason !== 'toolUse' (normal completion)
  • stopReason === 'maxTokens' (error - throw exception)
  • Any unhandled exception occurs (error propagation)

Dependencies & Integration

Existing code to use:

  • src/tools/registry.ts - ToolRegistry class for managing tools
  • src/models/model.ts - Model interface for model providers
  • src/models/streaming.ts - ModelStreamEvent types
  • src/types/messages.ts - Message, ContentBlock, SystemPrompt types
  • src/tools/tool.ts - Tool interface and ToolStreamEvent
  • src/tools/types.ts - ToolSpec, ToolUse, ToolResult types

Model provider compatibility:

  • Must work with both BedrockModel and OpenAIModel
  • Use the base Model interface, not provider-specific implementations
  • No provider-specific handling needed

Testing Requirements

Unit Tests (src/agent/__tests__/agent_loop.test.ts)

Create comprehensive unit tests using:

  • Mock model provider - Create if one doesn't exist in src/models/__tests__/test-utils.ts
  • Mock tools - Create helper for generating mock Tool implementations if needed
  • Test scenarios:
    1. Simple completion without tools (endTurn stop reason)
    2. Single tool use cycle (toolUse → execute → continue → endTurn)
    3. Multiple tool uses in sequence
    4. Multiple agentic loop iterations
    5. Transactional message handling (message not added on early error)
    6. Error propagation from model provider
    7. Error propagation from tool execution
    8. MaxTokens stop reason (should throw)
    9. Event streaming (verify all events yielded)
    10. ContentBlock construction from streaming events

Integration Tests

NOT required for this task - Will be added later when Agent class is implemented

File Structure

src/
├── agent_loop.ts              # Main agent loop function (NEW)
├── agent/
│   ├── streaming.ts           # AgentStreamEvent types (NEW)
│   └── __tests__/
│       └── agent_loop.test.ts # Comprehensive unit tests (NEW)
├── models/
│   └── __tests__/
│       └── test-utils.ts      # Add mock model provider if needed
└── tools/
    └── __tests__/
        └── test-utils.ts      # Add mock tool helper if needed (NEW)

Acceptance Criteria

  • agent_loop function implemented with correct signature
  • AgentStreamEvent type defined with all required event types
  • Transactional message handling: messages not added before first model event
  • Model provider streaming events yielded to caller
  • ContentBlocks correctly constructed from streaming events
  • Tool execution triggered on toolUse stop reason
  • Tools executed sequentially with real-time event streaming
  • Tool results added to messages and loop continues
  • Loop terminates correctly on non-toolUse stop reasons
  • MaxTokens stop reason throws error
  • Error handling for model provider errors
  • Error handling for tool execution errors
  • Unit tests cover all major scenarios
  • 80%+ test coverage maintained
  • All tests pass
  • Code follows TypeScript coding patterns from AGENTS.md
  • TSDoc documentation for all exported functions
  • No linting errors
  • Types exported from src/index.ts

Notes & Future Considerations

Extensibility considerations (do NOT implement now):

  • Allow entry point to jump directly into tool-execution (based on if last message was a toolUse)
  • This will be implemented later but keep in mind during design

Agent class (do NOT implement now):

  • Full Agent class will be implemented in a separate task
  • This task focuses only on the core loop mechanism

Integration tests (do NOT implement now):

  • Integration tests with real model providers will be added after Agent class exists

Repository Documentation Updates

After implementation, update:

  • AGENTS.md - Add agent loop to directory structure if new directories created
  • README.md - Update development status to mark agent loop as complete

Original Context

Create an async iterator agent loop that coordinates execution between model providers and tools. The event loop manages the conversation flow by streaming model responses, executing tools when needed, and continuing until completion.

Exit Criteria

A working agent loop async iterator that coordinates model provider streaming and tool execution, properly constructs ContentBlocks from responses, handles tool_use cycles, streams all events back to the caller, and passes comprehensive unit tests.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Language

None yet

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions