@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/coder/agentapi/lib/msgfmt"
1212 "github.com/coder/agentapi/lib/util"
13+ "github.com/coder/quartz"
1314 "github.com/danielgtaylor/huma/v2"
1415 "golang.org/x/xerrors"
1516)
@@ -27,8 +28,8 @@ type AgentIO interface {
2728type ConversationConfig struct {
2829 AgentType msgfmt.AgentType
2930 AgentIO AgentIO
30- // GetTime returns the current time
31- GetTime func () time. Time
31+ // Clock provides time operations for the conversation
32+ Clock quartz. Clock
3233 // How often to take a snapshot for the stability check
3334 SnapshotInterval time.Duration
3435 // How long the screen should not change to be considered stable
@@ -109,6 +110,9 @@ func getStableSnapshotsThreshold(cfg ConversationConfig) int {
109110}
110111
111112func NewConversation (ctx context.Context , cfg ConversationConfig , initialPrompt string ) * Conversation {
113+ if cfg .Clock == nil {
114+ cfg .Clock = quartz .NewReal ()
115+ }
112116 threshold := getStableSnapshotsThreshold (cfg )
113117 c := & Conversation {
114118 cfg : cfg ,
@@ -118,7 +122,7 @@ func NewConversation(ctx context.Context, cfg ConversationConfig, initialPrompt
118122 {
119123 Message : "" ,
120124 Role : ConversationRoleAgent ,
121- Time : cfg .GetTime (),
125+ Time : cfg .Clock . Now (),
122126 },
123127 },
124128 InitialPrompt : initialPrompt ,
@@ -130,11 +134,13 @@ func NewConversation(ctx context.Context, cfg ConversationConfig, initialPrompt
130134
131135func (c * Conversation ) StartSnapshotLoop (ctx context.Context ) {
132136 go func () {
137+ ticker := c .cfg .Clock .NewTicker (c .cfg .SnapshotInterval )
138+ defer ticker .Stop ()
133139 for {
134140 select {
135141 case <- ctx .Done ():
136142 return
137- case <- time . After ( c . cfg . SnapshotInterval ) :
143+ case <- ticker . C :
138144 // It's important that we hold the lock while reading the screen.
139145 // There's a race condition that occurs without it:
140146 // 1. The screen is read
@@ -250,7 +256,7 @@ func (c *Conversation) updateLastAgentMessage(screen string, timestamp time.Time
250256// assumes the caller holds the lock
251257func (c * Conversation ) addSnapshotInner (screen string ) {
252258 snapshot := screenSnapshot {
253- timestamp : c .cfg .GetTime (),
259+ timestamp : c .cfg .Clock . Now (),
254260 screen : screen ,
255261 }
256262 c .snapshotBuffer .Add (snapshot )
@@ -320,10 +326,11 @@ func (c *Conversation) writeMessageWithConfirmation(ctx context.Context, message
320326 Timeout : 15 * time .Second ,
321327 MinInterval : 50 * time .Millisecond ,
322328 InitialWait : true ,
329+ Clock : c .cfg .Clock ,
323330 }, func () (bool , error ) {
324331 screen := c .cfg .AgentIO .ReadScreen ()
325332 if screen != screenBeforeMessage {
326- time . Sleep ( 1 * time .Second )
333+ <- util . After ( c . cfg . Clock , time .Second )
327334 newScreen := c .cfg .AgentIO .ReadScreen ()
328335 return newScreen == screen , nil
329336 }
@@ -338,17 +345,18 @@ func (c *Conversation) writeMessageWithConfirmation(ctx context.Context, message
338345 if err := util .WaitFor (ctx , util.WaitTimeout {
339346 Timeout : 15 * time .Second ,
340347 MinInterval : 25 * time .Millisecond ,
348+ Clock : c .cfg .Clock ,
341349 }, func () (bool , error ) {
342350 // we don't want to spam additional carriage returns because the agent may process them
343351 // (aider does this), but we do want to retry sending one if nothing's
344352 // happening for a while
345- if time .Since (lastCarriageReturnTime ) >= 3 * time .Second {
346- lastCarriageReturnTime = time .Now ()
353+ if c . cfg . Clock .Since (lastCarriageReturnTime ) >= 3 * time .Second {
354+ lastCarriageReturnTime = c . cfg . Clock .Now ()
347355 if _ , err := c .cfg .AgentIO .Write ([]byte ("\r " )); err != nil {
348356 return false , xerrors .Errorf ("failed to write carriage return: %w" , err )
349357 }
350358 }
351- time . Sleep ( 25 * time .Millisecond )
359+ <- util . After ( c . cfg . Clock , 25 * time .Millisecond )
352360 screen := c .cfg .AgentIO .ReadScreen ()
353361
354362 return screen != screenBeforeCarriageReturn , nil
@@ -359,9 +367,11 @@ func (c *Conversation) writeMessageWithConfirmation(ctx context.Context, message
359367 return nil
360368}
361369
362- var MessageValidationErrorWhitespace = xerrors .New ("message must be trimmed of leading and trailing whitespace" )
363- var MessageValidationErrorEmpty = xerrors .New ("message must not be empty" )
364- var MessageValidationErrorChanging = xerrors .New ("message can only be sent when the agent is waiting for user input" )
370+ var (
371+ MessageValidationErrorWhitespace = xerrors .New ("message must be trimmed of leading and trailing whitespace" )
372+ MessageValidationErrorEmpty = xerrors .New ("message must not be empty" )
373+ MessageValidationErrorChanging = xerrors .New ("message can only be sent when the agent is waiting for user input" )
374+ )
365375
366376func (c * Conversation ) SendMessage (messageParts ... MessagePart ) error {
367377 c .lock .Lock ()
@@ -382,7 +392,7 @@ func (c *Conversation) SendMessage(messageParts ...MessagePart) error {
382392 }
383393
384394 screenBeforeMessage := c .cfg .AgentIO .ReadScreen ()
385- now := c .cfg .GetTime ()
395+ now := c .cfg .Clock . Now ()
386396 c .updateLastAgentMessage (screenBeforeMessage , now )
387397
388398 if err := c .writeMessageWithConfirmation (context .Background (), messageParts ... ); err != nil {
0 commit comments