package cli import ( "bufio" "context" "encoding/json" "fmt" "io" "strings" "time" "iop/apps/node/internal/runtime" ) // lineEmitter parses one stdout line (already trimmed of trailing newline) and // returns the resulting RuntimeEvents to push to the sink. Empty events are // skipped. Returning a non-nil error aborts the run. type lineEmitter interface { Name() string Emit(line string) ([]runtime.RuntimeEvent, error) } // registeredEmitter wraps a lineEmitter with its maximum scanner buffer size. type registeredEmitter struct { emitter lineEmitter scanBufMax int } // jsonEmitters maps OutputFormat names to their registered emitter + buffer config. var jsonEmitters = map[string]registeredEmitter{ "stream-json": {emitter: streamJSONEmitter{}, scanBufMax: 4 * 1024 * 1024}, "claude-json": {emitter: claudeJSONEmitter{}, scanBufMax: 8 * 1024 * 1024}, "codex-json": {emitter: codexJSONEmitter{}, scanBufMax: 4 * 1024 * 1024}, "opencode-json": {emitter: opencodeJSONEmitter{}, scanBufMax: 4 * 1024 * 1024}, "cline-json": {emitter: clineJSONEmitter{}, scanBufMax: 8 * 1024 * 1024}, } // driveJSONLines runs a shared scanner loop over stdout, accumulates raw output // into outBuf, dispatches each line through the emitter, and forwards events // to sink. Returns total OutputTokens approximation. func driveJSONLines( ctx context.Context, stdout io.Reader, sink runtime.EventSink, runID string, outBuf *strings.Builder, emitter lineEmitter, scanBufMax int, ) (int, error) { scanner := bufio.NewScanner(stdout) scanner.Buffer(make([]byte, 64*1024), scanBufMax) outputTokens := 0 for scanner.Scan() { line := scanner.Bytes() outBuf.Write(line) outBuf.WriteByte('\n') trimmed := strings.TrimSpace(string(line)) if trimmed == "" || trimmed[0] != '{' { continue } events, err := emitter.Emit(trimmed) if err != nil { return outputTokens, err } for _, ev := range events { ev.RunID = runID if ev.Timestamp.IsZero() { ev.Timestamp = time.Now() } if ev.Type == runtime.EventTypeDelta { outputTokens += len(strings.Fields(ev.Delta)) } _ = sink.Emit(ctx, ev) } } if err := scanner.Err(); err != nil { return outputTokens, err } return outputTokens, nil } // --- stream-json emitter --- 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] + "..." } // --- claude-json emitter --- type claudeJSONEmitter struct{} func (claudeJSONEmitter) Name() string { return "claude-json" } func (e claudeJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) { var ev struct { Type string `json:"type"` Event struct { Type string `json:"type"` Delta struct { Type string `json:"type"` Text string `json:"text"` } `json:"delta"` } `json:"event"` IsError bool `json:"is_error"` Result string `json:"result"` } if err := json.Unmarshal([]byte(line), &ev); err != nil { return nil, nil } switch ev.Type { case "stream_event": if ev.Event.Type == "content_block_delta" && ev.Event.Delta.Type == "text_delta" && ev.Event.Delta.Text != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: ev.Event.Delta.Text, }}, nil } return nil, nil case "result": if ev.IsError && ev.Result != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: ev.Result, }}, nil } return nil, nil } return nil, nil } // --- codex-json emitter --- type codexJSONEmitter struct{} func (codexJSONEmitter) Name() string { return "codex-json" } func (e codexJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) { var ev struct { Type string `json:"type"` Item struct { Type string `json:"type"` Text string `json:"text"` Delta string `json:"delta"` Message struct { Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` } `json:"message"` } `json:"item"` Delta string `json:"delta"` Text string `json:"text"` Message string `json:"message"` Error struct { Message string `json:"message"` } `json:"error"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` } if err := json.Unmarshal([]byte(line), &ev); err != nil { return nil, nil } switch ev.Type { case "item.delta", "item.updated", "response.output_text.delta", "output_text.delta": if delta := codexDeltaText(ev); delta != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: delta, }}, nil } return nil, nil case "item.completed": if ev.Item.Type == "agent_message" && ev.Item.Text != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: ev.Item.Text, }}, nil } if delta := codexDeltaText(ev); delta != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: delta, }}, nil } return nil, nil case "error": return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: ev.Message, }}, nil case "turn.failed": return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: ev.Error.Message, }}, nil } return nil, nil } func codexDeltaText(ev struct { Type string `json:"type"` Item struct { Type string `json:"type"` Text string `json:"text"` Delta string `json:"delta"` Message struct { Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` } `json:"message"` } `json:"item"` Delta string `json:"delta"` Text string `json:"text"` Message string `json:"message"` Error struct { Message string `json:"message"` } `json:"error"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` }) string { if ev.Item.Delta != "" { return ev.Item.Delta } if ev.Delta != "" { return ev.Delta } if ev.Text != "" { return ev.Text } for _, part := range ev.Item.Message.Content { if part.Type == "output_text" && part.Text != "" { return part.Text } } for _, part := range ev.Content { if part.Type == "output_text" && part.Text != "" { return part.Text } } return "" } // --- opencode-json emitter --- type opencodeJSONEmitter struct{} func (opencodeJSONEmitter) Name() string { return "opencode-json" } func (e opencodeJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) { var ev struct { Type string `json:"type"` Error struct { Name string `json:"name"` Data struct { Message string `json:"message"` } `json:"data"` } `json:"error"` Part struct { Type string `json:"type"` Text string `json:"text"` } `json:"part"` } if err := json.Unmarshal([]byte(line), &ev); err != nil { return nil, nil } switch ev.Type { case "text": if ev.Part.Type == "text" && ev.Part.Text != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: ev.Part.Text, }}, nil } return nil, nil case "error": msg := ev.Error.Data.Message if msg == "" { msg = ev.Error.Name } if msg != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: msg, }}, nil } return nil, nil } return nil, nil } // --- cline-json emitter --- type clineJSONEmitter struct{} func (clineJSONEmitter) Name() string { return "cline-json" } func (e clineJSONEmitter) Emit(line string) ([]runtime.RuntimeEvent, error) { var ev struct { Type string `json:"type"` Say string `json:"say"` Ask string `json:"ask"` Text string `json:"text"` Message string `json:"message"` Status string `json:"status"` Error string `json:"error"` } if err := json.Unmarshal([]byte(line), &ev); err != nil { return nil, nil } switch { case ev.Type == "say" && ev.Say == "text" && ev.Text != "": return []runtime.RuntimeEvent{{ Type: runtime.EventTypeDelta, Delta: ev.Text, }}, nil case ev.Type == "say" && ev.Say == "error": msg := ev.Text if msg == "" { msg = ev.Message } if msg != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: msg, }}, nil } return nil, nil case ev.Type == "completion" && ev.Status == "error": msg := ev.Error if msg == "" { msg = ev.Message } if msg == "" { msg = "cline task failed" } return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: msg, }}, nil case ev.Type == "ask" && ev.Ask == "api_req_failed": msg := ev.Text if msg == "" { msg = ev.Message } if msg != "" { return []runtime.RuntimeEvent{{ Type: runtime.EventTypeError, Error: msg, }}, nil } return nil, nil } return nil, nil }