Tool, observe, next call

A real task might need several tool calls. The model reads one file, then decides it needs another, then wants to write something. The agent loop is the piece that keeps the conversation going until the model stops asking for tools.

agent/agent.go
go
func (a *Agent) Run(userMsg string) (string, error) {
	messages := []Message{
		{Role: "system", Content: a.System},
		{Role: "user", Content: userMsg},
	}

	for iter := 0; iter < a.MaxIters; iter++ {
		resp, err := a.LLM.Chat(ChatRequest{
			Messages: messages,
			Tools:    a.specs(),
		})
		if err != nil {
			return "", err
		}
		choice := resp.Choices[0]
		messages = append(messages, choice.Message)

		if len(choice.Message.ToolCalls) > 0 {
			// dispatch, append tool results, continue
			continue
		}
		if choice.FinishReason == "stop" || choice.Message.Content != "" {
			return strings.TrimSpace(choice.Message.Content), nil
		}
	}
	return "", fmt.Errorf("agent: exceeded max iterations (%d)", a.MaxIters)
}

The loop is boring and short. Every iteration sends the current conversation, appends whatever the model produces, dispatches tools if any, and returns when the model stops calling tools.

Two kinds of turns inside Run()

Every iteration either dispatches tools or returns the final answer.

Quiz: Quiz

Loading practice…