Add runtime abstraction and agent loop

Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-17 16:47:22 +00:00
co-authored by codegirl007
parent 54dc5658c6
commit 94146fcae2
17 changed files with 1512 additions and 2 deletions
+149
View File
@@ -0,0 +1,149 @@
package runtime
import (
"context"
"errors"
"fmt"
"io"
"time"
"agentbox/internal/agent"
"agentbox/internal/environment"
"agentbox/internal/trace"
)
type Config struct {
Task string
Command environment.Command
Agent agent.Agent
Env environment.Environment
Trace *trace.Store
MaxSteps int
Output io.Writer
}
func Run(ctx context.Context, config Config) (runErr error) {
if config.MaxSteps <= 0 {
config.MaxSteps = 20
}
if config.Output == nil {
config.Output = io.Discard
}
if config.Agent == nil || config.Env == nil || config.Trace == nil {
return errors.New("runtime requires agent, environment, and trace")
}
started := false
defer func() {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if started {
if logs, err := config.Env.Logs(cleanupCtx); err == nil {
if logErr := config.Trace.WriteLogs(logs); runErr == nil && logErr != nil {
runErr = logErr
}
}
if stopErr := config.Env.Stop(cleanupCtx); runErr == nil && stopErr != nil {
runErr = stopErr
}
}
if finishErr := config.Trace.Finish(runErr); runErr == nil && finishErr != nil {
runErr = finishErr
}
}()
if err := config.Env.Start(ctx); err != nil {
return err
}
started = true
if err := config.Env.Launch(ctx, config.Command); err != nil {
return err
}
fmt.Fprintln(config.Output, "Agent attached.")
var history []agent.Step
var actions []environment.InputAction
startedAt := time.Now()
for number := 1; number <= config.MaxSteps; number++ {
screenshot, err := config.Env.Screenshot(ctx)
if err != nil {
return err
}
screenshotPath, err := config.Trace.SaveScreenshot(number, screenshot)
if err != nil {
return err
}
logs, err := config.Env.Logs(ctx)
if err != nil {
return err
}
now := time.Now().UTC()
fmt.Fprintf(config.Output, "[%s] screenshot captured\n", elapsed(startedAt))
decision, err := config.Agent.NextAction(ctx, config.Task, history, agent.Observation{
Screenshot: screenshot,
Timestamp: now,
Logs: logs,
PreviousActions: append([]environment.InputAction(nil), actions...),
})
if err != nil {
return fmt.Errorf("agent next action: %w", err)
}
fmt.Fprintf(config.Output, "[%s] agent: %s\n", elapsed(startedAt), decision.Reason)
record := trace.StepRecord{
Step: number,
Timestamp: now,
Observation: trace.ObservationRecord{
Screenshot: screenshotPath,
Logs: logs,
PreviousActions: append([]environment.InputAction(nil), actions...),
},
Agent: trace.AgentRecord{Message: decision.Reason},
Action: decision.Action,
Done: decision.Done,
}
if err := config.Trace.Record(record); err != nil {
return err
}
history = append(history, agent.Step{
Number: number,
Timestamp: now,
ScreenshotPath: screenshotPath,
Message: decision.Reason,
Action: decision.Action,
})
if decision.Done {
fmt.Fprintln(config.Output, "Task complete.")
return nil
}
if decision.Action == nil {
return errors.New("agent returned neither action nor completion")
}
fmt.Fprintf(config.Output, "[%s] action: %s%s\n",
elapsed(startedAt), decision.Action.Type, actionDetail(*decision.Action))
if err := config.Env.SendInput(ctx, *decision.Action); err != nil {
return err
}
actions = append(actions, *decision.Action)
}
return fmt.Errorf("agent exceeded maximum of %d steps", config.MaxSteps)
}
func elapsed(start time.Time) string {
duration := time.Since(start).Round(time.Second)
return fmt.Sprintf("%02d:%02d", int(duration.Minutes()), int(duration.Seconds())%60)
}
func actionDetail(action environment.InputAction) string {
switch action.Type {
case environment.KeyDown, environment.KeyUp:
return " " + action.Key
case environment.MouseMove:
return fmt.Sprintf(" %d,%d", action.X, action.Y)
case environment.MouseDown, environment.MouseUp:
return fmt.Sprintf(" %d", action.Button)
case environment.Wait:
return " " + action.Duration.String()
default:
return ""
}
}