Comment paragraphs use the repository reflow tool, and event literals
put each field on its own line. The Telegram request comment
distinguishes method-bearing setup errors from safe network causes. Code
behavior and error values stay unchanged.
Issue:
3cdc678
14 files changed:
// answer. The replies, the tool calls, and the results go to the log of tools
// as they happen. The thoughts never do.
//
// 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
+// 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
// cache. The messages of take join each later request after the tool results.
type Chain struct {
client ChainClient
CallID string `json:"call_id"`
}
CallID string `json:"call_id"`
}
-// ChainFunctionCallOutput is the input item with the result of a function
-// call.
+// ChainFunctionCallOutput is the input item with the result of a function call.
type ChainFunctionCallOutput struct {
Type string `json:"type"`
CallID string `json:"call_id"`
type ChainFunctionCallOutput struct {
Type string `json:"type"`
CallID string `json:"call_id"`
}
// Run runs the chain from input, the input of its first request, and returns
}
// 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.
+// 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" {
func (c *Chain) Run(ctx context.Context, input []any) (string, error) {
request := c.request
for request.Turn = "first"; ; request.Turn = "next" {
}
// handle logs the replies of response and runs its tool calls. It returns the
}
// 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.
+// 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
func (c *Chain) handle(ctx context.Context, input []any, response ModelResponse) ([]any, string, error) {
var (
outputs []any
}
// Login runs the device code login. It writes the link and the code for the
}
// Login runs the device code login. It writes the link and the code for the
-// user to out, waits until the user approves the code, and writes the tokens
-// to the token file.
+// user to out, waits until the user approves the code, and writes the tokens to
+// the token file.
func (a *ChatGPTAuth) Login(ctx context.Context, out io.Writer) error {
a.mutex.Lock()
defer a.mutex.Unlock()
func (a *ChatGPTAuth) Login(ctx context.Context, out io.Writer) error {
a.mutex.Lock()
defer a.mutex.Unlock()
-// defaultConfig returns the settings that apply when config.json does not
-// set them.
+// defaultConfig returns the settings that apply when config.json does not set
+// them.
func defaultConfig() Config {
return Config{
Endpoint: configEndpointChatGPT,
func defaultConfig() Config {
return Config{
Endpoint: configEndpointChatGPT,
-// Accept serves each client of listener until the listener closes. A new
-// client gets a ready event with the name of the agent, and a busy event when a
-// turn runs.
+// Accept serves each client of listener until the listener closes. A new client
+// gets a ready event with the name of the agent, and a busy event when a turn
+// runs.
func (h *DaemonHub) Accept(ctx context.Context, listener net.Listener, l *TurnLoop) {
for {
conn, err := listener.Accept()
func (h *DaemonHub) Accept(ctx context.Context, listener net.Listener, l *TurnLoop) {
for {
conn, err := listener.Accept()
-// remove removes client, closes its connection, and ends its writer. The
-// caller holds the mutex.
+// remove removes client, closes its connection, and ends its writer. The caller
+// holds the mutex.
func (h *DaemonHub) remove(client *DaemonClient) {
delete(h.clients, client)
client.once.Do(func() {
func (h *DaemonHub) remove(client *DaemonClient) {
delete(h.clients, client)
client.once.Do(func() {
-// promptEffortEnum returns the values of the effort of spawn: promptEfforts
-// and null.
+// promptEffortEnum returns the values of the effort of spawn: promptEfforts and
+// null.
func promptEffortEnum() []any {
values := make([]any, 0, len(promptEfforts)+1)
for _, effort := range promptEfforts {
func promptEffortEnum() []any {
values := make([]any, 0, len(promptEfforts)+1)
for _, effort := range promptEfforts {
// work.json in the directory of the agent holds the names of the running
// subagents. Each start and each end of a run writes it whole, but Close does
// not, so after a stop of the daemon it names the runs that the stop cut, and
// work.json in the directory of the agent holds the names of the running
// subagents. Each start and each end of a run writes it whole, but Close does
// not, so after a stop of the daemon it names the runs that the stop cut, and
-// Resume goes on with them. The logs alone cannot tell this, because a stop
-// can cut a run just after its final reply.
+// Resume goes on with them. The logs alone cannot tell this, because a stop can
+// cut a run just after its final reply.
//
// Spawn and Tell split the view with the layout of the agent, which has no
// lock. They are tools, so they run in the goroutine of the turns, as each
//
// Spawn and Tell split the view with the layout of the agent, which has no
// lock. They are tools, so they run in the goroutine of the turns, as each
return file.Running, nil
}
return file.Running, nil
}
-// save writes the names of the running subagents to work.json. The caller
-// holds the mutex.
+// save writes the names of the running subagents to work.json. The caller holds
+// the mutex.
func (r *SubagentRuns) save() error {
file := SubagentWorkFile{
Running: append([]string{}, slices.Sorted(maps.Keys(r.running))...),
func (r *SubagentRuns) save() error {
file := SubagentWorkFile{
Running: append([]string{}, slices.Sorted(maps.Keys(r.running))...),
return chain.Run(ctx, input)
}
return chain.Run(ctx, input)
}
-// take returns the messages of Tell that wait for a request of run, and
-// removes them.
+// take returns the messages of Tell that wait for a request of run, and removes
+// them.
func (r *SubagentRuns) take(run *SubagentRun) []Message {
r.mutex.Lock()
defer r.mutex.Unlock()
func (r *SubagentRuns) take(run *SubagentRun) []Message {
r.mutex.Lock()
defer r.mutex.Unlock()
}
// subagentTestStart runs a loop of agent with its memory and subagents in the
}
// subagentTestStart runs a loop of agent with its memory and subagents in the
-// agent directory, as the daemon does. The returned function stops the loop
-// and closes the files.
+// agent directory, as the daemon does. The returned function stops the loop and
+// closes the files.
func subagentTestStart(t *testing.T, agent *Agent, server *ModelFake, events *TurnEventRecorder, logger *slog.Logger) (*TurnLoop, *Memory, func()) {
t.Helper()
func subagentTestStart(t *testing.T, agent *Agent, server *ModelFake, events *TurnEventRecorder, logger *slog.Logger) (*TurnLoop, *Memory, func()) {
t.Helper()
-// subagentTestChat returns each message of the chat in memory as
-// "kind: text".
+// subagentTestChat returns each message of the chat in memory as "kind: text".
func subagentTestChat(t *testing.T, memory *Memory) []string {
t.Helper()
func subagentTestChat(t *testing.T, memory *Memory) []string {
t.Helper()
return input[len(input)-1].(map[string]any)
}
return input[len(input)-1].(map[string]any)
}
-// subagentTestText returns the text of the user message item, or "" for
-// another item.
+// subagentTestText returns the text of the user message item, or "" for another
+// item.
func subagentTestText(item map[string]any) string {
content, _ := item["content"].([]any)
func subagentTestText(item map[string]any) string {
content, _ := item["content"].([]any)
// itself, and as message entities only when Telegram refuses a rich message.
// The rest is plain text. Tool calls do not go to the chat.
//
// itself, and as message entities only when Telegram refuses a rich message.
// The rest is plain text. Tool calls do not go to the chat.
//
-// The offset file holds the ID of the next update. It moves past an update
-// only after the message of the update is durable in the log, so a restart
-// does not lose a message. A crash between the flush and the offset write
-// makes Telegram send the update again, and the message repeats once.
+// The offset file holds the ID of the next update. It moves past an update only
+// after the message of the update is durable in the log, so a restart does not
+// lose a message. A crash between the flush and the offset write makes Telegram
+// send the update again, and the message repeats once.
//
// A failed Bot API call waits and tries again, from telegramRetryDelay up to
// telegramRetryLimit, or as long as the Bot API asks. It never stops the agent.
//
// A failed Bot API call waits and tries again, from telegramRetryDelay up to
// telegramRetryLimit, or as long as the Bot API asks. It never stops the agent.
// call sends one call of method with the JSON params and decodes its result
// into result, when result is not nil. The call can take up to timeout. Its
// call sends one call of method with the JSON params and decodes its result
// into result, when result is not nil. The call can take up to timeout. Its
-// errors tell the cause in plain words, with no method name, so that a log line
-// can say first what the call was for.
+// request setup errors name the method. Network and answer errors give a safe
+// cause, so the caller can add the operation without exposing the request URL.
func (b *TelegramAPI) call(ctx context.Context, timeout time.Duration, method string, params, result any) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
func (b *TelegramAPI) call(ctx context.Context, timeout time.Duration, method string, params, result any) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
- name: "reply as it is",
- event: Event{Kind: "reply", Text: "Done."},
- text: "Done.",
+ name: "reply as it is",
+ event: Event{
+ Kind: "reply",
+ Text: "Done.",
+ },
+ text: "Done.",
- name: "own user message not shown",
- event: Event{Kind: "user", Text: "typed", Source: terminalSource},
- text: "",
+ name: "own user message not shown",
+ event: Event{
+ Kind: "user",
+ Text: "typed",
+ Source: terminalSource,
+ },
+ text: "",
- name: "user message of another source marked",
- event: Event{Kind: "user", Text: "from the phone", Source: "telegram"},
- text: "[telegram] from the phone",
+ name: "user message of another source marked",
+ event: Event{
+ Kind: "user",
+ Text: "from the phone",
+ Source: "telegram",
+ },
+ text: "[telegram] from the phone",
- name: "start of a subagent",
- event: Event{Kind: "subagent", Text: "GreenOtter"},
- text: "[GreenOtter] started",
+ name: "start of a subagent",
+ event: Event{
+ Kind: "subagent",
+ Text: "GreenOtter",
+ },
+ text: "[GreenOtter] started",
- name: "report of a subagent whole",
- event: Event{Kind: "work", Text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")", Source: "GreenOtter", Status: subagentFinished},
- text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
+ name: "report of a subagent whole",
+ event: Event{
+ Kind: "work",
+ Text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
+ Source: "GreenOtter",
+ Status: subagentFinished,
+ },
+ text: "[GreenOtter]\nThere are 3 files.\n\nFull chat: zoom(\"GreenOtter\")",
- name: "long result cut",
- event: Event{Kind: "echo", Text: strings.Repeat("line\n", 12)},
- text: "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
+ name: "long result cut",
+ event: Event{
+ Kind: "echo",
+ Text: strings.Repeat("line\n", 12),
+ },
+ text: "[result] " + strings.Repeat("line\n", 9) + "line\n[2 more lines]",
return output.String(), nil
}
return output.String(), nil
}
-// write replaces the file at path with text, and makes its missing
-// directories.
+// write replaces the file at path with text, and makes its missing directories.
func (b *Toolbox) write(path, text string) (string, error) {
path = b.path(path)
err := os.MkdirAll(filepath.Dir(path), 0o755)
func (b *Toolbox) write(path, text string) (string, error) {
path = b.path(path)
err := os.MkdirAll(filepath.Dir(path), 0o755)
return fmt.Sprintf("wrote %d bytes to %s", len(text), path), nil
}
return fmt.Sprintf("wrote %d bytes to %s", len(text), path), nil
}
-// edit replaces the one occurrence of old in the file at path with
-// replacement. It keeps the mode of the file.
+// edit replaces the one occurrence of old in the file at path with replacement.
+// It keeps the mode of the file.
func (b *Toolbox) edit(path, old, replacement string) (string, error) {
path = b.path(path)
data, err := os.ReadFile(path)
func (b *Toolbox) edit(path, old, replacement string) (string, error) {
path = b.path(path)
data, err := os.ReadFile(path)
return "edited " + path, nil
}
return "edited " + path, nil
}
-// path returns path, relative to the working directory when it is not
-// absolute.
+// path returns path, relative to the working directory when it is not absolute.
func (b *Toolbox) path(path string) string {
if filepath.IsAbs(path) {
return path
func (b *Toolbox) path(path string) string {
if filepath.IsAbs(path) {
return path
// layout read the view of the turn before from the prompt cache in each of 10
// turns, and with the view in one message it missed once in 5.
//
// layout read the view of the turn before from the prompt cache in each of 10
// turns, and with the view in one message it missed once in 5.
//
-// The first message starts with "<chat>\n". The closing tag goes into the
-// state message after the view, so that no view message changes when lines
-// come. The layout keeps the line count and a hash of the text of each message
-// in layout.json, so it outlives a restart.
+// The first message starts with "<chat>\n". The closing tag goes into the state
+// message after the view, so that no view message changes when lines come. The
+// layout keeps the line count and a hash of the text of each message in
+// layout.json, so it outlives a restart.
type ViewLayout struct {
path string
Messages []ViewLayoutMessage `json:"messages"`
type ViewLayout struct {
path string
Messages []ViewLayoutMessage `json:"messages"`
}
// Split returns the view messages for lines and saves the layout. It keeps the
}
// Split returns the view messages for lines and saves the layout. It keeps the
-// earlier messages up to the first one whose text differs, and puts the rest
-// of the lines into one new message.
+// earlier messages up to the first one whose text differs, and puts the rest of
+// the lines into one new message.
func (l *ViewLayout) Split(lines []string) ([]string, error) {
var (
texts []string
func (l *ViewLayout) Split(lines []string) ([]string, error) {
var (
texts []string
// WhatsAppGroups holds the names of the groups of the account, by the ID of the
// group. They come from the events of the groups and from reads of the groups
// from WhatsApp, and they stay in memory only. triedAt is the time of the last
// WhatsAppGroups holds the names of the groups of the account, by the ID of the
// group. They come from the events of the groups and from reads of the groups
// from WhatsApp, and they stay in memory only. triedAt is the time of the last
-// read, and readAt the time of the last read that succeeded. The mutex
-// protects all fields after logger.
+// read, and readAt the time of the last read that succeeded. The mutex protects
+// all fields after logger.
type WhatsAppGroups struct {
logger *slog.Logger
type WhatsAppGroups struct {
logger *slog.Logger
-// whatsappTestState opens the batches of the state file at path with the
-// times of the plan.
+// whatsappTestState opens the batches of the state file at path with the times
+// of the plan.
func whatsappTestState(t *testing.T, path string) *WhatsAppState {
t.Helper()
func whatsappTestState(t *testing.T, path string) *WhatsAppState {
t.Helper()