X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/5da3ea038d1e9f9a2634d5ab137aef6989a3a789..6d1d9d9a7cf90b25a2e10af1c5bc56faee7e494c:/turn.go?ds=sidebyside diff --git a/turn.go b/turn.go index 76f524e..029f24e 100644 --- a/turn.go +++ b/turn.go @@ -5,23 +5,27 @@ import ( "encoding/json" "errors" "fmt" + "path/filepath" "strings" "sync" "time" ) -// modelClient sends model calls. -type modelClient interface { - Create(ctx context.Context, request Request) (Response, error) +// TurnClient sends model calls. +type TurnClient interface { + Create(ctx context.Context, request ModelRequest) (ModelResponse, error) } // 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 @@ -30,6 +34,9 @@ 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, 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 // hold the reasoning with its encrypted content, because the calls do not @@ -40,8 +47,9 @@ type Event struct { type loop struct { agent *Agent memory *Memory - client modelClient + client TurnClient tools *toolbox + layout *viewLayout clock func() time.Time notify func(Event) wake chan struct{} @@ -68,11 +76,17 @@ type functionCallOutput struct { // 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 { +func newLoop(agent *Agent, memory *Memory, client TurnClient, 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, clock: clock, notify: notify, wake: make(chan struct{}, 1), @@ -84,13 +98,14 @@ 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 -// 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 } @@ -122,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 @@ -246,14 +276,20 @@ func (l *loop) chain(ctx context.Context) error { skip[message.ID] = true } - input := turnInput(l.memory.ViewWithout(skip), l.state(), messages) - for { - response, err := l.client.Create(ctx, Request{ + 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, ModelRequest{ Model: l.agent.config.Model, Effort: l.agent.config.Effort, Instructions: instructions, Tools: agentTools(), Input: input, + Turn: turn, }) if err != nil { return err @@ -274,7 +310,7 @@ func (l *loop) chain(ctx context.Context) error { // input of the next request: input, the output items, the tool results, and // the user messages that came in the meantime. After the final answer it // returns no input. -func (l *loop) handle(ctx context.Context, input []any, response Response) ([]any, error) { +func (l *loop) handle(ctx context.Context, input []any, response ModelResponse) ([]any, error) { var outputs []any for _, raw := range response.Output { item, err := inputItem(raw) @@ -292,7 +328,7 @@ func (l *loop) handle(ctx context.Context, input []any, response Response) ([]an switch call.Type { case "message": - text, err := outputText([]json.RawMessage{raw}) + text, err := modelOutputText([]json.RawMessage{raw}) if err != nil { return nil, err } @@ -326,27 +362,34 @@ func (l *loop) handle(ctx context.Context, input []any, response Response) ([]an input = append(input, outputs...) for _, message := range l.take() { - input = append(input, userMessage(message.Text)) + input = append(input, modelUserMessage(message.Text)) } return input, nil } -// state returns the per-turn state: the date and time of the turn. +// 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 := make([]any, 0, len(view)+1+len(messages)) + for _, text := range view { + input = append(input, modelUserMessage(text)) + } + + input = append(input, modelUserMessage(state)) for _, message := range messages { - parts = append(parts, message.Text) + input = append(input, modelUserMessage(message.Text)) } - return []any{userMessage(parts...)} + return input } // inputItem returns an output item as an input item for the next request. The