update cli emitters and edge config

This commit is contained in:
toki 2026-05-12 14:43:11 +09:00
parent 4b1b5472af
commit 1a72d9caa6
3 changed files with 158 additions and 6 deletions

View file

@ -211,6 +211,42 @@ func TestStreamJSONEmitter_ErrorEvent(t *testing.T) {
}
}
func TestStreamJSONEmitter_ToolUseBecomesVisibleDelta(t *testing.T) {
e := streamJSONEmitter{}
events, _ := e.Emit(`{"type":"tool_use","tool_name":"run_shell_command","tool_id":"tool-1","parameters":{"command":"go test ./..."}}`)
if len(events) != 1 || events[0].Type != runtime.EventTypeDelta {
t.Fatalf("unexpected events: %+v", events)
}
if events[0].Delta != "[tool] run_shell_command: go test ./...\n" {
t.Fatalf("unexpected delta: %q", events[0].Delta)
}
if events[0].Metadata["kind"] != "tool_use" || events[0].Metadata["tool_name"] != "run_shell_command" {
t.Fatalf("unexpected metadata: %+v", events[0].Metadata)
}
}
func TestStreamJSONEmitter_ToolResultBecomesVisibleDelta(t *testing.T) {
e := streamJSONEmitter{}
events, _ := e.Emit(`{"type":"tool_result","tool_id":"tool-1","status":"success","output":"ok\n"}`)
if len(events) != 1 || events[0].Type != runtime.EventTypeDelta {
t.Fatalf("unexpected events: %+v", events)
}
if events[0].Delta != "[tool-result] success: ok\n" {
t.Fatalf("unexpected delta: %q", events[0].Delta)
}
if events[0].Metadata["kind"] != "tool_result" {
t.Fatalf("unexpected metadata: %+v", events[0].Metadata)
}
}
func TestStreamJSONEmitter_ResultError(t *testing.T) {
e := streamJSONEmitter{}
events, _ := e.Emit(`{"type":"result","status":"error","error":{"message":"quota exceeded"}}`)
if len(events) != 1 || events[0].Type != runtime.EventTypeError || events[0].Error != "quota exceeded" {
t.Fatalf("unexpected events: %+v", events)
}
}
func TestStreamJSONEmitter_EmptyContentSkipped(t *testing.T) {
e := streamJSONEmitter{}
events, _ := e.Emit(`{"type":"message","role":"assistant","content":""}`)

View file

@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
@ -91,10 +92,16 @@ func (streamJSONEmitter) Name() string { return "stream-json" }
func (e streamJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) {
var ev struct {
Type string `json:"type"`
Role string `json:"role"`
Content string `json:"content"`
Error string `json:"error"`
Type string `json:"type"`
Role string `json:"role"`
Content string `json:"content"`
Error any `json:"error"`
Message string `json:"message"`
Status string `json:"status"`
ToolName string `json:"tool_name"`
ToolID string `json:"tool_id"`
Parameters map[string]any `json:"parameters"`
Output string `json:"output"`
}
if err := json.Unmarshal([]byte(line), &ev); err != nil {
return nil, nil
@ -108,15 +115,124 @@ func (e streamJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) {
Type: runtime.EventTypeDelta,
Delta: ev.Content,
}}, nil
case "tool_use":
if delta := geminiToolUseDelta(ev.ToolName, ev.Parameters); delta != "" {
return []runtime.RuntimeEvent{{
Type: runtime.EventTypeDelta,
Delta: delta,
Metadata: map[string]string{
"kind": "tool_use",
"tool_name": ev.ToolName,
"tool_id": ev.ToolID,
},
}}, nil
}
return nil, nil
case "tool_result":
if delta := geminiToolResultDelta(ev.Status, ev.Output); delta != "" {
return []runtime.RuntimeEvent{{
Type: runtime.EventTypeDelta,
Delta: delta,
Metadata: map[string]string{
"kind": "tool_result",
"tool_id": ev.ToolID,
},
}}, nil
}
return nil, nil
case "result":
if ev.Status != "" && ev.Status != "success" {
msg := ev.Message
if msg == "" {
msg = geminiErrorString(ev.Error)
}
if msg == "" {
msg = ev.Status
}
return []runtime.RuntimeEvent{{
Type: runtime.EventTypeError,
Error: msg,
}}, nil
}
return nil, nil
case "error":
return []runtime.RuntimeEvent{{
Type: runtime.EventTypeError,
Error: ev.Error,
Error: geminiErrorString(ev.Error),
}}, nil
}
return nil, nil
}
func geminiToolUseDelta(toolName string, params map[string]any) string {
if toolName == "" {
return ""
}
summary := geminiToolParamSummary(params)
if summary == "" {
return fmt.Sprintf("[tool] %s\n", toolName)
}
return fmt.Sprintf("[tool] %s: %s\n", toolName, summary)
}
func geminiToolParamSummary(params map[string]any) string {
if params == nil {
return ""
}
for _, key := range []string{"command", "description", "title", "summary"} {
if s, ok := params[key].(string); ok && strings.TrimSpace(s) != "" {
return truncateGeminiToolText(strings.TrimSpace(s), 180)
}
}
return ""
}
func geminiToolResultDelta(status, output string) string {
status = strings.TrimSpace(status)
output = strings.TrimSpace(output)
if status == "" && output == "" {
return ""
}
if status == "" {
status = "done"
}
if output == "" {
return fmt.Sprintf("[tool-result] %s\n", status)
}
return fmt.Sprintf("[tool-result] %s: %s\n", status, truncateGeminiToolText(output, 180))
}
func geminiErrorString(v any) string {
switch err := v.(type) {
case string:
return err
case map[string]any:
if msg, ok := err["message"].(string); ok {
return msg
}
if data, ok := err["data"].(map[string]any); ok {
if msg, ok := data["message"].(string); ok {
return msg
}
}
if name, ok := err["name"].(string); ok {
return name
}
}
return ""
}
func truncateGeminiToolText(s string, max int) string {
s = strings.Join(strings.Fields(s), " ")
if len(s) <= max {
return s
}
if max <= 3 {
return s[:max]
}
return s[:max-3] + "..."
}
// --- claude-json emitter ---
type claudeJSONEmitter struct{}

View file

@ -17,7 +17,7 @@ metrics:
console:
adapter: "cli"
target: "claude"
target: "gemini"
session_id: "default"
background: false
timeout_sec: 300