iop/apps/node/internal/adapters/cli/emitter_stream_json.go

156 lines
3.6 KiB
Go

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] + "..."
}