X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/e0ab6220b6fd35f7e92a04e4bf149940185713ab..d206de3a2e839ffbda9cbdb10a3ed93c4915fce3:/turn.go diff --git a/turn.go b/turn.go index 91723d2..0b2b29e 100644 --- a/turn.go +++ b/turn.go @@ -17,12 +17,15 @@ type modelClient interface { } // Event tells the clients what the agent does as it happens. Kind is the kind -// of a logged message (user, reply, tool, echo), or one of busy, idle, and -// error. ID is the ID of a logged message. +// of a logged message (user, reply, tool, echo), or one of ready, busy, idle, +// and error. ID is the ID of a logged message. Source names the client that +// sent a user message, such as terminal or telegram, so that each client can +// show the messages of the others. type Event struct { - Kind string `json:"kind"` - ID int `json:"id,omitempty"` - Text string `json:"text,omitempty"` + Kind string `json:"kind"` + ID int `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Source string `json:"source,omitempty"` } // loop runs the turns of one agent. A user message goes to the log when it @@ -31,12 +34,8 @@ 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. +// The view goes out as the messages of a viewLayout, so the first request of +// a turn reads the view of the turn before from the prompt cache. // // 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 @@ -51,7 +50,6 @@ type loop struct { client modelClient tools *toolbox layout *viewLayout - warmUp bool clock func() time.Time notify func(Event) wake chan struct{} @@ -76,9 +74,8 @@ type functionCallOutput struct { Output string `json:"output"` } -// 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. +// 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, error) { layout, err := loadLayout(filepath.Join(agent.dir, layoutFileName)) if err != nil { @@ -90,7 +87,6 @@ func newLoop(agent *Agent, memory *Memory, client modelClient, clock func() time memory: memory, client: client, layout: layout, - warmUp: true, clock: clock, notify: notify, wake: make(chan struct{}, 1), @@ -105,10 +101,11 @@ func newLoop(agent *Agent, memory *Memory, client modelClient, clock func() time return l, nil } -// Submit logs a user message and wakes the loop. The message joins the next -// request: the first one of a new turn, or the next one of a running turn. -func (l *loop) Submit(text string) (Message, error) { - message, err := l.Append("user", text) +// Submit logs a user message from the client source and wakes the loop. The +// message joins the next request: the first one of a new turn, or the next one +// of a running turn. +func (l *loop) Submit(source, text string) (Message, error) { + message, err := l.record("user", source, text) if err != nil { return Message{}, err } @@ -140,18 +137,33 @@ func (l *loop) Interrupt() bool { return true } +// Busy tells if a turn runs. +func (l *loop) Busy() bool { + l.mutex.Lock() + defer l.mutex.Unlock() + + return l.cancel != nil +} + // Append logs a message and tells the clients. The toolbox logs its calls and // results through it. func (l *loop) Append(kind, text string) (Message, error) { + return l.record(kind, "", text) +} + +// record logs a message and tells the clients, with the client source of a user +// message. +func (l *loop) record(kind, source, text string) (Message, error) { message, err := l.memory.Append(kind, text) if err != nil { return Message{}, err } l.notify(Event{ - Kind: kind, - ID: message.ID, - Text: text, + Kind: kind, + ID: message.ID, + Text: text, + Source: source, }) return message, nil @@ -162,24 +174,15 @@ func (l *loop) Flush(ctx context.Context) error { return l.memory.Flush(ctx) } -// 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. +// 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. 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 { @@ -365,37 +368,6 @@ func (l *loop) handle(ctx context.Context, input []any, response Response) ([]an return input, nil } -// 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 { @@ -407,7 +379,11 @@ func (l *loop) state() string { // 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 := make([]any, 0, len(view)+1+len(messages)) + for _, text := range view { + input = append(input, userMessage(text)) + } + input = append(input, userMessage(state)) for _, message := range messages { input = append(input, userMessage(message.Text)) @@ -416,16 +392,6 @@ func turnInput(view []string, state string, messages []Message) []any { 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 input -} - // inputItem returns an output item as an input item for the next request. The // calls do not store their items, so the item goes back whole, without its id. func inputItem(raw json.RawMessage) (map[string]any, error) {