package cli import ( "encoding/json" "fmt" "strings" "iop/apps/node/internal/runtime" ) type streamJSONEmitter struct{} 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 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 } switch ev.Type { case "message": if ev.Role != "assistant" || ev.Content == "" { return nil, nil } return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: ev.Content, }}, nil case "tool_use": if delta := streamToolUseDelta(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 := streamToolResultDelta(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 = streamErrorString(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: streamErrorString(ev.Error), }}, nil } return nil, nil } func streamToolUseDelta(toolName string, params map[string]any) string { if toolName == "" { return "" } summary := streamToolParamSummary(params) if summary == "" { return fmt.Sprintf("[tool] %s\n", toolName) } return fmt.Sprintf("[tool] %s: %s\n", toolName, summary) } func streamToolParamSummary(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 truncateStreamToolText(strings.TrimSpace(s), 180) } } return "" } func streamToolResultDelta(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, truncateStreamToolText(output, 180)) } func streamErrorString(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 truncateStreamToolText(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] + "..." }