ag-ui-protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,717 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require_relative "../util"
6
+
7
+ module AgUiProtocol
8
+ module Core
9
+ # The Agent User Interaction Protocol Ruby SDK is built on a set of core types
10
+ # that represent the fundamental structures used throughout the system. This page
11
+ # documents these types and their properties.
12
+ #
13
+ # ## Message Types
14
+ #
15
+ # The SDK includes several message types that represent different kinds of
16
+ # messages in the system.
17
+ #
18
+ module Types
19
+ # Represents the possible roles a message sender can have.
20
+ #
21
+ # ```ruby
22
+ #
23
+ # AgUiProtocol::Core::Types::Role
24
+ # # => ["developer", "system", "assistant", "user", "tool", "activity"]
25
+ #
26
+ # ```
27
+ # @category Message Types
28
+ Role = ["developer", "system", "assistant", "user", "tool", "activity"].freeze
29
+
30
+ # Base model for protocol entities.
31
+ #
32
+ # Subclasses should implement {#to_h}. JSON serialization is derived from
33
+ # that hash via {#as_json} and {#to_json}.
34
+ class Model
35
+ extend T::Sig
36
+
37
+ # Returns a Ruby Hash representation using snake_case keys or raise NotImplementedError in case of not implemented.
38
+ #
39
+ # Subclasses override this method to provide their shape.
40
+ #
41
+ # @return [Hash<Symbol, Object>, raise NotImplementedError]
42
+ sig { returns(T::Hash[Symbol, T.untyped]) }
43
+ def to_h
44
+ raise NotImplementedError, 'Implement this method by concrect class'
45
+ end
46
+
47
+ # Returns a JSON-ready representation.
48
+ #
49
+ # This converts keys to camelCase and removes nil values recursively.
50
+ #
51
+ # @return [Object]
52
+ sig { returns(T.untyped) }
53
+ def as_json
54
+ AgUiProtocol::Util.deep_transform_keys_to_camel(AgUiProtocol::Util.deep_compact(to_h))
55
+ end
56
+
57
+ # Serializes the model to a JSON string.
58
+ #
59
+ # @param _args [Array<Object>] Unused; kept for compatibility with ActiveSupport.
60
+ # @return [String]
61
+ sig { params(_args: T.untyped).returns(String) }
62
+ def to_json(*_args)
63
+ AgUiProtocol::Util.dump_json(as_json)
64
+ end
65
+ end
66
+
67
+ # Function invocation descriptor used inside tool calls.
68
+ #
69
+ # ```ruby
70
+ #
71
+ # fn = AgUiProtocol::Core::Types::FunctionCall.new(
72
+ # name: "search",
73
+ # arguments: "{\"q\":\"AG-UI\"}"
74
+ # )
75
+ #
76
+ # ```
77
+ # @category ToolCall
78
+ class FunctionCall < Model
79
+ sig { returns(String) }
80
+ attr_reader :name
81
+
82
+ sig { returns(String) }
83
+ attr_reader :arguments
84
+
85
+ # @param name [String] Function name.
86
+ # @param arguments [String] JSON-encoded arguments.
87
+ sig { params(name: String, arguments: String).void }
88
+ def initialize(name:, arguments:)
89
+ @name = name
90
+ @arguments = arguments
91
+ end
92
+
93
+ sig { returns(T::Hash[Symbol, T.untyped]) }
94
+ def to_h
95
+ {
96
+ name: @name,
97
+ arguments: @arguments
98
+ }
99
+ end
100
+ end
101
+
102
+ # Tool calls are embedded within assistant messages.
103
+ #
104
+ # ```ruby
105
+ #
106
+ # tool_call = AgUiProtocol::Core::Types::ToolCall.new(
107
+ # id: "tc_1",
108
+ # function: { name: "search", arguments: "{\"q\":\"AG-UI\"}" }
109
+ # )
110
+ #
111
+ # ```
112
+ class ToolCall < Model
113
+ sig { returns(String) }
114
+ attr_reader :id
115
+
116
+ sig { returns(String) }
117
+ attr_reader :type
118
+
119
+ sig { returns(FunctionCall) }
120
+ attr_reader :function
121
+
122
+ # @param id [String] Unique identifier for the tool call
123
+ # @param function [FunctionCall, Hash] Function name and arguments
124
+ # @param type [String] Type of the tool call
125
+ sig { params(id: String, function: T.untyped, type: String).void }
126
+ def initialize(id:, function:, type: 'function')
127
+ @id = id
128
+ @type = type
129
+ @function = function.is_a?(FunctionCall) ? function : FunctionCall.new(**function)
130
+ end
131
+
132
+ sig { returns(T::Hash[Symbol, T.untyped]) }
133
+ def to_h
134
+ {
135
+ id: @id,
136
+ type: @type,
137
+ function: @function
138
+ }
139
+ end
140
+ end
141
+
142
+ # Base class for message shapes.
143
+ class BaseMessage < Model
144
+ sig { returns(String) }
145
+ attr_reader :id
146
+
147
+ sig { returns(String) }
148
+ attr_reader :role
149
+
150
+ sig { returns(T.nilable(T.any(String, T::Array[T.any(TextInputContent, BinaryInputContent)]))) }
151
+ attr_reader :content
152
+
153
+ sig { returns(T.nilable(String)) }
154
+ attr_reader :name
155
+
156
+ # @param id [String] Unique identifier for the message
157
+ # @param role [String] Role of the message sender
158
+ # @param content [Object] Text content of the message
159
+ # @param name [String] Optional name of the sender
160
+ sig { params(id: String, role: String, content: T.nilable(T.any(String, T::Array[T.any(TextInputContent, BinaryInputContent)])), name: T.nilable(String)).void }
161
+ def initialize(id:, role:, content: nil, name: nil)
162
+ @id = id
163
+ @role = role
164
+ @content = content
165
+ @name = name
166
+ end
167
+
168
+ sig { returns(T::Hash[Symbol, T.untyped]) }
169
+ def to_h
170
+ {
171
+ id: @id,
172
+ role: @role,
173
+ content: @content,
174
+ name: @name
175
+ }
176
+ end
177
+ end
178
+
179
+ # Represents a message from a developer.
180
+ #
181
+ # ```ruby
182
+ #
183
+ # msg = AgUiProtocol::Core::Types::DeveloperMessage.new(
184
+ # id: "dev_1",
185
+ # content: "You are a helpful assistant."
186
+ # )
187
+ #
188
+ # ```
189
+ # @category Message Types
190
+ class DeveloperMessage < BaseMessage
191
+
192
+ sig { returns(String) }
193
+ attr_reader :content
194
+
195
+ # @param id [String] Unique identifier for the message
196
+ # @param content [Object] Text content of the message (required)
197
+ # @param name [String] Optional name of the sender
198
+ sig { params(id: String, content: String, name: T.nilable(String)).void }
199
+ def initialize(id:, content:, name: nil)
200
+ super(id: id, role: "developer", content: content, name: name)
201
+ end
202
+ end
203
+
204
+ # Represents a system message.
205
+ #
206
+ # ```ruby
207
+ #
208
+ # msg = AgUiProtocol::Core::Types::SystemMessage.new(
209
+ # id: "sys_1",
210
+ # content: "Follow the protocol."
211
+ # )
212
+ #
213
+ # ```
214
+ # @category Message Types
215
+ class SystemMessage < BaseMessage
216
+
217
+ # @param id [String] Unique identifier for the message
218
+ # @param content [Object] Text content of the message (required)
219
+ # @param name [String] Optional name of the sender
220
+ sig { params(id: String, content: String, name: T.nilable(String)).void }
221
+ def initialize(id:, content:, name: nil)
222
+ super(id: id, role: "system", content: content, name: name)
223
+ end
224
+ end
225
+
226
+ # Represents a message from an assistant.
227
+ #
228
+ # ```ruby
229
+ #
230
+ # msg = AgUiProtocol::Core::Types::AssistantMessage.new(
231
+ # id: "asst_1",
232
+ # content: "Hello!",
233
+ # tool_calls: [
234
+ # {
235
+ # id: "tc_1",
236
+ # function: { name: "search", arguments: "{\"q\":\"AG-UI\"}" }
237
+ # }
238
+ # ]
239
+ # )
240
+ #
241
+ # ```
242
+ # @category Message Types
243
+ class AssistantMessage < BaseMessage
244
+
245
+ sig { returns(T.nilable(T::Array[ToolCall])) }
246
+ attr_reader :tool_calls
247
+
248
+ # @param id [String] Unique identifier for the message
249
+ # @param content [Object] Text content of the message
250
+ # @param tool_calls [Array<ToolCall, Hash>] Tool calls made in this message
251
+ # @param name [String] Name of the sender
252
+ sig { params(id: String, content: T.untyped, tool_calls: T.nilable(T::Array[ToolCall]), name: T.nilable(String)).void }
253
+ def initialize(id:, content: nil, tool_calls: nil, name: nil)
254
+ super(id: id, role: "assistant", content: content, name: name)
255
+ @tool_calls = tool_calls&.map do |tc|
256
+ tc.is_a?(ToolCall) ? tc : ToolCall.new(**tc)
257
+ end
258
+ end
259
+
260
+ sig { returns(T::Hash[Symbol, T.untyped]) }
261
+ def to_h
262
+ super.merge(tool_calls: @tool_calls)
263
+ end
264
+ end
265
+
266
+ # Represents a text fragment inside a multimodal user message.
267
+ #
268
+ # ```ruby
269
+ #
270
+ # content = AgUiProtocol::Core::Types::TextInputContent.new(text: "hello")
271
+ #
272
+ # ```
273
+ # @category Message Types
274
+ class TextInputContent < Model
275
+ sig { returns(String) }
276
+ attr_reader :type
277
+
278
+ sig { returns(String) }
279
+ attr_reader :text
280
+
281
+ # @param text [String] Text content
282
+ # @param type [String] Identifies the fragment type
283
+ sig { params(text: String, type: String).void }
284
+ def initialize(text:, type: "text")
285
+ @type = type
286
+ @text = text
287
+ end
288
+
289
+ sig { returns(T::Hash[Symbol, T.untyped]) }
290
+ def to_h
291
+ {
292
+ type: @type,
293
+ text: @text
294
+ }
295
+ end
296
+ end
297
+
298
+ # Represents binary data such as images, audio, or files.
299
+ #
300
+ # ```ruby
301
+ #
302
+ # content = AgUiProtocol::Core::Types::BinaryInputContent.new(
303
+ # mime_type: "image/png",
304
+ # url: "https://example.com/cat.png"
305
+ # )
306
+ #
307
+ # ```
308
+ #
309
+ # > **Validation:** At least one of `id`, `url`, or `data` must be provided.
310
+ # @category Message Types
311
+ class BinaryInputContent < Model
312
+ sig { returns(String) }
313
+ attr_reader :type
314
+
315
+ sig { returns(String) }
316
+ attr_reader :mime_type
317
+
318
+ sig { returns(T.nilable(String)) }
319
+ attr_reader :id
320
+
321
+ sig { returns(T.nilable(String)) }
322
+ attr_reader :url
323
+
324
+ sig { returns(T.nilable(String)) }
325
+ attr_reader :data
326
+
327
+ sig { returns(T.nilable(String)) }
328
+ attr_reader :filename
329
+
330
+ # @param type [String] Identifies the fragment type
331
+ # @param mime_type [String] MIME type, for example `"image/png"`
332
+ # @param id [String] Reference to previously uploaded content
333
+ # @param url [String] Remote URL where the content can be retrieved
334
+ # @param data [String] Base64 encoded content
335
+ # @param filename [String] Optional filename hint
336
+ sig do
337
+ params(
338
+ mime_type: String,
339
+ type: String,
340
+ id: T.nilable(String),
341
+ url: T.nilable(String),
342
+ data: T.nilable(String),
343
+ filename: T.nilable(String)
344
+ ).void
345
+ end
346
+ def initialize(mime_type:, type: "binary", id: nil, url: nil, data: nil, filename: nil)
347
+ if [id, url, data].all?(&:nil?)
348
+ raise ArgumentError, "BinaryInputContent requires id, url, or data to be provided."
349
+ end
350
+
351
+ @type = type
352
+ @mime_type = mime_type
353
+ @id = id
354
+ @url = url
355
+ @data = data
356
+ @filename = filename
357
+ end
358
+
359
+ sig { returns(T::Hash[Symbol, T.untyped]) }
360
+ def to_h
361
+ {
362
+ type: @type,
363
+ mime_type: @mime_type,
364
+ id: @id,
365
+ url: @url,
366
+ data: @data,
367
+ filename: @filename
368
+ }
369
+ end
370
+ end
371
+
372
+ # Represents a message from a user.
373
+ #
374
+ # ```ruby
375
+ #
376
+ # msg = AgUiProtocol::Core::Types::UserMessage.new(
377
+ # id: "user_2",
378
+ # content: [
379
+ # { type: "text", text: "Please describe this image" },
380
+ # { type: "binary", mimeType: "image/png", url: "https://example.com/cat.png" }
381
+ # ]
382
+ # )
383
+ #
384
+ # ```
385
+ # @category Message Types
386
+ class UserMessage < BaseMessage
387
+ sig { returns(String) }
388
+ attr_reader :id
389
+
390
+ sig { returns(String) }
391
+ attr_reader :role
392
+
393
+ sig { returns(T.any(String, T::Array[T.any(TextInputContent, BinaryInputContent)])) }
394
+ attr_reader :content
395
+
396
+ sig { returns(T.nilable(String)) }
397
+ attr_reader :name
398
+
399
+ # @param id [String] Unique identifier for the message
400
+ # @param content [String, Array<TextInputContent | BinaryInputContent>] Either a plain text string or an ordered list of multimodal fragments
401
+ # @param name [String] Optional name of the sender
402
+ sig { params(id: String, content: T.any(String, T::Array[T.any(TextInputContent, BinaryInputContent)]), name: T.nilable(String)).void }
403
+ def initialize(id:, content:, name: nil)
404
+ super(id: id, role: "user", content: normalize_user_content(content), name: name)
405
+ end
406
+
407
+ sig { params(content: T.any(String, T::Array[T.any(TextInputContent, BinaryInputContent)])).returns(T.any(String, T::Array[T.any(TextInputContent, BinaryInputContent)])) }
408
+ def normalize_user_content(content)
409
+ if content.is_a?(Array)
410
+ content.map do |c|
411
+ if c.is_a?(Model)
412
+ c
413
+ elsif c.is_a?(Hash)
414
+ case c[:type] || c["type"]
415
+ when "text"
416
+ TextInputContent.new(text: c[:text] || c["text"])
417
+ when "binary"
418
+ BinaryInputContent.new(
419
+ mime_type: c[:mime_type] || c["mime_type"] || c[:mimeType] || c["mimeType"],
420
+ id: c[:id] || c["id"],
421
+ url: c[:url] || c["url"],
422
+ data: c[:data] || c["data"],
423
+ filename: c[:filename] || c["filename"]
424
+ )
425
+ else
426
+ c
427
+ end
428
+ else
429
+ c
430
+ end
431
+ end
432
+ else
433
+ content
434
+ end
435
+ end
436
+
437
+ sig { returns(T::Hash[Symbol, T.untyped]) }
438
+ def to_h
439
+ {
440
+ id: @id,
441
+ role: @role,
442
+ content: @content,
443
+ name: @name
444
+ }
445
+ end
446
+ end
447
+
448
+ # Tool result message.
449
+ #
450
+ # ```ruby
451
+ #
452
+ # msg = AgUiProtocol::Core::Types::ToolMessage.new(
453
+ # id: "tool_msg_1",
454
+ # tool_call_id: "tc_1",
455
+ # content: "ok"
456
+ # )
457
+ #
458
+ # ```
459
+ # @category Message Types
460
+ class ToolMessage < BaseMessage
461
+ sig { returns(String) }
462
+ attr_reader :id
463
+
464
+ sig { returns(String) }
465
+ attr_reader :role
466
+
467
+ sig { returns(String) }
468
+ attr_reader :content
469
+
470
+ sig { returns(String) }
471
+ attr_reader :tool_call_id
472
+
473
+ sig { returns(T.nilable(String)) }
474
+ attr_reader :error
475
+
476
+ # @param id [String] Unique identifier for the message.
477
+ # @param content [String] Tool result content.
478
+ # @param tool_call_id [String] ID of the tool call this message responds to.
479
+ # @param error [String] Error payload if the tool call failed.
480
+ sig { params(id: String, content: String, tool_call_id: String, error: T.nilable(String)).void }
481
+ def initialize(id:, content:, tool_call_id:, error: nil)
482
+ super(id: id, role: "tool", content: content)
483
+ @tool_call_id = tool_call_id
484
+ @error = error
485
+ end
486
+
487
+ sig { returns(T::Hash[Symbol, T.untyped]) }
488
+ def to_h
489
+ {
490
+ id: @id,
491
+ role: @role,
492
+ content: @content,
493
+ tool_call_id: @tool_call_id,
494
+ error: @error
495
+ }
496
+ end
497
+ end
498
+
499
+ # Represents structured activity progress emitted between chat messages.
500
+ #
501
+ # ```ruby
502
+ #
503
+ # msg = AgUiProtocol::Core::Types::ActivityMessage.new(
504
+ # id: "activity_1",
505
+ # activity_type: "progress",
506
+ # content: { "pct" => 10 }
507
+ # )
508
+ #
509
+ # ```
510
+ # @category Message Types
511
+ class ActivityMessage < BaseMessage
512
+ sig { returns(String) }
513
+ attr_reader :id
514
+
515
+ sig { returns(String) }
516
+ attr_reader :role
517
+
518
+ sig { returns(String) }
519
+ attr_reader :activity_type
520
+
521
+ sig { returns(T::Hash[T.any(Symbol, String), T.untyped]) }
522
+ attr_reader :content
523
+
524
+ # @param id [String] Unique identifier for the activity message.
525
+ # @param activity_type [String] Activity discriminator used for renderer selection.
526
+ # @param content [Hash] Structured payload representing the activity state.
527
+ sig { params(id: String, activity_type: String, content: T::Hash[T.any(Symbol, String), T.untyped]).void }
528
+ def initialize(id:, activity_type:, content:)
529
+ @id = id
530
+ @role = 'activity'
531
+ @activity_type = activity_type
532
+ @content = content
533
+ end
534
+
535
+ sig { returns(T::Hash[Symbol, T.untyped]) }
536
+ def to_h
537
+ {
538
+ id: @id,
539
+ role: @role,
540
+ activity_type: @activity_type,
541
+ content: @content
542
+ }
543
+ end
544
+ end
545
+
546
+ # Represents a piece of contextual information provided to an agent.
547
+ #
548
+ # ```ruby
549
+ #
550
+ # ctx = AgUiProtocol::Core::Types::Context.new(description: "User locale", value: "es-CL")
551
+ #
552
+ # ```
553
+ class Context < Model
554
+ sig { returns(String) }
555
+ attr_reader :description
556
+
557
+ sig { returns(String) }
558
+ attr_reader :value
559
+
560
+ # @param description [String] Description of what this context represents.
561
+ # @param value [String] The actual context value.
562
+ sig { params(description: String, value: String).void }
563
+ def initialize(description:, value:)
564
+ @description = description
565
+ @value = value
566
+ end
567
+
568
+ sig { returns(T::Hash[Symbol, T.untyped]) }
569
+ def to_h
570
+ {
571
+ description: @description,
572
+ value: @value
573
+ }
574
+ end
575
+ end
576
+
577
+ # Defines a tool that can be called by an agent.
578
+ #
579
+ # ```ruby
580
+ #
581
+ # tool = AgUiProtocol::Core::Types::Tool.new(
582
+ # name: "search",
583
+ # description: "Search the web",
584
+ # parameters: { "type" => "object", "properties" => { "q" => { "type" => "string" } } }
585
+ # )
586
+ #
587
+ # ```
588
+ class Tool < Model
589
+ sig { returns(String) }
590
+ attr_reader :name
591
+
592
+ sig { returns(String) }
593
+ attr_reader :description
594
+
595
+ sig { returns(T.untyped) }
596
+ attr_reader :parameters
597
+
598
+ # @param name [String] Name of the tool.
599
+ # @param description [String] Description of what the tool does.
600
+ # @param parameters [Object] JSON Schema for tool parameters.
601
+ sig { params(name: String, description: String, parameters: T.untyped).void }
602
+ def initialize(name:, description:, parameters:)
603
+ @name = name
604
+ @description = description
605
+ @parameters = parameters
606
+ end
607
+
608
+ sig { returns(T::Hash[Symbol, T.untyped]) }
609
+ def to_h
610
+ {
611
+ name: @name,
612
+ description: @description,
613
+ parameters: @parameters
614
+ }
615
+ end
616
+ end
617
+
618
+ # Input parameters for running an agent. In the HTTP API, this is the body of the `POST` request.
619
+ #
620
+ # ```ruby
621
+ #
622
+ # input = AgUiProtocol::Core::Types::RunAgentInput.new(
623
+ # thread_id: "thread_123",
624
+ # run_id: "run_123",
625
+ # parent_run_id: nil,
626
+ # state: {},
627
+ # messages: [],
628
+ # tools: [],
629
+ # context: [],
630
+ # forwarded_props: {}
631
+ # )
632
+ #
633
+ # ```
634
+ class RunAgentInput < Model
635
+ sig { returns(String) }
636
+ attr_reader :thread_id
637
+
638
+ sig { returns(String) }
639
+ attr_reader :run_id
640
+
641
+ sig { returns(T.nilable(String)) }
642
+ attr_reader :parent_run_id
643
+
644
+ sig { returns(T.untyped) }
645
+ attr_reader :state
646
+
647
+ sig { returns(T::Array[BaseMessage]) }
648
+ attr_reader :messages
649
+
650
+ sig { returns(T::Array[Tool]) }
651
+ attr_reader :tools
652
+
653
+ sig { returns(T::Array[Context]) }
654
+ attr_reader :context
655
+
656
+ sig { returns(T.untyped) }
657
+ attr_reader :forwarded_props
658
+
659
+ # @param thread_id [String] ID of the conversation thread
660
+ # @param run_id [String] ID of the current run
661
+ # @param state [Object] Current state of the agent
662
+ # @param messages [Array<BaseMessage>] List of messages in the conversation
663
+ # @param tools [Array<Tool>] List of tools available to the agent
664
+ # @param context [Array<Context>] List of context objects provided to the agent
665
+ # @param forwarded_props [Object] Additional properties forwarded to the agent
666
+ # @param parent_run_id [String] Lineage pointer for branching/time travel
667
+ # @raise [ArgumentError] if messages is not an Array of BaseMessage
668
+ sig do
669
+ params(
670
+ thread_id: String,
671
+ run_id: String,
672
+ state: T.untyped,
673
+ messages: T::Array[BaseMessage],
674
+ tools: T::Array[Tool],
675
+ context: T::Array[Context],
676
+ forwarded_props: T.untyped,
677
+ parent_run_id: T.nilable(String)
678
+ ).void.checked(:always)
679
+ end
680
+ def initialize(thread_id:, run_id:, state:, messages:, tools:, context:, forwarded_props:, parent_run_id: nil)
681
+ unless messages.is_a?(Array) && messages.all? { |m| m.is_a?(BaseMessage) }
682
+ raise ArgumentError, "messages must be an Array of BaseMessage"
683
+ end
684
+ unless tools.is_a?(Array) && tools.all? { |m| m.is_a?(Tool) }
685
+ raise ArgumentError, "tools must be an Array of Tool"
686
+ end
687
+ unless context.is_a?(Array) && context.all? { |m| m.is_a?(Context) }
688
+ raise ArgumentError, "context must be an Array of Context"
689
+ end
690
+
691
+ @thread_id = thread_id
692
+ @run_id = run_id
693
+ @parent_run_id = parent_run_id
694
+ @state = state
695
+ @messages = messages
696
+ @tools = tools
697
+ @context = context
698
+ @forwarded_props = forwarded_props
699
+ end
700
+
701
+ sig { returns(T::Hash[Symbol, T.untyped]) }
702
+ def to_h
703
+ {
704
+ thread_id: @thread_id,
705
+ run_id: @run_id,
706
+ parent_run_id: @parent_run_id,
707
+ state: @state,
708
+ messages: @messages,
709
+ tools: @tools,
710
+ context: @context,
711
+ forwarded_props: @forwarded_props
712
+ }
713
+ end
714
+ end
715
+ end
716
+ end
717
+ end