X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/5da3ea038d1e9f9a2634d5ab137aef6989a3a789..e0ab6220b6fd35f7e92a04e4bf149940185713ab:/turn.go diff --git a/turn.go b/turn.go index 76f524e..91723d2 100644 --- a/turn.go +++ b/turn.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "path/filepath" "strings" "sync" "time" @@ -30,6 +31,13 @@ type Event struct { // 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 @@ -42,6 +50,8 @@ type loop struct { memory *Memory client modelClient tools *toolbox + layout *viewLayout + warmUp bool clock func() time.Time notify func(Event) wake chan struct{} @@ -66,13 +76,21 @@ type functionCallOutput 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), @@ -84,7 +102,7 @@ func newLoop(agent *Agent, memory *Memory, client modelClient, clock func() time workdir: agent.config.Workdir, } - return l + return l, nil } // Submit logs a user message and wakes the loop. The message joins the next @@ -144,15 +162,24 @@ func (l *loop) Flush(ctx context.Context) error { 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 { @@ -246,14 +273,20 @@ func (l *loop) chain(ctx context.Context) error { 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 @@ -332,21 +365,65 @@ func (l *loop) handle(ctx context.Context, input []any, response Response) ([]an 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 "\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