package agent import ( "encoding/json" "errors" "fmt" "time" "git.ipao.vip/rogee/go-sip/internal/contract" ) // EventWriter keeps Agent-produced realtime facts in the approved event // vocabulary before they are handed to Dispatcher/MQ. Transcript text is // written to the Agent spool as an archive as well as returned for realtime // publication; the archive is not a substitute for transcript.updated. type EventWriter struct { TenantID string TenantKey string TraceID string } func (w EventWriter) TranscriptUpdated(now time.Time, eventID, callID, turnID, segmentID, role, text string, revision int64, final bool, startMS, endMS int64) ([]byte, error) { if eventID == "" || callID == "" || turnID == "" || segmentID == "" || role == "" { return nil, errors.New("transcript event identity is required") } if revision < 1 || startMS < 0 || endMS < startMS { return nil, errors.New("transcript timing or revision is invalid") } return (contract.EventBuilder{ TenantID: w.TenantID, TenantKey: w.TenantKey, TraceID: w.TraceID, EventType: "transcript.updated", Aggregate: "transcript_segment", AggregateID: segmentID, Version: revision, Payload: map[string]any{ "call_id": callID, "turn_id": turnID, "segment_id": segmentID, "role": role, "revision": revision, "text": text, "is_final": final, "start_ms": startMS, "end_ms": endMS, "playback_state": "not_applicable", }, }).Marshal(now, eventID) } func (w EventWriter) RecordingReady(now time.Time, eventID, callID, recordingID, ossID, format string, channels int32, sampleRateHz int32, durationMS, sizeBytes int64, checksum string) ([]byte, error) { if eventID == "" || callID == "" || recordingID == "" || ossID == "" || checksum == "" { return nil, errors.New("verified recording identity and checksum are required") } if sizeBytes < 1 || durationMS < 0 || channels != 1 || sampleRateHz < 8000 { return nil, errors.New("recording metadata is invalid") } return (contract.EventBuilder{ TenantID: w.TenantID, TenantKey: w.TenantKey, TraceID: w.TraceID, EventType: "recording.ready", Aggregate: "recording", AggregateID: recordingID, Version: 1, Payload: map[string]any{ "call_id": callID, "recording_id": recordingID, "oss_id": ossID, "format": format, "channels": channels, "sample_rate_hz": sampleRateHz, "duration_ms": durationMS, "size_bytes": sizeBytes, "checksum_sha256": checksum, }, }).Marshal(now, eventID) } func (s *Spool) AppendApprovedEvent(executionID string, event []byte) error { var envelope struct { EventType string `json:"event_type"` } if err := json.Unmarshal(event, &envelope); err != nil { return fmt.Errorf("decode event envelope: %w", err) } if envelope.EventType != "transcript.updated" { return fmt.Errorf("Agent transcript archive accepts transcript.updated only, got %q", envelope.EventType) } if err := contract.ValidateEvent(event); err != nil { return err } return s.AppendTranscript(executionID, event) }