1# Loop Package
2
3The `loop` package provides the core agentic conversation loop for Shelley,
4handling LLM interactions, tool execution, and message recording.
5
6## Features
7
8- **LLM Integration**: Works with any LLM service implementing the `llm.Service` interface
9- **Predictable Testing**: Includes a `PredictableService` for deterministic testing
10- **Tool Execution**: Automatically executes tools called by the LLM
11- **Message Recording**: Records all conversation messages via a configurable function
12- **Usage Tracking**: Tracks token usage and costs across all LLM calls
13- **Context Cancellation**: Gracefully handles context cancellation
14- **Thread Safety**: All methods are safe for concurrent use
15
16## Basic Usage
17
18```go
19// Create tools (using claudetool package or custom tools)
20tools := []*llm.Tool{bashTool, patchTool, thinkTool}
21
22// Define message recording function (typically saves to the database)
23recordMessage := func(ctx context.Context, message llm.Message, usage llm.Usage) error {
24 return messageService.Create(ctx, db.CreateMessageParams{
25 ConversationID: conversationID,
26 Type: getMessageType(message.Role),
27 LLMData: message,
28 UsageData: usage,
29 })
30}
31
32// Create loop with explicit LLM configuration
33agentLoop := loop.NewLoop(loop.Config{
34 LLM: &ant.Service{APIKey: apiKey},
35 History: history, // existing conversation history
36 Tools: tools,
37 RecordMessage: recordMessage,
38 Logger: logger,
39 System: systemPrompt, // []llm.SystemContent
40})
41
42// Queue user messages for the current turn
43agentLoop.QueueUserMessage(llm.UserStringMessage("Hello, please help me with something"))
44
45// Run the conversation turn
46ctx := context.Background()
47if err := agentLoop.ProcessOneTurn(ctx); err != nil {
48 log.Fatalf("conversation failed: %v", err)
49}
50```
51
52## Testing with PredictableService
53
54The `PredictableService` records requests and returns deterministic responses that are convenient for tests:
55
56```go
57service := loop.NewPredictableService()
58
59testLoop := loop.NewLoop(loop.Config{
60 LLM: service,
61 RecordMessage: func(context.Context, llm.Message, llm.Usage) error { return nil },
62})
63
64testLoop.QueueUserMessage(llm.UserStringMessage("hello"))
65if err := testLoop.ProcessOneTurn(context.Background()); err != nil {
66 t.Fatalf("loop failed: %v", err)
67}
68
69last := service.GetLastRequest()
70require.NotNil(t, last)
71```