README.md

 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```