mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-19 04:38:50 +02:00
Compare commits
1 Commits
feat/cli-v
...
agent/j/89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ac1b7aa43 |
@@ -84,8 +84,15 @@ func LoadConfig(overrides Overrides) (Config, error) {
|
||||
Model: strings.TrimSpace(os.Getenv("MULTICA_CODEX_MODEL")),
|
||||
}
|
||||
}
|
||||
opencodePath := envOrDefault("MULTICA_OPENCODE_PATH", "opencode")
|
||||
if _, err := exec.LookPath(opencodePath); err == nil {
|
||||
agents["opencode"] = AgentEntry{
|
||||
Path: opencodePath,
|
||||
Model: strings.TrimSpace(os.Getenv("MULTICA_OPENCODE_MODEL")),
|
||||
}
|
||||
}
|
||||
if len(agents) == 0 {
|
||||
return Config{}, fmt.Errorf("no agent CLI found: install claude or codex and ensure it is on PATH")
|
||||
return Config{}, fmt.Errorf("no agent CLI found: install claude, codex, or opencode and ensure it is on PATH")
|
||||
}
|
||||
|
||||
// Host info
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package agent provides a unified interface for executing prompts via
|
||||
// coding agents (Claude Code, Codex). It mirrors the happy-cli AgentBackend
|
||||
// pattern, translated to idiomatic Go.
|
||||
// coding agents (Claude Code, Codex, OpenCode). It mirrors the happy-cli
|
||||
// AgentBackend pattern, translated to idiomatic Go.
|
||||
package agent
|
||||
|
||||
import (
|
||||
@@ -90,8 +90,10 @@ func New(agentType string, cfg Config) (Backend, error) {
|
||||
return &claudeBackend{cfg: cfg}, nil
|
||||
case "codex":
|
||||
return &codexBackend{cfg: cfg}, nil
|
||||
case "opencode":
|
||||
return &opencodeBackend{cfg: cfg}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown agent type: %q (supported: claude, codex)", agentType)
|
||||
return nil, fmt.Errorf("unknown agent type: %q (supported: claude, codex, opencode)", agentType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,17 @@ func TestNewReturnsCodexBackend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewReturnsOpencodeBackend(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, err := New("opencode", Config{ExecutablePath: "/nonexistent/opencode"})
|
||||
if err != nil {
|
||||
t.Fatalf("New(opencode) error: %v", err)
|
||||
}
|
||||
if _, ok := b.(*opencodeBackend); !ok {
|
||||
t.Fatalf("expected *opencodeBackend, got %T", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRejectsUnknownType(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := New("gpt", Config{})
|
||||
|
||||
264
server/pkg/agent/opencode.go
Normal file
264
server/pkg/agent/opencode.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// opencodeBackend implements Backend by spawning the OpenCode CLI
|
||||
// with `opencode run --format json`.
|
||||
type opencodeBackend struct {
|
||||
cfg Config
|
||||
}
|
||||
|
||||
func (b *opencodeBackend) Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error) {
|
||||
execPath := b.cfg.ExecutablePath
|
||||
if execPath == "" {
|
||||
execPath = "opencode"
|
||||
}
|
||||
if _, err := exec.LookPath(execPath); err != nil {
|
||||
return nil, fmt.Errorf("opencode executable not found at %q: %w", execPath, err)
|
||||
}
|
||||
|
||||
timeout := opts.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = 20 * time.Minute
|
||||
}
|
||||
runCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
|
||||
args := []string{"run", "--format", "json"}
|
||||
if opts.Model != "" {
|
||||
args = append(args, "--model", opts.Model)
|
||||
}
|
||||
args = append(args, prompt)
|
||||
|
||||
cmd := exec.CommandContext(runCtx, execPath, args...)
|
||||
if opts.Cwd != "" {
|
||||
cmd.Dir = opts.Cwd
|
||||
}
|
||||
cmd.Env = buildEnv(b.cfg.Env)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("opencode stdout pipe: %w", err)
|
||||
}
|
||||
cmd.Stderr = newLogWriter(b.cfg.Logger, "[opencode:stderr] ")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("start opencode: %w", err)
|
||||
}
|
||||
|
||||
b.cfg.Logger.Info("opencode started", "pid", cmd.Process.Pid, "cwd", opts.Cwd, "model", opts.Model)
|
||||
|
||||
msgCh := make(chan Message, 256)
|
||||
resCh := make(chan Result, 1)
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer close(msgCh)
|
||||
defer close(resCh)
|
||||
|
||||
startTime := time.Now()
|
||||
var output strings.Builder
|
||||
var sessionID string
|
||||
finalStatus := "completed"
|
||||
var finalError string
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var evt opencodeEvent
|
||||
if err := json.Unmarshal([]byte(line), &evt); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if evt.SessionID != "" && sessionID == "" {
|
||||
sessionID = evt.SessionID
|
||||
}
|
||||
|
||||
switch evt.Type {
|
||||
case "text":
|
||||
b.handleText(evt, msgCh, &output)
|
||||
case "tool_use":
|
||||
b.handleToolUse(evt, msgCh)
|
||||
case "step_start":
|
||||
trySend(msgCh, Message{Type: MessageStatus, Status: "running"})
|
||||
case "step_finish":
|
||||
b.handleStepFinish(evt, msgCh)
|
||||
case "error":
|
||||
b.handleError(evt, msgCh, &finalStatus, &finalError)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for process exit
|
||||
exitErr := cmd.Wait()
|
||||
duration := time.Since(startTime)
|
||||
|
||||
if runCtx.Err() == context.DeadlineExceeded {
|
||||
finalStatus = "timeout"
|
||||
finalError = fmt.Sprintf("opencode timed out after %s", timeout)
|
||||
} else if runCtx.Err() == context.Canceled {
|
||||
finalStatus = "aborted"
|
||||
finalError = "execution cancelled"
|
||||
} else if exitErr != nil && finalStatus == "completed" {
|
||||
finalStatus = "failed"
|
||||
finalError = fmt.Sprintf("opencode exited with error: %v", exitErr)
|
||||
}
|
||||
|
||||
b.cfg.Logger.Info("opencode finished", "pid", cmd.Process.Pid, "status", finalStatus, "duration", duration.Round(time.Millisecond).String())
|
||||
|
||||
resCh <- Result{
|
||||
Status: finalStatus,
|
||||
Output: output.String(),
|
||||
Error: finalError,
|
||||
DurationMs: duration.Milliseconds(),
|
||||
SessionID: sessionID,
|
||||
}
|
||||
}()
|
||||
|
||||
return &Session{Messages: msgCh, Result: resCh}, nil
|
||||
}
|
||||
|
||||
func (b *opencodeBackend) handleText(evt opencodeEvent, ch chan<- Message, output *strings.Builder) {
|
||||
text := evt.Part.Text
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
partType := evt.Part.Type
|
||||
switch partType {
|
||||
case "thinking":
|
||||
trySend(ch, Message{Type: MessageThinking, Content: text})
|
||||
default:
|
||||
// "text" or any other type → treat as assistant text output
|
||||
output.WriteString(text)
|
||||
trySend(ch, Message{Type: MessageText, Content: text})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *opencodeBackend) handleToolUse(evt opencodeEvent, ch chan<- Message) {
|
||||
tool := evt.Part.Tool
|
||||
callID := evt.Part.CallID
|
||||
state := evt.Part.State
|
||||
|
||||
var input map[string]any
|
||||
if state.Input != nil {
|
||||
_ = json.Unmarshal(state.Input, &input)
|
||||
}
|
||||
|
||||
if state.Status == "completed" {
|
||||
// Emit both tool_use and tool_result for completed tool calls
|
||||
trySend(ch, Message{
|
||||
Type: MessageToolUse,
|
||||
Tool: tool,
|
||||
CallID: callID,
|
||||
Input: input,
|
||||
})
|
||||
trySend(ch, Message{
|
||||
Type: MessageToolResult,
|
||||
Tool: tool,
|
||||
CallID: callID,
|
||||
Output: state.Output,
|
||||
})
|
||||
} else {
|
||||
// Running or pending tool call
|
||||
trySend(ch, Message{
|
||||
Type: MessageToolUse,
|
||||
Tool: tool,
|
||||
CallID: callID,
|
||||
Input: input,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *opencodeBackend) handleStepFinish(evt opencodeEvent, ch chan<- Message) {
|
||||
reason := evt.Part.Reason
|
||||
status := "step completed"
|
||||
if reason != "" {
|
||||
status = fmt.Sprintf("step finished: %s", reason)
|
||||
}
|
||||
trySend(ch, Message{Type: MessageStatus, Status: status})
|
||||
}
|
||||
|
||||
func (b *opencodeBackend) handleError(evt opencodeEvent, ch chan<- Message, finalStatus, finalError *string) {
|
||||
errMsg := ""
|
||||
if evt.Error != nil {
|
||||
errMsg = evt.Error.Name
|
||||
if evt.Error.Data.Message != "" {
|
||||
errMsg = evt.Error.Data.Message
|
||||
}
|
||||
}
|
||||
if errMsg != "" {
|
||||
*finalStatus = "failed"
|
||||
*finalError = errMsg
|
||||
trySend(ch, Message{Type: MessageError, Content: errMsg})
|
||||
}
|
||||
}
|
||||
|
||||
// ── OpenCode JSON event types ──
|
||||
|
||||
// opencodeEvent represents a single JSONL event from `opencode run --format json`.
|
||||
type opencodeEvent struct {
|
||||
Type string `json:"type"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
Part opencodePartData `json:"part"`
|
||||
Error *opencodeError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// opencodePartData holds the "part" payload — its fields vary by event type.
|
||||
type opencodePartData struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
MessageID string `json:"messageID,omitempty"`
|
||||
Type string `json:"type,omitempty"` // "text", "thinking", "step-start", "step-finish"
|
||||
Text string `json:"text,omitempty"` // for text events
|
||||
Tool string `json:"tool,omitempty"` // for tool_use events (e.g. "bash", "read")
|
||||
CallID string `json:"callID,omitempty"` // unique tool call ID
|
||||
Snapshot string `json:"snapshot,omitempty"`
|
||||
Reason string `json:"reason,omitempty"` // "stop", "tool-calls" — for step_finish
|
||||
Cost float64 `json:"cost,omitempty"`
|
||||
|
||||
State opencodeToolState `json:"state"` // for tool_use events
|
||||
|
||||
Tokens *opencodeTokens `json:"tokens,omitempty"`
|
||||
}
|
||||
|
||||
// opencodeToolState holds tool execution state within a tool_use event.
|
||||
type opencodeToolState struct {
|
||||
Status string `json:"status,omitempty"` // "completed", "running"
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Time float64 `json:"time,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// opencodeTokens tracks token usage from step_finish events.
|
||||
type opencodeTokens struct {
|
||||
Input int `json:"input,omitempty"`
|
||||
Output int `json:"output,omitempty"`
|
||||
Reasoning int `json:"reasoning,omitempty"`
|
||||
Cache int `json:"cache,omitempty"`
|
||||
}
|
||||
|
||||
// opencodeError holds error details from error events.
|
||||
type opencodeError struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Data struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
289
server/pkg/agent/opencode_test.go
Normal file
289
server/pkg/agent/opencode_test.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpencodeHandleText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
var output strings.Builder
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "text",
|
||||
SessionID: "ses_abc123",
|
||||
Part: opencodePartData{
|
||||
Type: "text",
|
||||
Text: "Hello from OpenCode",
|
||||
},
|
||||
}
|
||||
|
||||
b.handleText(evt, ch, &output)
|
||||
|
||||
if output.String() != "Hello from OpenCode" {
|
||||
t.Fatalf("expected output 'Hello from OpenCode', got %q", output.String())
|
||||
}
|
||||
select {
|
||||
case m := <-ch:
|
||||
if m.Type != MessageText || m.Content != "Hello from OpenCode" {
|
||||
t.Fatalf("unexpected message: %+v", m)
|
||||
}
|
||||
default:
|
||||
t.Fatal("expected message on channel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleTextThinking(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
var output strings.Builder
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "text",
|
||||
Part: opencodePartData{
|
||||
Type: "thinking",
|
||||
Text: "Let me think about this...",
|
||||
},
|
||||
}
|
||||
|
||||
b.handleText(evt, ch, &output)
|
||||
|
||||
// Thinking text should NOT go to output
|
||||
if output.String() != "" {
|
||||
t.Fatalf("expected empty output for thinking, got %q", output.String())
|
||||
}
|
||||
select {
|
||||
case m := <-ch:
|
||||
if m.Type != MessageThinking || m.Content != "Let me think about this..." {
|
||||
t.Fatalf("unexpected message: %+v", m)
|
||||
}
|
||||
default:
|
||||
t.Fatal("expected message on channel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleTextEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
var output strings.Builder
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "text",
|
||||
Part: opencodePartData{Type: "text", Text: ""},
|
||||
}
|
||||
|
||||
b.handleText(evt, ch, &output)
|
||||
|
||||
if output.String() != "" {
|
||||
t.Fatalf("expected empty output, got %q", output.String())
|
||||
}
|
||||
select {
|
||||
case m := <-ch:
|
||||
t.Fatalf("expected no message for empty text, got %+v", m)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleToolUseCompleted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "tool_use",
|
||||
Part: opencodePartData{
|
||||
Tool: "bash",
|
||||
CallID: "call-42",
|
||||
State: opencodeToolState{
|
||||
Status: "completed",
|
||||
Input: mustMarshal(t, map[string]any{"command": "ls -la"}),
|
||||
Output: "total 8\ndrwxr-xr-x 2 user user 4096 ...",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.handleToolUse(evt, ch)
|
||||
|
||||
// Should emit both tool_use and tool_result
|
||||
m1 := <-ch
|
||||
if m1.Type != MessageToolUse || m1.Tool != "bash" || m1.CallID != "call-42" {
|
||||
t.Fatalf("unexpected tool_use message: %+v", m1)
|
||||
}
|
||||
if m1.Input["command"] != "ls -la" {
|
||||
t.Fatalf("expected input command 'ls -la', got %v", m1.Input["command"])
|
||||
}
|
||||
|
||||
m2 := <-ch
|
||||
if m2.Type != MessageToolResult || m2.CallID != "call-42" {
|
||||
t.Fatalf("unexpected tool_result message: %+v", m2)
|
||||
}
|
||||
if !strings.Contains(m2.Output, "total 8") {
|
||||
t.Fatalf("expected output containing 'total 8', got %q", m2.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleToolUseRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "tool_use",
|
||||
Part: opencodePartData{
|
||||
Tool: "read",
|
||||
CallID: "call-99",
|
||||
State: opencodeToolState{
|
||||
Status: "running",
|
||||
Input: mustMarshal(t, map[string]any{"path": "/tmp/test.go"}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.handleToolUse(evt, ch)
|
||||
|
||||
// Should emit only tool_use (no result yet)
|
||||
m := <-ch
|
||||
if m.Type != MessageToolUse || m.Tool != "read" || m.CallID != "call-99" {
|
||||
t.Fatalf("unexpected message: %+v", m)
|
||||
}
|
||||
|
||||
select {
|
||||
case extra := <-ch:
|
||||
t.Fatalf("expected no extra message for running tool, got %+v", extra)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleStepFinish(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "step_finish",
|
||||
Part: opencodePartData{
|
||||
Type: "step-finish",
|
||||
Reason: "stop",
|
||||
},
|
||||
}
|
||||
|
||||
b.handleStepFinish(evt, ch)
|
||||
|
||||
m := <-ch
|
||||
if m.Type != MessageStatus || !strings.Contains(m.Status, "stop") {
|
||||
t.Fatalf("unexpected message: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
|
||||
finalStatus := "completed"
|
||||
finalError := ""
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "error",
|
||||
Error: &opencodeError{
|
||||
Name: "RateLimitError",
|
||||
},
|
||||
}
|
||||
evt.Error.Data.Message = "rate limit exceeded"
|
||||
|
||||
b.handleError(evt, ch, &finalStatus, &finalError)
|
||||
|
||||
if finalStatus != "failed" {
|
||||
t.Fatalf("expected status 'failed', got %q", finalStatus)
|
||||
}
|
||||
if finalError != "rate limit exceeded" {
|
||||
t.Fatalf("expected error 'rate limit exceeded', got %q", finalError)
|
||||
}
|
||||
|
||||
m := <-ch
|
||||
if m.Type != MessageError || m.Content != "rate limit exceeded" {
|
||||
t.Fatalf("unexpected message: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeHandleErrorNameOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &opencodeBackend{cfg: Config{Logger: slog.Default()}}
|
||||
ch := make(chan Message, 10)
|
||||
|
||||
finalStatus := "completed"
|
||||
finalError := ""
|
||||
|
||||
evt := opencodeEvent{
|
||||
Type: "error",
|
||||
Error: &opencodeError{
|
||||
Name: "UnknownError",
|
||||
},
|
||||
}
|
||||
|
||||
b.handleError(evt, ch, &finalStatus, &finalError)
|
||||
|
||||
if finalError != "UnknownError" {
|
||||
t.Fatalf("expected error 'UnknownError', got %q", finalError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeEventParsing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := `{"type":"text","timestamp":1767036059338,"sessionID":"ses_abc","part":{"type":"text","text":"Hello"}}`
|
||||
var evt opencodeEvent
|
||||
if err := json.Unmarshal([]byte(raw), &evt); err != nil {
|
||||
t.Fatalf("failed to parse event: %v", err)
|
||||
}
|
||||
|
||||
if evt.Type != "text" {
|
||||
t.Fatalf("expected type 'text', got %q", evt.Type)
|
||||
}
|
||||
if evt.SessionID != "ses_abc" {
|
||||
t.Fatalf("expected sessionID 'ses_abc', got %q", evt.SessionID)
|
||||
}
|
||||
if evt.Part.Text != "Hello" {
|
||||
t.Fatalf("expected part.text 'Hello', got %q", evt.Part.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeToolUseEventParsing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := `{"type":"tool_use","timestamp":1767036060000,"sessionID":"ses_xyz","part":{"callID":"call-1","tool":"bash","state":{"status":"completed","input":{"command":"echo hi"},"output":"hi\n","time":0.5}}}`
|
||||
var evt opencodeEvent
|
||||
if err := json.Unmarshal([]byte(raw), &evt); err != nil {
|
||||
t.Fatalf("failed to parse event: %v", err)
|
||||
}
|
||||
|
||||
if evt.Type != "tool_use" {
|
||||
t.Fatalf("expected type 'tool_use', got %q", evt.Type)
|
||||
}
|
||||
if evt.Part.Tool != "bash" {
|
||||
t.Fatalf("expected tool 'bash', got %q", evt.Part.Tool)
|
||||
}
|
||||
if evt.Part.CallID != "call-1" {
|
||||
t.Fatalf("expected callID 'call-1', got %q", evt.Part.CallID)
|
||||
}
|
||||
if evt.Part.State.Status != "completed" {
|
||||
t.Fatalf("expected state.status 'completed', got %q", evt.Part.State.Status)
|
||||
}
|
||||
if evt.Part.State.Output != "hi\n" {
|
||||
t.Fatalf("expected state.output 'hi\\n', got %q", evt.Part.State.Output)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user