13 // modelClient sends model calls.
14 type modelClient interface {
15 Create(ctx context.Context, request Request) (Response, error)
18 // Event tells the clients what the agent does as it happens. Kind is the kind
19 // of a logged message (user, reply, tool, echo), or one of busy, idle, and
20 // error. ID is the ID of a logged message.
22 Kind string `json:"kind"`
23 ID int `json:"id,omitempty"`
24 Text string `json:"text,omitempty"`
27 // loop runs the turns of one agent. A user message goes to the log when it
28 // comes, and it wakes the loop. A turn waits until every message before it is
29 // summarized. Then its first request holds the prompt, the tools, and the
30 // input of turnInput. The loop runs the tool calls of each response and sends
31 // their results back, until a response has no tool call: the final answer.
33 // Each later request of a turn begins with the whole input of the request
34 // before it, then its output items, then the tool results. The output items
35 // hold the reasoning with its encrypted content, because the calls do not
36 // store it. So each request begins with all of the previous one and reads it
37 // from the prompt cache. User messages that come during a turn join the next
38 // request after the tool results. The replies, the tool calls, and the results
39 // go to the log as they happen. The thoughts never do.
45 clock func() time.Time
51 cancel context.CancelFunc
54 // functionCall holds the fields of a function call output item.
55 type functionCall struct {
56 Type string `json:"type"`
57 Name string `json:"name"`
58 Arguments string `json:"arguments"`
59 CallID string `json:"call_id"`
62 // functionCallOutput is the input item with the result of a function call.
63 type functionCallOutput struct {
64 Type string `json:"type"`
65 CallID string `json:"call_id"`
66 Output string `json:"output"`
69 // newLoop returns the loop of agent with its memory. Its calls go through
70 // client, and notify gets each event. The clock gives the date of a turn.
71 func newLoop(agent *Agent, memory *Memory, client modelClient, clock func() time.Time, notify func(Event)) *loop {
78 wake: make(chan struct{}, 1),
84 workdir: agent.config.Workdir,
90 // Submit logs a user message and wakes the loop. The message joins the next
91 // request: the first one of a new turn, or the next one of a running turn.
92 func (l *loop) Submit(text string) (Message, error) {
93 message, err := l.Append("user", text)
99 l.pending = append(l.pending, message)
103 case l.wake <- struct{}{}:
110 // Interrupt ends the running turn. A running tool is killed, and the turn sends
111 // no further request.
112 func (l *loop) Interrupt() bool {
114 defer l.mutex.Unlock()
125 // Append logs a message and tells the clients. The toolbox logs its calls and
126 // results through it.
127 func (l *loop) Append(kind, text string) (Message, error) {
128 message, err := l.memory.Append(kind, text)
130 return Message{}, err
142 // Flush makes the log durable.
143 func (l *loop) Flush(ctx context.Context) error {
144 return l.memory.Flush(ctx)
147 // Run runs a turn each time user messages wait, until ctx ends. It returns the
148 // error of a failed log write, which stops the agent.
149 func (l *loop) Run(ctx context.Context) error {
166 // waiting tells if user messages wait for a request.
167 func (l *loop) waiting() bool {
169 defer l.mutex.Unlock()
171 return len(l.pending) > 0
174 // take returns the user messages that wait for a request, and removes them.
175 func (l *loop) take() []Message {
177 defer l.mutex.Unlock()
179 messages := l.pending
185 // turn runs one turn. A failed or interrupted model call ends the turn and
186 // tells the clients. The messages of the turn stay in the log and the view, and
187 // the next turn starts only with a new message, so a lasting failure such as a
188 // needed login does not repeat. Only a failed log write is an error of turn.
189 func (l *loop) turn(ctx context.Context) error {
190 ctx, cancel := context.WithCancel(ctx)
208 if err != nil && !errors.Is(err, errLogWrite) {
211 Text: turnError(ctx, err),
224 // errLogWrite marks an error of a log write, which ends the loop.
225 var errLogWrite = errors.New("log write failed")
227 // chain runs the request chain of a turn until the final answer.
228 func (l *loop) chain(ctx context.Context) error {
230 if len(messages) == 0 {
234 err := l.memory.Wait(ctx)
239 instructions, err := l.agent.Prompt()
244 skip := make(map[int]bool, len(messages))
245 for _, message := range messages {
246 skip[message.ID] = true
249 input := turnInput(l.memory.ViewWithout(skip), l.state(), messages)
251 response, err := l.client.Create(ctx, Request{
252 Model: l.agent.config.Model,
253 Effort: l.agent.config.Effort,
254 Instructions: instructions,
262 input, err = l.handle(ctx, input, response)
273 // handle logs the replies of response and runs its tool calls. It returns the
274 // input of the next request: input, the output items, the tool results, and
275 // the user messages that came in the meantime. After the final answer it
277 func (l *loop) handle(ctx context.Context, input []any, response Response) ([]any, error) {
279 for _, raw := range response.Output {
280 item, err := inputItem(raw)
285 input = append(input, item)
287 var call functionCall
288 err = json.Unmarshal(raw, &call)
290 return nil, fmt.Errorf("decode output item: %w", err)
295 text, err := outputText([]json.RawMessage{raw})
300 text = strings.TrimSpace(text)
305 _, err = l.Append("reply", text)
307 return nil, fmt.Errorf("%w: %w", errLogWrite, err)
309 case "function_call":
310 output, err := l.tools.Call(ctx, call.Name, call.Arguments)
312 return nil, fmt.Errorf("%w: %w", errLogWrite, err)
315 outputs = append(outputs, functionCallOutput{
316 Type: "function_call_output",
323 if len(outputs) == 0 {
327 input = append(input, outputs...)
328 for _, message := range l.take() {
329 input = append(input, userMessage(message.Text))
335 // state returns the per-turn state: the date and time of the turn.
336 func (l *loop) state() string {
337 return "Now: " + l.clock().Format("Monday 2006-01-02 15:04 -07:00")
340 // turnInput returns the input of the first request of a turn: one user message
341 // with the view, the per-turn state, and each new message whole. The view
342 // leaves out the leaf lines of the new messages, which come whole after it.
343 func turnInput(view, state string, messages []Message) []any {
344 parts := []string{view + "\n", state + "\n"}
345 for _, message := range messages {
346 parts = append(parts, message.Text)
349 return []any{userMessage(parts...)}
352 // inputItem returns an output item as an input item for the next request. The
353 // calls do not store their items, so the item goes back whole, without its id.
354 func inputItem(raw json.RawMessage) (map[string]any, error) {
355 var item map[string]any
356 err := json.Unmarshal(raw, &item)
358 return nil, fmt.Errorf("decode output item: %w", err)
366 // turnError returns the text of the error that ended a turn.
367 func turnError(ctx context.Context, err error) string {
368 if ctx.Err() != nil {