package cli import ( "context" "encoding/json" "fmt" "strings" "time" "iop/apps/node/internal/runtime" ) type opencodeEnvelope struct { Type string `json:"type"` Properties map[string]any `json:"properties"` } type opencodeGlobalEnvelope struct { Payload *opencodeEnvelope `json:"payload"` } func opencodeEventEnvelope(data []byte) (opencodeEnvelope, bool) { var env opencodeEnvelope if err := json.Unmarshal(data, &env); err == nil && env.Type != "" && env.Type != "sync" { return env, true } var global opencodeGlobalEnvelope if err := json.Unmarshal(data, &global); err != nil || global.Payload == nil || global.Payload.Type == "" || global.Payload.Type == "sync" { return opencodeEnvelope{}, false } return *global.Payload, true } type opencodeEventHandler func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) var opencodeEventHandlers = map[string]opencodeEventHandler{ "session.next.text.delta": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { delta := opencodeDeltaText(ev.Properties) if delta != "" { r.outputTokens += len(strings.Fields(delta)) _ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{ RunID: r.spec.RunID, Type: runtime.EventTypeDelta, Delta: delta, Timestamp: time.Now(), }) r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "delta"}) } else { r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"}) } return false, nil }, "message.part.updated": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { if partID, partType, ok := opencodePartInfo(ev.Properties); ok { r.partTypes[partID] = partType } if in, out, ok := opencodeStepTokens(ev.Properties); ok { if in > 0 { r.inputTokens = in } if out > 0 { r.outputTokens = out } } r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "ok"}) return false, nil }, "message.part.delta": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { delta := opencodeMessagePartDeltaText(ev.Properties, r.partTypes) if delta != "" { r.outputTokens += len(strings.Fields(delta)) _ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{ RunID: r.spec.RunID, Type: runtime.EventTypeDelta, Delta: delta, Timestamp: time.Now(), }) r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "delta"}) } else { r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"}) } return false, nil }, "session.next.step.ended": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { if in, out, ok := opencodeStepTokens(ev.Properties); ok { if in > 0 { r.inputTokens = in } if out > 0 { r.outputTokens = out } } r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "ok"}) return false, nil }, "session.idle": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "idle"}) return true, emitOpencodeComplete(r.ctx, r.sink, r.spec.RunID, r.inputTokens, r.outputTokens) }, "session.status": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { if opencodeStatusIdle(ev.Properties) { r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "idle"}) return true, emitOpencodeComplete(r.ctx, r.sink, r.spec.RunID, r.inputTokens, r.outputTokens) } r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"}) return false, nil }, "session.error": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { msg := opencodeErrorMessage(ev.Properties) r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "error"}) _ = r.sink.Emit(r.ctx, runtime.RuntimeEvent{ RunID: r.spec.RunID, Type: runtime.EventTypeError, Error: msg, Timestamp: time.Now(), }) return true, fmt.Errorf("cli adapter: opencode session error: %s", msg) }, "permission.asked": func(r *opencodeSSERun, ev opencodeEnvelope, sidStatus string) (bool, error) { permID, _ := stringFromProp(ev.Properties, "id") if permID == "" { permID, _ = stringFromProp(ev.Properties, "permissionID") } if permID != "" { reply := "reject" if r.opts.DangerouslySkipPermissions { reply = "once" } bg, cancel := context.WithTimeout(context.Background(), 5*time.Second) _ = opencodePermissionReply(bg, r.sess.serverURL, permID, reply) cancel() } r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "ok"}) return false, nil }, } func (r *opencodeSSERun) handleEvent(ev opencodeEnvelope, sidStatus string) (bool, error) { handler := opencodeEventHandlers[ev.Type] if handler == nil { r.addTrace(sseEvtTrace{Type: ev.Type, SID: sidStatus, Outcome: "skip"}) return false, nil } return handler(r, ev, sidStatus) } func stringFromProp(props map[string]any, key string) (string, bool) { if props == nil { return "", false } v, ok := props[key] if !ok { return "", false } s, ok := v.(string) return s, ok } func opencodeDeltaText(props map[string]any) string { if props == nil { return "" } if s, ok := props["delta"].(string); ok && s != "" { return s } if part, ok := props["part"].(map[string]any); ok { if s, ok := part["text"].(string); ok { return s } } return "" } func opencodePartInfo(props map[string]any) (string, string, bool) { if props == nil { return "", "", false } part, ok := props["part"].(map[string]any) if !ok { return "", "", false } id, _ := part["id"].(string) partType, _ := part["type"].(string) if id == "" || partType == "" { return "", "", false } return id, partType, true } func opencodeMessagePartDeltaText(props map[string]any, partTypes map[string]string) string { if props == nil { return "" } field, _ := props["field"].(string) if field != "text" { return "" } partID, _ := props["partID"].(string) if partID != "" { if pt, known := partTypes[partID]; known && pt != "text" { return "" } } delta, _ := props["delta"].(string) return delta } func opencodeStepTokens(props map[string]any) (int, int, bool) { if props == nil { return 0, 0, false } tokens, ok := props["tokens"].(map[string]any) if !ok { if part, partOK := props["part"].(map[string]any); partOK { tokens, ok = part["tokens"].(map[string]any) } } if !ok { return 0, 0, false } in := intFromNumber(tokens["input"]) out := intFromNumber(tokens["output"]) return in, out, true } func intFromNumber(v any) int { switch n := v.(type) { case float64: return int(n) case int: return n case json.Number: if i, err := n.Int64(); err == nil { return int(i) } } return 0 } func emitOpencodeComplete(ctx context.Context, sink runtime.EventSink, runID string, inputTokens, outputTokens int) error { return sink.Emit(ctx, runtime.RuntimeEvent{ RunID: runID, Type: runtime.EventTypeComplete, Message: "opencode sse execution complete", Usage: &runtime.UsageStats{ InputTokens: inputTokens, OutputTokens: outputTokens, }, Timestamp: time.Now(), }) } func opencodeModelPayload(s string) (map[string]any, error) { idx := strings.Index(s, "/") if idx < 0 { return nil, fmt.Errorf("cli adapter: opencode model %q missing provider/model separator", s) } provider := s[:idx] model := s[idx+1:] if provider == "" || model == "" { return nil, fmt.Errorf("cli adapter: opencode model %q has empty provider or model", s) } return map[string]any{"providerID": provider, "modelID": model}, nil } func opencodeStatusIdle(props map[string]any) bool { if props == nil { return false } switch v := props["status"].(type) { case string: return v == "idle" case map[string]any: t, _ := v["type"].(string) return t == "idle" } return false } func opencodeErrorMessage(props map[string]any) string { if props == nil { return "opencode error" } if errObj, ok := props["error"].(map[string]any); ok { if data, ok := errObj["data"].(map[string]any); ok { if m, ok := data["message"].(string); ok && m != "" { return m } } if name, ok := errObj["name"].(string); ok && name != "" { return name } } if m, ok := props["message"].(string); ok && m != "" { return m } return "opencode error" }