156 lines
3.6 KiB
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] + "..."
|
|
}
|