+package main
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+)
+
+// errChainLogWrite marks an error of a log write, which ends the chain.
+var errChainLogWrite = errors.New("log write failed")
+
+// ChainClient sends model calls.
+type ChainClient interface {
+ Create(ctx context.Context, request ModelRequest) (ModelResponse, error)
+}
+
+// Chain is one request chain of a turn or of a subagent. It sends request with
+// the input of the first request, runs the tool calls of each response, and
+// sends their results back, until a response has no tool call: the final
+// answer. The replies, the tool calls, and the results go to the log of tools
+// as they happen. The thoughts never do.
+//
+// Each later request 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 store it. So
+// each request begins with all of the previous one and reads it from the prompt
+// cache. The messages of take join each later request after the tool results.
+type Chain struct {
+ client ChainClient
+ tools *Toolbox
+ request ModelRequest
+ take func() []Message
+}
+
+// Run runs the chain from input, the input of its first request, and returns
+// the text of the final answer. An error of a log write wraps
+// errChainLogWrite.
+func (c *Chain) Run(ctx context.Context, input []any) (string, error) {
+ request := c.request
+ for request.Turn = "first"; ; request.Turn = "next" {
+ request.Input = input
+ response, err := c.client.Create(ctx, request)
+ if err != nil {
+ return "", err
+ }
+
+ var text string
+ input, text, err = c.handle(ctx, input, response)
+ if err != nil {
+ return "", err
+ }
+
+ if len(input) == 0 {
+ return text, nil
+ }
+ }
+}
+
+// handle logs the replies of response and runs its tool calls. It returns the
+// input of the next request: input, the output items, the tool results, and
+// the messages of take. After the final answer it returns no input, and the
+// replies of the answer as its text.
+func (c *Chain) handle(ctx context.Context, input []any, response ModelResponse) ([]any, string, error) {
+ var outputs []any
+ var replies []string
+ for _, raw := range response.Output {
+ item, err := chainInputItem(raw)
+ if err != nil {
+ return nil, "", err
+ }
+
+ input = append(input, item)
+
+ var call ChainFunctionCall
+ err = json.Unmarshal(raw, &call)
+ if err != nil {
+ return nil, "", fmt.Errorf("decode output item: %w", err)
+ }
+
+ switch call.Type {
+ case "message":
+ text, err := modelOutputText([]json.RawMessage{raw})
+ if err != nil {
+ return nil, "", err
+ }
+
+ text = strings.TrimSpace(text)
+ if text == "" {
+ continue
+ }
+
+ _, err = c.tools.log.Append("reply", text)
+ if err != nil {
+ return nil, "", fmt.Errorf("%w: %w", errChainLogWrite, err)
+ }
+
+ replies = append(replies, text)
+ case "function_call":
+ output, err := c.tools.Call(ctx, call.Name, call.Arguments)
+ if err != nil {
+ return nil, "", fmt.Errorf("%w: %w", errChainLogWrite, err)
+ }
+
+ outputs = append(outputs, ChainFunctionCallOutput{
+ Type: "function_call_output",
+ CallID: call.CallID,
+ Output: output,
+ })
+ }
+ }
+
+ if len(outputs) == 0 {
+ return nil, strings.Join(replies, "\n\n"), nil
+ }
+
+ input = append(input, outputs...)
+ for _, message := range c.take() {
+ input = append(input, modelUserMessage(message.Text))
+ }
+
+ return input, "", nil
+}
+
+// ChainFunctionCall holds the fields of a function call output item.
+type ChainFunctionCall struct {
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Arguments string `json:"arguments"`
+ CallID string `json:"call_id"`
+}
+
+// ChainFunctionCallOutput is the input item with the result of a function
+// call.
+type ChainFunctionCallOutput struct {
+ Type string `json:"type"`
+ CallID string `json:"call_id"`
+ Output string `json:"output"`
+}
+
+// chainInputItem 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 chainInputItem(raw json.RawMessage) (map[string]any, error) {
+ var item map[string]any
+ err := json.Unmarshal(raw, &item)
+ if err != nil {
+ return nil, fmt.Errorf("decode output item: %w", err)
+ }
+
+ delete(item, "id")
+
+ return item, nil
+}