Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/agentapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"os/signal"
"syscall"

"github.com/kooshapari/agentapi/internal/routing"
"github.com/kooshapari/agentapi/internal/server"
"github.com/coder/agentapi/internal/routing"
"github.com/coder/agentapi/internal/server"
)

var (
Expand All @@ -27,11 +27,11 @@ func main() {

// Start the server
srv := server.New(*port, router)

// Handle graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

go func() {
<-quit
log.Println("Shutting down agentapi...")
Expand All @@ -40,7 +40,7 @@ func main() {

log.Printf("AgentAPI starting on port %d", *port)
log.Printf("Connecting to cliproxy+bifrost at %s", *cliproxyURL)

if err := srv.Start(); err != nil {
log.Fatalf("Server error: %v", err)
}
Expand Down
125 changes: 125 additions & 0 deletions e2e/asciinema/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Package asciinema provides utilities for parsing asciinema recordings
// and converting them to echo agent scripts.
package asciinema

import (
"encoding/json"
"fmt"
"os"
)

// ScriptEntry represents an echo agent script entry
type ScriptEntry struct {
ExpectMessage string `json:"expectMessage"`
ThinkDurationMS int64 `json:"thinkDurationMS"`
ResponseMessage string `json:"responseMessage"`
}

// Recording represents an asciinema v2 recording file format
type Recording struct {
Version float64 `json:"version"`
Width int `json:"width"`
Height int `json:"height"`
Lines []Event `json:"lines"`
}

// Event represents a single asciinema event
type Event struct {
Time float64 `json:"time"`
Type string `json:"type"`
Data interface{} `json:"data"`
}

// ParseRecording parses an asciinema recording file
func ParseRecording(path string) (*Recording, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open recording: %w", err)
}
defer f.Close()

Check failure on line 39 in e2e/asciinema/parser.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `f.Close` is not checked (errcheck)

var rec Recording
if err := json.NewDecoder(f).Decode(&rec); err != nil {
return nil, fmt.Errorf("decode recording: %w", err)
}

return &rec, nil
}

// ToEchoScript converts an asciinema recording to echo agent script format
func ToEchoScript(rec *Recording) ([]ScriptEntry, error) {
var entries []ScriptEntry
var lastTime float64

for _, event := range rec.Lines {
// Only process output events
if event.Type != "o" {
continue
}

// Get the output content
var frames []interface{}
switch d := event.Data.(type) {
case []interface{}:
frames = d
default:
continue
}

// Calculate think duration based on time delta
thinkDuration := int64((event.Time - lastTime) * 1000)
if thinkDuration < 0 {
thinkDuration = 0
}

// Combine frames into response
var output string
for _, frame := range frames {
switch f := frame.(type) {
case string:
output += f + "\n"
case []interface{}:
for _, line := range f {
if s, ok := line.(string); ok {
output += s + "\n"
}
}
}
}

if output != "" {
entries = append(entries, ScriptEntry{
ThinkDurationMS: thinkDuration,
ResponseMessage: output,
})
}

lastTime = event.Time
}

return entries, nil
}

// WriteEchoScript writes the converted script to a JSON file
func WriteEchoScript(entries []ScriptEntry, path string) error {
data, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
return os.WriteFile(path, data, 0644)
}

// LoadAndConvert loads an asciinema recording and converts to echo script
func LoadAndConvert(asciinemaPath, echoPath string) error {
rec, err := ParseRecording(asciinemaPath)
if err != nil {
return err
}

entries, err := ToEchoScript(rec)
if err != nil {
return err
}

return WriteEchoScript(entries, echoPath)
}
63 changes: 63 additions & 0 deletions e2e/asciinema/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package asciinema

import (
"os"
"testing"
)

func TestParseRecording(t *testing.T) {
testRec := `{
"version": 2,
"width": 80,
"height": 24,
"lines": [
[0.0, "o", [["Hello world"]],
[0.5, "o", [["Thinking..."]],
[1.0, "o", [["Response here"]]
]
}`

tmp, err := os.CreateTemp("", "test-*.json")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmp.Name())

Check failure on line 24 in e2e/asciinema/parser_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `os.Remove` is not checked (errcheck)

if _, err := tmp.WriteString(testRec); err != nil {
t.Fatal(err)
}
tmp.Close()

Check failure on line 29 in e2e/asciinema/parser_test.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `tmp.Close` is not checked (errcheck)

rec, err := ParseRecording(tmp.Name())
if err != nil {
t.Fatalf("parse error: %v", err)
}

if rec.Version != 2 {
t.Errorf("expected version 2, got %v", rec.Version)
}

if len(rec.Lines) != 3 {
t.Errorf("expected 3 lines, got %d", len(rec.Lines))
}
}

func TestToEchoScript(t *testing.T) {
rec := &Recording{
Version: 2,
Lines: []Event{
{Time: 0.0, Type: "o", Data: []interface{}{[]interface{}{"Hello"}}},
{Time: 0.5, Type: "o", Data: []interface{}{[]interface{}{"World"}}},
{Time: 1.0, Type: "i", Data: []interface{}{"x"}},
},
}

entries, err := ToEchoScript(rec)
if err != nil {
t.Fatal(err)
}

if len(entries) != 2 {
t.Errorf("expected 2 entries, got %d", len(entries))
}
}
Loading
Loading