Class: LittleGhost::AGUI::Adapter

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/ag_ui/adapter.rb

Overview

Adapter turns a LittleGhost stream into AG-UI event hashes. It lets a Ruby agent drive compatible chat interfaces without changing the agent itself.

events = CustomerSupportAgent.stream_ask("Where is my order?")
adapter = LittleGhost::AGUI::Adapter.new
adapter.stream(events, thread_id: "thread-1", run_id: "run-1").each do |event|
websocket.write(JSON.generate(event))
end

The adapter has no state between #stream calls, so one instance can translate independent runs.

Security and trust

Provider plaintext reasoning becomes AG-UI reasoning events. Tool arguments and results, invocation metadata, subagent events, trace context, and selected error text also pass through without redaction. Authorize and filter the complete stream before transport, and send it only to an interface trusted to display that data. Encrypted reasoning and provider continuity artifacts are never exposed here.

Constant Summary collapse

TERMINAL_EVENTS =

:nodoc:

%i[run_partial run_cancel run_stop run_error].freeze

Instance Method Summary collapse

Instance Method Details

#stream(events, thread_id:, run_id:) ⇒ Object

Lazily translates events for one AG-UI run.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/little_ghost/ag_ui/adapter.rb', line 33

def stream(events, thread_id:, run_id:)
  Enumerator.new do |output|
    message_id = nil
    message_started = false
    reasoning_id = nil
    reasoning_message_id = nil
    tool_call_ids = {}

    events.each do |source|
      superseded_message_id = message_id if source.type == :model_retry && message_started
      if reasoning_id && source.type != :reasoning_delta
        output << event("REASONING_MESSAGE_END", messageId: reasoning_message_id)
        output << event("REASONING_END", messageId: reasoning_id)
        reasoning_id = nil
        reasoning_message_id = nil
      end
      if message_started && (TERMINAL_EVENTS.include?(source.type) || source.type == :model_retry)
        output << event("TEXT_MESSAGE_END", messageId: message_id)
        message_id = nil
        message_started = false
      end
      if message_started && source.type == :message_start
        output << event("TEXT_MESSAGE_END", messageId: message_id)
        message_id = nil
        message_started = false
      end

      case source.type
      when :run_start
        output << event("RUN_STARTED", threadId: thread_id, runId: run_id)
      when :message_start
        message_id = SecureRandom.uuid
      when :reasoning_delta
        if message_started
          output << event("TEXT_MESSAGE_END", messageId: message_id)
          message_id = nil
          message_started = false
        end
        unless reasoning_id
          reasoning_id = SecureRandom.uuid
          reasoning_message_id = SecureRandom.uuid
          output << event("REASONING_START", messageId: reasoning_id)
          output << event(
            "REASONING_MESSAGE_START",
            messageId: reasoning_message_id,
            role: "reasoning"
          )
        end
        output << event(
          "REASONING_MESSAGE_CONTENT",
          messageId: reasoning_message_id,
          delta: source.data.fetch(:text)
        )
      when :text_delta
        message_id ||= SecureRandom.uuid
        unless message_started
          output << event("TEXT_MESSAGE_START", messageId: message_id, role: "assistant")
          message_started = true
        end
        output << event("TEXT_MESSAGE_CONTENT", messageId: message_id, delta: source.data.fetch(:text))
      when :message_stop
        if message_started
          output << event("TEXT_MESSAGE_END", messageId: message_id)
        end
        message_id = nil
        message_started = false
      when :tool_call_start
        tool_call_ids[source.data.fetch(:index)] = source.data.fetch(:id)
        output << event(
          "TOOL_CALL_START",
          toolCallId: source.data.fetch(:id),
          toolCallName: source.data.fetch(:name),
          parentMessageId: (message_id if message_started)
        )
      when :tool_call_delta
        output << event(
          "TOOL_CALL_ARGS",
          toolCallId: tool_call_ids.fetch(source.data.fetch(:index), source.data.fetch(:index).to_s),
          delta: source.data.fetch(:arguments)
        )
      when :tool_call_stop
        output << event("TOOL_CALL_END", toolCallId: source.data.fetch(:tool_use).id)
      when :tool_stop
        tool_use = source.data.fetch(:tool_use)
        result = source.data.fetch(:result)
        output << event(
          "TOOL_CALL_RESULT",
          messageId: SecureRandom.uuid,
          toolCallId: tool_use.id,
          content: result.content,
          status: result.status,
          role: "tool"
        )
      when :invocation_stop
        result = source.data.fetch(:result)
        output << custom(
          "little_ghost.usage",
          usage: result.usage.to_h,
          metadata: source.data.fetch(:metadata, {})
        )
      when :invocation_error
        output << custom(
          "little_ghost.usage",
          usage: source.data.fetch(:usage).to_h,
          metadata: source.data.fetch(:metadata, {})
        )
      when :model_retry
        tool_call_ids.clear
        output << custom(
          "little_ghost.model_retry",
          source.data.merge(superseded_message_id:).compact
        )
      when :agent_interrupt_delivered
        output << custom(
          "little_ghost.agent_interrupt_delivered",
          source.data.slice(:interruption_ids, :batch_key).compact
        )
      when :subagent
        output << custom("little_ghost.subagent", source.data.fetch(:event, source.data))
      when :trace_context
        output << custom("little_ghost.trace_context", source.data.fetch(:context, source.data))
      when :run_partial
        output << custom(
          "little_ghost.run.partial",
          response: source.data.fetch(:response),
          message: source.data[:error]&.message
        )
        output << event(
          "RUN_FINISHED", threadId: thread_id, runId: run_id,
          result: {response: source.data.fetch(:response)}
        )
      when :run_cancel
        output << custom("little_ghost.run.canceled", reason: source.data[:error]&.message)
        output << event("RUN_FINISHED", threadId: thread_id, runId: run_id)
      when :run_stop
        output << event(
          "RUN_FINISHED", threadId: thread_id, runId: run_id,
          result: {response: source.data.fetch(:response)}
        )
      when :run_error
        output << event(
          "RUN_ERROR", threadId: thread_id, runId: run_id,
          message: source.data.fetch(:message),
          cleanupFailed: source.data.fetch(:cleanup_failed, true)
        )
      end
    end
  end
end