summary |
shortlog |
log |
commit | commitdiff |
tree
raw |
inline | side by side (from parent 1:
8d9547f)
A turn that ends with an error logs one line with the error that the
clients get, such as a needed login or HTTP 429, so that it is not lost
when no client is connected. An interrupt of the user or a stop of the
daemon logs nothing.
The end of each subagent run logs one line with the name of the
subagent: done, failed with the cause, or stopped by the user. A cut
subagent that cannot go on after a restart logs its cause too.
Issue:
e4acfcb
}
return daemonServe(ctx, listener, telegram, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
}
return daemonServe(ctx, listener, telegram, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
- return newTurnLoop(agent, memory, subagents, whatsapp, schedules, client, time.Now, notify)
+ return newTurnLoop(agent, memory, subagents, whatsapp, schedules, client, time.Now, logger, notify)
"path/filepath"
"testing"
"time"
"path/filepath"
"testing"
"time"
done := make(chan error)
go func() {
done <- daemonServe(ctx, listener, bot, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
done := make(chan error)
go func() {
done <- daemonServe(ctx, listener, bot, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
- return newTurnLoop(agent, memory, newTestSubagents(t), whatsapp, schedules, client, clock, notify)
+ return newTurnLoop(agent, memory, newTestSubagents(t), whatsapp, schedules, client, clock, log.New(io.Discard, "", 0), notify)
"encoding/json"
"errors"
"fmt"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"maps"
"os"
"path/filepath"
// of the subagent. A message of Tell that comes during a chain joins its next
// request after the tool results. One that comes after the final answer starts
// one more chain, which reads the log again. The report of the run goes to
// of the subagent. A message of Tell that comes during a chain joins its next
// request after the tool results. One that comes after the final answer starts
// one more chain, which reads the log again. The report of the run goes to
-// report: the final answer of each chain, or the error of a failed call. Stop
-// ends one run, and its report says that it stopped. At the stop of the daemon
-// the runs end with no report.
+// report: the final answer of each chain, or the error of a failed call. The
+// logger gets one line at the end of a run: done, failed with the cause, or
+// stopped. Stop ends one run, and its report says that it stopped. At the stop
+// of the daemon the runs end with no report.
//
// 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
//
// 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
report func(name, text, status string)
note func(text string) error
fail func(err error)
report func(name, text, status string)
note func(text string) error
fail func(err error)
notify func(Event)
mutex sync.Mutex
notify func(Event)
mutex sync.Mutex
for _, name := range names {
_, err = r.Tell(ctx, name, text)
if err != nil {
for _, name := range names {
_, err = r.Tell(ctx, name, text)
if err != nil {
+ r.logger.Printf("subagent %s cannot go on after the restart, and its report tells the agent: %v", name, err)
r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
}
}
r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
}
}
defer run.cancel()
var replies []string
defer run.cancel()
var replies []string
stopped := false
status := subagentFinished
for more := true; more; {
stopped := false
status := subagentFinished
for more := true; more; {
case err != nil:
reply = "failed: " + err.Error()
status = subagentFailed
case err != nil:
reply = "failed: " + err.Error()
status = subagentFailed
}
more = err == nil && ctx.Err() == nil && len(run.pending) > 0
}
more = err == nil && ctx.Err() == nil && len(run.pending) > 0
status = subagentStopped
}
status = subagentStopped
}
+ switch status {
+ case subagentStopped:
+ r.logger.Printf("subagent %s is stopped by the user", run.subagent.name)
+ case subagentFailed:
+ r.logger.Printf("subagent %s failed, and its report tells the agent: %v", run.subagent.name, failure)
+ default:
+ r.logger.Printf("subagent %s is done, and its report goes to the agent", run.subagent.name)
+ }
+
r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
}
r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
}
"net/http"
"os"
"path/filepath"
"net/http"
"os"
"path/filepath"
Source: name,
Status: subagentFinished,
}, test.events.LastEvent("work"))
Source: name,
Status: subagentFinished,
}, test.events.LastEvent("work"))
+ require.Equal(t, "subagent "+name+" is done, and its report goes to the agent\n", test.logs.String())
var first, second *ModelFakeCall
for n := range server.Calls() {
var first, second *ModelFakeCall
for n := range server.Calls() {
"reply: It failed.",
}, test.Log(t))
require.Equal(t, subagentFailed, test.events.LastEvent("work").Status)
"reply: It failed.",
}, test.Log(t))
require.Equal(t, subagentFailed, test.events.LastEvent("work").Status)
+ require.Equal(t, "subagent "+name+" failed, and its report tells the agent: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String())
}
func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
}
func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
"work: " + subagentReport(name, []string{"stopped"}),
}, test.Log(t))
require.Equal(t, subagentStopped, test.events.LastEvent("work").Status)
"work: " + subagentReport(name, []string{"stopped"}),
}, test.Log(t))
require.Equal(t, subagentStopped, test.events.LastEvent("work").Status)
+ require.Equal(t, "subagent "+name+" is stopped by the user\n", test.logs.String())
log, err := subagent.Show()
require.NoError(t, err)
log, err := subagent.Show()
require.NoError(t, err)
subagents := newSubagents(filepath.Join(agent.dir, subagentDirName), clock.Now, subagentNameRandom)
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(agent.dir, "usage.jsonl"))
whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), clock)
subagents := newSubagents(filepath.Join(agent.dir, subagentDirName), clock.Now, subagentNameRandom)
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(agent.dir, "usage.jsonl"))
whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), clock)
- loop, err := newTurnLoop(agent, memory, subagents, whatsapp, newTestSchedules(t, clock.Now), client, clock.Now, events.Add)
+ loop, err := newTurnLoop(agent, memory, subagents, whatsapp, newTestSchedules(t, clock.Now), client, clock.Now, log.New(io.Discard, "", 0), events.Add)
require.NoError(t, err)
ctx, cancel := context.WithCancel(t.Context())
require.NoError(t, err)
ctx, cancel := context.WithCancel(t.Context())
"path/filepath"
"sync"
"time"
"path/filepath"
"sync"
"time"
// newTurnLoop returns the loop of agent with its memory, its subagents, and the
// WhatsApp link and the schedules of its tools. Its calls go through client,
// newTurnLoop returns the loop of agent with its memory, its subagents, and the
// WhatsApp link and the schedules of its tools. Its calls go through client,
-// and notify gets each event. The clock gives the date of a turn.
-func newTurnLoop(agent *Agent, memory *Memory, subagents *Subagents, whatsapp *WhatsApp, schedules *Schedules, client ChainClient, clock func() time.Time, notify func(Event)) (*TurnLoop, error) {
+// and notify gets each event. The clock gives the date of a turn. logger gets
+// the failed turns and the ends of the subagents.
+func newTurnLoop(agent *Agent, memory *Memory, subagents *Subagents, whatsapp *WhatsApp, schedules *Schedules, client ChainClient, clock func() time.Time, logger *log.Logger, notify func(Event)) (*TurnLoop, error) {
layout, err := loadViewLayout(filepath.Join(agent.dir, viewLayoutFileName))
if err != nil {
return nil, err
layout, err := loadViewLayout(filepath.Join(agent.dir, viewLayoutFileName))
if err != nil {
return nil, err
client: client,
layout: layout,
clock: clock,
client: client,
layout: layout,
clock: clock,
notify: notify,
wake: make(chan struct{}, 1),
}
notify: notify,
wake: make(chan struct{}, 1),
}
report: l.Report,
note: l.note,
fail: l.fail,
report: l.Report,
note: l.note,
fail: l.fail,
notify: notify,
running: make(map[string]*SubagentRun),
}
notify: notify,
running: make(map[string]*SubagentRun),
}
runs *SubagentRuns
layout *ViewLayout
clock func() time.Time
runs *SubagentRuns
layout *ViewLayout
clock func() time.Time
notify func(Event)
wake chan struct{}
notify func(Event)
wake chan struct{}
}
// turn runs one turn. A failed or interrupted model call ends the turn and
}
// turn runs one turn. A failed or interrupted model call ends the turn and
-// tells the clients. The messages of the turn stay in the log and the view, and
-// the next turn starts only with a new message, so a lasting failure such as a
-// needed login does not repeat. Only a failed log write is an error of turn.
+// tells the clients. A failure also goes to the logger, so that it is not lost
+// when no client is connected, but an interrupt does not. The messages of the
+// turn stay in the log and the view, and the next turn starts only with a new
+// message, so a lasting failure such as a needed login does not repeat. Only a
+// failed log write is an error of turn.
func (l *TurnLoop) turn(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
func (l *TurnLoop) turn(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
err := l.chain(ctx)
if err != nil && !errors.Is(err, errChainLogWrite) {
err := l.chain(ctx)
if err != nil && !errors.Is(err, errChainLogWrite) {
+ if ctx.Err() == nil {
+ l.logger.Printf("the turn stops before its end, and the agent waits for the next message: %v", err)
+ }
+
l.notify(Event{
Kind: "error",
Text: turnError(ctx, err),
l.notify(Event{
Kind: "error",
Text: turnError(ctx, err),
"context"
"encoding/json"
"errors"
"context"
"encoding/json"
"errors"
"net/http"
"path/filepath"
"strings"
"net/http"
"path/filepath"
"strings"
require.NoError(t, err)
test.events.WaitFor(t, "idle", 2)
require.Equal(t, "reply: answer", test.Log(t)[2])
require.NoError(t, err)
test.events.WaitFor(t, "idle", 2)
require.Equal(t, "reply: answer", test.Log(t)[2])
+ require.Empty(t, test.logs.String())
}
func TestFailedCallEndsTheTurnWithoutARetry(t *testing.T) {
}
func TestFailedCallEndsTheTurnWithoutARetry(t *testing.T) {
time.Sleep(50 * time.Millisecond)
require.Equal(t, 1, server.Calls())
require.Contains(t, test.events.Last("error"), "HTTP 503")
time.Sleep(50 * time.Millisecond)
require.Equal(t, 1, server.Calls())
require.Contains(t, test.events.Last("error"), "HTTP 503")
+ require.Equal(t, 1, strings.Count(test.logs.String(), "\n"))
+ require.Contains(t, test.logs.String(), "the turn stops before its end, and the agent waits for the next message: ")
+ require.Contains(t, test.logs.String(), "HTTP 503")
+ require.NotContains(t, test.logs.String(), "hello")
}
func TestViewMessagesOnlyGrowBetweenTurns(t *testing.T) {
}
func TestViewMessagesOnlyGrowBetweenTurns(t *testing.T) {
memory *Memory
loop *TurnLoop
events *TurnEventRecorder
memory *Memory
loop *TurnLoop
events *TurnEventRecorder
agent: agent,
memory: memory,
events: &TurnEventRecorder{},
agent: agent,
memory: memory,
events: &TurnEventRecorder{},
usagePath: filepath.Join(t.TempDir(), "usage.jsonl"),
}
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), test.usagePath)
whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), clock)
usagePath: filepath.Join(t.TempDir(), "usage.jsonl"),
}
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), test.usagePath)
whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), clock)
- test.loop, err = newTurnLoop(agent, memory, newTestSubagents(t), whatsapp, newTestSchedules(t, clock.Now), client, clock.Now, test.events.Add)
+ test.loop, err = newTurnLoop(agent, memory, newTestSubagents(t), whatsapp, newTestSchedules(t, clock.Now), client, clock.Now, log.New(test.logs, "", 0), test.events.Add)
require.NoError(t, err)
ctx, cancel := context.WithCancel(t.Context())
done := make(chan error)
require.NoError(t, err)
ctx, cancel := context.WithCancel(t.Context())
done := make(chan error)