"encoding/json"
"errors"
"fmt"
+ "path/filepath"
"strings"
"sync"
"time"
// input of turnInput. The loop runs the tool calls of each response and sends
// their results back, until a response has no tool call: the final answer.
//
+// The view goes out as the messages of a viewLayout. After the turns, when no
+// message waits, a warm-up request sends only the prompt, the tools, and the
+// view messages, and stops at its first output item. It writes a cache entry
+// at the end of the view, where the next turn has a message end, so the first
+// request of that turn reads the view from the cache. The warm-up is not
+// logged, and its output is not used.
+//
// Each later request of a turn begins with the whole input of the request
// before it, then its output items, then the tool results. The output items
// hold the reasoning with its encrypted content, because the calls do not
memory *Memory
client modelClient
tools *toolbox
+ layout *viewLayout
+ warmUp bool
clock func() time.Time
notify func(Event)
wake chan struct{}
Output string `json:"output"`
}
-// newLoop returns the loop of agent with its memory. Its calls go through
-// client, and notify gets each event. The clock gives the date of a turn.
-func newLoop(agent *Agent, memory *Memory, client modelClient, clock func() time.Time, notify func(Event)) *loop {
+// newLoop returns the loop of agent with its memory, with the warm-up on. Its
+// calls go through client, and notify gets each event. The clock gives the
+// date of a turn.
+func newLoop(agent *Agent, memory *Memory, client modelClient, clock func() time.Time, notify func(Event)) (*loop, error) {
+ layout, err := loadLayout(filepath.Join(agent.dir, layoutFileName))
+ if err != nil {
+ return nil, err
+ }
+
l := &loop{
agent: agent,
memory: memory,
client: client,
+ layout: layout,
+ warmUp: true,
clock: clock,
notify: notify,
wake: make(chan struct{}, 1),
workdir: agent.config.Workdir,
}
- return l
+ return l, nil
}
// Submit logs a user message and wakes the loop. The message joins the next
return l.memory.Flush(ctx)
}
-// Run runs a turn each time user messages wait, until ctx ends. It returns the
-// error of a failed log write, which stops the agent.
+// Run runs a turn each time user messages wait, and a warm-up after the turns,
+// until ctx ends. It returns the error of a failed log write, which stops the
+// agent. A failed warm-up only costs the cache of the next turn, so its error
+// stays in the usage file.
func (l *loop) Run(ctx context.Context) error {
for {
+ turned := false
for l.waiting() {
err := l.turn(ctx)
if err != nil {
return err
}
+
+ turned = true
+ }
+
+ if turned && l.warmUp {
+ _ = l.warm(ctx)
}
select {
skip[message.ID] = true
}
- input := turnInput(l.memory.ViewWithout(skip), l.state(), messages)
- for {
+ view, err := l.layout.Split(l.memory.ViewLines(skip))
+ if err != nil {
+ return err
+ }
+
+ input := turnInput(view, l.state(), messages)
+ for turn := "first"; ; turn = "next" {
response, err := l.client.Create(ctx, Request{
Model: l.agent.config.Model,
Effort: l.agent.config.Effort,
Instructions: instructions,
Tools: agentTools(),
Input: input,
+ Turn: turn,
})
if err != nil {
return err
return input, nil
}
-// state returns the per-turn state: the date and time of the turn.
+// warm sends the warm-up request, when the builds are done and no message
+// waits.
+func (l *loop) warm(ctx context.Context) error {
+ err := l.memory.Wait(ctx)
+ if err != nil || l.waiting() {
+ return err
+ }
+
+ instructions, err := l.agent.Prompt()
+ if err != nil {
+ return err
+ }
+
+ view, err := l.layout.Split(l.memory.ViewLines(nil))
+ if err != nil {
+ return err
+ }
+
+ _, err = l.client.Create(ctx, Request{
+ Model: l.agent.config.Model,
+ Effort: l.agent.config.Effort,
+ Instructions: instructions,
+ Tools: agentTools(),
+ Input: warmUpInput(view),
+ Turn: "warmup",
+ StopAtOutput: true,
+ })
+
+ return err
+}
+
+// state returns the per-turn state: the end of the view, and the date and time
+// of the turn.
func (l *loop) state() string {
- return "Now: " + l.clock().Format("Monday 2006-01-02 15:04 -07:00")
+ return "</chat>\n\nNow: " + l.clock().Format("Monday 2006-01-02 15:04 -07:00")
}
-// turnInput returns the input of the first request of a turn: one user message
-// with the view, the per-turn state, and each new message whole. The view
-// leaves out the leaf lines of the new messages, which come whole after it.
-func turnInput(view, state string, messages []Message) []any {
- parts := []string{view + "\n", state + "\n"}
+// turnInput returns the input of the first request of a turn, stable parts
+// first: a user message for each view message, one with the per-turn state,
+// and one for each new message, whole. The view leaves out the leaf lines of
+// the new messages.
+func turnInput(view []string, state string, messages []Message) []any {
+ input := warmUpInput(view)
+ input = append(input, userMessage(state))
for _, message := range messages {
- parts = append(parts, message.Text)
+ input = append(input, userMessage(message.Text))
+ }
+
+ return input
+}
+
+// warmUpInput returns a user message for each view message.
+func warmUpInput(view []string) []any {
+ input := make([]any, len(view))
+ for n, text := range view {
+ input[n] = userMessage(text)
}
- return []any{userMessage(parts...)}
+ return input
}
// inputItem returns an output item as an input item for the next request. The