ruby_llm-ag_ui 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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +48 -0
  3. data/CONTRIBUTING.md +56 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +146 -0
  6. data/SECURITY.md +30 -0
  7. data/docs/_advanced/rails-integration.md +199 -0
  8. data/docs/_advanced/security-and-deployment.md +204 -0
  9. data/docs/_core_features/approval-and-resume.md +62 -0
  10. data/docs/_core_features/input-validation.md +64 -0
  11. data/docs/_core_features/reconnects.md +42 -0
  12. data/docs/_core_features/streaming-runs.md +49 -0
  13. data/docs/_getting_started/configuration.md +57 -0
  14. data/docs/_getting_started/getting-started.md +52 -0
  15. data/docs/_getting_started/overview.md +189 -0
  16. data/docs/_getting_started/whats-new-in-0-1.md +41 -0
  17. data/docs/_reference/api-reference.md +26 -0
  18. data/docs/_reference/compatibility.md +130 -0
  19. data/docs/_reference/protocol-coverage.md +175 -0
  20. data/docs/_reference/release-history.md +22 -0
  21. data/docs/_reference/releasing.md +54 -0
  22. data/docs/_reference/third-party-notices.md +82 -0
  23. data/docs/_reference/upgrading.md +42 -0
  24. data/docs/index.md +53 -0
  25. data/lib/generators/ruby_llm/ag_ui/install_generator.rb +37 -0
  26. data/lib/generators/ruby_llm/ag_ui/templates/create_ruby_llm_ag_ui_runs.rb.tt +24 -0
  27. data/lib/generators/ruby_llm/ag_ui/templates/initializer.rb.tt +24 -0
  28. data/lib/ruby_llm/ag_ui/canonical.rb +40 -0
  29. data/lib/ruby_llm/ag_ui/configuration.rb +113 -0
  30. data/lib/ruby_llm/ag_ui/coverage.rb +163 -0
  31. data/lib/ruby_llm/ag_ui/emitter.rb +119 -0
  32. data/lib/ruby_llm/ag_ui/error.rb +69 -0
  33. data/lib/ruby_llm/ag_ui/error_sanitizer.rb +69 -0
  34. data/lib/ruby_llm/ag_ui/input.rb +299 -0
  35. data/lib/ruby_llm/ag_ui/interrupt.rb +207 -0
  36. data/lib/ruby_llm/ag_ui/mapper.rb +333 -0
  37. data/lib/ruby_llm/ag_ui/message_parser.rb +127 -0
  38. data/lib/ruby_llm/ag_ui/protocol.rb +130 -0
  39. data/lib/ruby_llm/ag_ui/rails/active_record_store.rb +119 -0
  40. data/lib/ruby_llm/ag_ui/rails/railtie.rb +29 -0
  41. data/lib/ruby_llm/ag_ui/rails/record.rb +51 -0
  42. data/lib/ruby_llm/ag_ui/rails/streaming.rb +166 -0
  43. data/lib/ruby_llm/ag_ui/reconnect.rb +86 -0
  44. data/lib/ruby_llm/ag_ui/redaction.rb +58 -0
  45. data/lib/ruby_llm/ag_ui/redemption.rb +62 -0
  46. data/lib/ruby_llm/ag_ui/reservation.rb +56 -0
  47. data/lib/ruby_llm/ag_ui/resume.rb +108 -0
  48. data/lib/ruby_llm/ag_ui/resume_entry.rb +49 -0
  49. data/lib/ruby_llm/ag_ui/run.rb +177 -0
  50. data/lib/ruby_llm/ag_ui/run_record.rb +229 -0
  51. data/lib/ruby_llm/ag_ui/sse_writer.rb +119 -0
  52. data/lib/ruby_llm/ag_ui/store/base.rb +123 -0
  53. data/lib/ruby_llm/ag_ui/store/memory_store.rb +123 -0
  54. data/lib/ruby_llm/ag_ui/validation.rb +63 -0
  55. data/lib/ruby_llm/ag_ui/version.rb +8 -0
  56. data/lib/ruby_llm/ag_ui.rb +65 -0
  57. metadata +223 -0
@@ -0,0 +1,204 @@
1
+ ---
2
+ layout: default
3
+ title: "Security and Deployment"
4
+ nav_order: 2
5
+ description: Keep identity, tool permissions, and sensitive output under the host application's control.
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * Which values the gem treats as untrusted.
16
+ * How redaction, error sanitization, and interrupt redemption work.
17
+ * Which security decisions the host application must make.
18
+
19
+ This gem is a **producer**, not an authorization layer. It decides what a run may
20
+ report on the wire and refuses what it cannot report truthfully. It does not
21
+ decide who may run what: authentication, authorization, which tools and model an
22
+ actor may use, and whether a tool may execute are the host's decisions.
23
+
24
+ The model below is the one the test suite enforces, and the negative cases are
25
+ fixtures, not prose. If a claim here has no test, it is marked as host
26
+ responsibility.
27
+
28
+ ## Trust boundaries
29
+
30
+ | Input | Trusted? | Why |
31
+ |---|---|---|
32
+ | `messages`, `state`, `tools`, `context`, `forwardedProps` | **No** | Arbitrary JSON from the client. Accepted as data only. |
33
+ | `threadId`, `runId`, `parentRunId`, `resume` | **No** | They name things; they never select a record or grant a right. |
34
+ | `protocolVersion` | **No** | Reported and compared; an unknown version is refused, not honored. |
35
+ | The `scope` string | **Yes, because the host built it** | It must come from authenticated identity. The gem rejects a non-String or empty scope. |
36
+ | The `chat` object | **Yes** | The host constructed it, with the host's tools and model. |
37
+ | Tool output | **No** | A tool reads databases, APIs and files. Its payload can hold secrets and personal data. |
38
+ | Provider exceptions | **No** | They can embed request bodies, URLs, headers and keys. |
39
+ | The run store | **Yes** | Host infrastructure. |
40
+
41
+ ## What a client cannot do
42
+
43
+ - **Choose the model, tools or system prompt.** `input.tools` is informational:
44
+ `Input#tool_names` and `Input#client_tools?` report it, and nothing in the gem
45
+ registers a client-supplied tool on the chat. `input.messages` is parsed and
46
+ echoed, never appended to the chat as trusted turns; `config.allow_client_history`
47
+ is off and documented as off.
48
+ - **Reach another actor's run.** Every lookup is bounded by the scope the host
49
+ supplies. The same `(threadId, runId)` under a different scope is a different
50
+ run (`spec/ruby_llm/ag_ui/run_spec.rb`: "does not let one scope attach to
51
+ another tenant run").
52
+ - **Redeclare a run id.** The same identity with a *different* input is refused
53
+ as `RunConflictError` (409 in the Rails adapter) rather than executed
54
+ ("refuses the same identity with a changed input as a conflict").
55
+ - **Run the same tool twice.** A resume is recorded once; a second answer to the
56
+ same interrupt is refused without executing the tool ("refuses a decision
57
+ another resume already recorded, without executing the tool").
58
+ - **Widen what an approval covers.** An interrupt pins the tool call and the
59
+ digest of the arguments RubyLLM asked to run. A resume whose call arguments
60
+ changed, whose call is no longer pending, or which names an interrupt that
61
+ expired or does not exist is refused before anything is reserved or written
62
+ ("refuses a resume whose tool call arguments changed since the interrupt",
63
+ "refuses an expired interrupt", "refuses an unknown interrupt before the run is
64
+ reserved or opened").
65
+ - **Inject SSE framing.** Every frame is one JSON document on one `data:` line;
66
+ a payload containing newlines stays inside the JSON string. There is no path
67
+ that writes a raw payload into a frame.
68
+ - **Exhaust the process with one body.** The body is bounded before it is parsed
69
+ (`max_input_bytes`, 256 KB default), and so are message, tool and context
70
+ counts.
71
+
72
+ ## Redaction is the default
73
+
74
+ `config.redact` defaults to `RubyLLM::AgUI::Redaction.method(:default)`: the
75
+ payload keeps its **shape** and loses its **content** (`[redacted]`), because the
76
+ gem cannot know which part of a tool result is a secret. The mapper warns once per
77
+ run that the host has not chosen a policy
78
+ (`spec/ruby_llm/ag_ui/mapper_spec.rb`: "keeps the shape and withholds the
79
+ content", "warns once that the host has not chosen a policy").
80
+
81
+ Opting out is explicit:
82
+
83
+ ```ruby
84
+ RubyLLM::AgUI.configure do |config|
85
+ config.redact = RubyLLM::AgUI::Redaction.method(:none) # publish as-is
86
+ # or
87
+ config.redact = ->(kind, payload) { MyPolicy.call(kind, payload) }
88
+ end
89
+ ```
90
+
91
+ A `nil` policy is not an opt-out: `Configuration#redact` falls back to the
92
+ default, so a mistyped configuration cannot publish raw payloads. What is in
93
+ force is reported by `RubyLLM::AgUI.config.to_h[:redact]` as `:default`, `:none`
94
+ or `:custom`.
95
+
96
+ Redaction applies to what the gem publishes. It does not scrub the host's logs,
97
+ the chat's own history, the provider's copy of the conversation, or any payload
98
+ the host writes to the wire by other means.
99
+
100
+ ## Errors never carry exception text
101
+
102
+ `RUN_ERROR` is built from `ErrorSanitizer`: a fixed message and code per error
103
+ class, and a fixed `internal_error` fallback for anything unrecognized. The raw
104
+ exception goes to the host logger, if one is configured, and nowhere else
105
+ (`spec/ruby_llm/ag_ui/run_spec.rb`: "never echoes the exception text";
106
+ `spec/ruby_llm/ag_ui/error_sanitizer_spec.rb`).
107
+
108
+ This matters because upstream Rails examples put `e.message` straight into the
109
+ event. A provider error message can contain the prompt, the tool arguments, a URL
110
+ with a key in it, or an internal path.
111
+
112
+ ## Interrupts
113
+
114
+ An interrupt is a pause, not a permission. It carries the tool call id, a reason,
115
+ a human-readable question, and a host-owned expiry. It does **not** carry the
116
+ arguments digest that the resume check uses ("does not let the arguments digest
117
+ reach the client"), so the client cannot forge a matching approval.
118
+
119
+ - TTL is `config.interrupt_ttl` (900 s default) or per-run `expires_in:`.
120
+ - An expired interrupt is not answerable; a resume for it is refused.
121
+ - A resume with `status: "cancelled"` records a denial. The chat's next
122
+ `complete` reports the denial as RubyLLM normally would; no tool runs.
123
+ - Approval does not skip the host's re-authorization: the host re-checks before
124
+ a tool executes, and the gem cannot do it for them.
125
+
126
+ The store's redemption step is the concurrency guard: the first decision wins and
127
+ is recorded, the loser is told it was already recorded. That is why an approved
128
+ tool executes once even when two resumes arrive together.
129
+
130
+ ## Reconnect
131
+
132
+ A run that already ended is served from its record: `RUN_STARTED` (with the
133
+ protocol version and the input echo), `STATE_SNAPSHOT` when one was recorded, and
134
+ the recorded terminal event. Deltas are never replayed, and a run that has not
135
+ ended is refused rather than attached ("refuses to attach a second stream to a run
136
+ that has not ended"). Both rules exist to prevent a duplicated side effect, not
137
+ just duplicated text.
138
+
139
+ A recorded failure is reported with the sanitized message and code it was stored
140
+ with. A record with no recorded failure reports `internal_error` rather than
141
+ guessing.
142
+
143
+ ## Transport
144
+
145
+ The gem writes to whatever object the host gives it and sets no security policy
146
+ of its own beyond the framing. The Rails adapter adds:
147
+
148
+ - `Accept` negotiation: a client that cannot read `text/event-stream` gets 406
149
+ before any event.
150
+ - Input refusal before the response commits: 400, 404, 409, 413, never a
151
+ half-open stream.
152
+ - `Content-Type: text/event-stream`, `Cache-Control: no-cache`,
153
+ `Connection: keep-alive`, `X-Accel-Buffering: no`.
154
+ - A client disconnect is swallowed: it is not a run outcome.
155
+
156
+ The gem does not terminate TLS, authenticate, rate-limit, validate Origin/CSRF
157
+ for browser clients, or configure CORS. Those are the host's, and getting them
158
+ wrong is outside what this gem can detect.
159
+
160
+ ## Deployment checklist
161
+
162
+ 1. Authenticate before parsing the body; build `scope` from the authenticated
163
+ actor, never from anything in the body.
164
+ 2. Authorize the `threadId → chat` lookup inside the actor's own association, so
165
+ a client-chosen id cannot select somebody else's conversation.
166
+ 3. Re-authorize before an approved tool runs. An approval answers an interrupt;
167
+ it does not grant permission.
168
+ 4. Use a durable store with a uniqueness constraint on
169
+ `(scope, thread_id, run_id)` and `(scope, thread_id, interrupt_id)`. The Rails
170
+ store is built for that; `MemoryStore` is single-process only.
171
+ 5. Keep redaction on until you have classified which payloads may be published.
172
+ Scrub raw bodies, prompts and tool payloads out of your logs.
173
+ 6. Treat `messages`, `state` and `forwardedProps` as untrusted text in any UI you
174
+ build. Escape it where it is rendered; a producer cannot prevent XSS.
175
+ 7. Bound the resource: request size (`max_input_bytes`), provider timeouts, rate
176
+ limits, and the number of concurrent streams (an `ActionController::Live`
177
+ response holds a thread for the run's lifetime).
178
+ 8. Serve over TLS, and set CORS and cookie policy deliberately if the stream is
179
+ consumed from a browser.
180
+ 9. Do not enable client-supplied tools for a multi-tenant deployment in this
181
+ release; they are never used, and that is the intended behavior.
182
+ 10. Do not log the request body or the events at info level.
183
+
184
+ ## What is out of scope
185
+
186
+ - **Prompt injection.** Content that arrives in `messages`, tool results or
187
+ documents can steer a model. This gem maps the model's behavior faithfully; it
188
+ cannot make the model trustworthy.
189
+ - **Host authentication and authorization**, including whether a given actor
190
+ should be allowed to run a given tool.
191
+ - **Provider-side data handling**, retention and training.
192
+ - **Anything the host writes outside the gem's events**, such as its own log lines,
193
+ its own extra SSE frames, its own UI.
194
+ - **Denial of service by an authenticated actor** who opens many legitimate
195
+ streams.
196
+
197
+ ## Reporting
198
+
199
+ See the [repository security policy](https://github.com/yorzi/ruby_llm-ag-ui/blob/main/SECURITY.md) for the reporting process. Reports about a silent duplicate execution, an approval that widens what it covers, a secret in a `RUN_ERROR`, or a leak into the log are treated as vulnerabilities.
200
+
201
+ ## Next Steps
202
+
203
+ * [Configuration]({% link _getting_started/configuration.md %}) - set safe redaction and storage defaults.
204
+ * [Rails Integration]({% link _advanced/rails-integration.md %}) - apply these boundaries in a controller.
@@ -0,0 +1,62 @@
1
+ ---
2
+ layout: default
3
+ title: "Approval and Resume"
4
+ nav_order: 3
5
+ description: "Pause a run for host-authorized tool approval and answer the interrupt on a new run."
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * What an approval interrupt contains.
16
+ * How the client answers it on a new run.
17
+ * What the host must authorize again before a tool executes.
18
+
19
+ ## Return an Interrupt
20
+
21
+ When RubyLLM pauses on `requires_approval`, the run ends with an interrupt outcome. The interrupt identifies the pending tool call and includes a host-owned expiry. The id and expiry below are from a sample run; each run receives new values.
22
+
23
+ ```json
24
+ {
25
+ "type": "RUN_FINISHED",
26
+ "threadId": "thread-1",
27
+ "runId": "run-1",
28
+ "outcome": {
29
+ "type": "interrupt",
30
+ "interrupts": [{
31
+ "id": "817cc7a4-c2b4-4393-a25b-09819cf7198a",
32
+ "reason": "tool_call_confirmation",
33
+ "message": "Approve the cancel_order tool call?",
34
+ "toolCallId": "call_cancel_42",
35
+ "expiresAt": "2026-10-18T02:19:38Z"
36
+ }]
37
+ }
38
+ }
39
+ ```
40
+
41
+ The interrupt does not expose the tool arguments digest used by the server. A client cannot use the response as a permission token.
42
+
43
+ ## Answer on a New Run
44
+
45
+ The client sends a new `runId` for the same thread and includes a resume entry. `resolved` approves the pending call; `cancelled` records a denial.
46
+
47
+ ```json
48
+ {
49
+ "threadId": "thread-1",
50
+ "runId": "run-2",
51
+ "messages": [{ "id": "m1", "role": "user", "content": "Cancel order 42." }],
52
+ "resume": [{ "interruptId": "817cc7a4-c2b4-4393-a25b-09819cf7198a", "status": "resolved" }]
53
+ }
54
+ ```
55
+
56
+ The interrupt must still be pending and unexpired. The tool call id and argument digest must match the call RubyLLM is holding. The host must re-authorize the action before the tool executes. The store records only one decision, so a replay cannot execute the tool twice.
57
+
58
+ ## Next Steps
59
+
60
+ * [Streaming Runs]({% link _core_features/streaming-runs.md %}) - follow the event and outcome sequence.
61
+ * [Reconnects and Run Identity]({% link _core_features/reconnects.md %}) - understand stored run identity.
62
+ * [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - keep approval separate from authorization.
@@ -0,0 +1,64 @@
1
+ ---
2
+ layout: default
3
+ title: "Validating Input"
4
+ nav_order: 1
5
+ description: "Validate a bounded AG-UI request and turn it into immutable values."
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * How to parse a request body and inspect its typed values.
16
+ * How unknown members, size limits, and validation errors work.
17
+ * Which request values remain untrusted after parsing.
18
+
19
+ ## Parse a Request
20
+
21
+ `RunAgentInput` arrives as JSON. Parse it before choosing a chat so malformed or oversized input is refused before provider work begins.
22
+
23
+ ```ruby
24
+ require 'ruby_llm/ag_ui'
25
+
26
+ input = RubyLLM::AgUI::Input.parse({
27
+ 'threadId' => 'thread-1',
28
+ 'runId' => 'run-1',
29
+ 'messages' => [{ 'id' => 'm1', 'role' => 'user', 'content' => 'Hello' }]
30
+ })
31
+
32
+ puts [input.thread_id, input.run_id].join('/')
33
+ # => thread-1/run-1
34
+ ```
35
+
36
+ `Input.parse` accepts a JSON string, a Hash, or a readable stream. The result is immutable. It exposes the accepted values, a canonical digest, and any non-fatal warnings.
37
+
38
+ ## Unknown Members and Limits
39
+
40
+ By default, top-level members outside the pinned schema are stripped and recorded in `warnings`. Set `strict: true` to reject them. The parser also bounds the request body, nesting depth, and counts for messages, tools, and context.
41
+
42
+ ```ruby
43
+ input = RubyLLM::AgUI::Input.parse({
44
+ 'threadId' => 'thread-1',
45
+ 'runId' => 'run-1',
46
+ 'messages' => [],
47
+ 'clientOnly' => true
48
+ })
49
+
50
+ input.warnings
51
+ # => ["stripped unknown RunAgentInput member \"clientOnly\""]
52
+ ```
53
+
54
+ Pass `max_bytes:` to set a per-parse body limit. Configure `max_messages`, `max_tools`, and `max_context` on `RubyLLM::AgUI.config`. Invalid JSON, unsupported protocol versions, missing identifiers, and invalid message shapes raise an `InputError` subclass before a run starts.
55
+
56
+ ## Parsed Data Does Not Grant Trust
57
+
58
+ The parser validates shape, not authority. Client-supplied tools are reported by `tool_names` and `client_tools?`, but they are never registered on the chat. The host chooses the model, tools, system prompt, chat history, and authenticated scope. Do not use `thread_id` or `run_id` as authorization by themselves.
59
+
60
+ ## Next Steps
61
+
62
+ * [Getting Started]({% link _getting_started/getting-started.md %}) - install and parse a request.
63
+ * [Streaming Runs]({% link _core_features/streaming-runs.md %}) - pass a validated input to a RubyLLM chat.
64
+ * [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - define the host trust boundary.
@@ -0,0 +1,42 @@
1
+ ---
2
+ layout: default
3
+ title: "Reconnects and Run Identity"
4
+ nav_order: 4
5
+ description: "Reserve each run once and report its recorded outcome after a reconnect."
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * How the gem reserves `(scope, threadId, runId)` for one input digest.
16
+ * How retries and reconnects differ from duplicate execution.
17
+ * Which store to use for one process or several workers.
18
+
19
+ ## Reuse a Run Identity
20
+
21
+ The store reserves a run before any provider work. It keys the record by authenticated scope, thread id, and run id, then compares the canonical digest of the accepted input.
22
+
23
+ | Request | Result |
24
+ | --- | --- |
25
+ | New identity | Reserve and execute the run. |
26
+ | Same identity and same input while running | Refuse a second live stream. |
27
+ | Same identity and same input after completion | Return the recorded start, optional state snapshot, and terminal event. |
28
+ | Same identity with different input | Raise a run conflict. |
29
+
30
+ Reconnect does not replay text or tool deltas. Replaying deltas could display text twice or repeat a side effect. The host can publish a state snapshot for a finished run, but the gem does not reconstruct conversation history.
31
+
32
+ ## Choose a Store
33
+
34
+ `MemoryStore` is suitable for tests and a single process. Its records disappear on restart and are not shared between workers. The Rails adapter's `ActiveRecordStore` persists records and redeems interrupts under a database lock.
35
+
36
+ The store boundary is `RubyLLM::AgUI::Store::Base`. Configure a compatible store globally or pass it to `Run.new`. For multi-process deployments, the database must enforce unique run identity and serialize interrupt redemption.
37
+
38
+ ## Next Steps
39
+
40
+ * [Configuration]({% link _getting_started/configuration.md %}) - set the default store.
41
+ * [Rails Integration]({% link _advanced/rails-integration.md %}) - install the durable ActiveRecord store.
42
+ * [Protocol Coverage]({% link _reference/protocol-coverage.md %}) - inspect reconnect events.
@@ -0,0 +1,49 @@
1
+ ---
2
+ layout: default
3
+ title: "Streaming Runs"
4
+ nav_order: 2
5
+ description: "Run one RubyLLM chat turn and write ordered AG-UI events as server-sent events."
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * How to connect a host-owned chat, parsed input, scope, and store.
16
+ * How a RubyLLM turn becomes ordered AG-UI events.
17
+ * What each terminal outcome means.
18
+
19
+ ## Run a Complete Example
20
+
21
+ The repository includes a scripted chat that emits an approval pause, resumes the tool call, and prints the server-sent events. Run it from the repository root; it does not need a provider key.
22
+
23
+ ```sh
24
+ bundle exec rake example
25
+ ```
26
+
27
+ In an application, parse the request first, then pass the host-selected chat, parsed input, authenticated scope, and run store to `RubyLLM::AgUI::Run`. Call `stream(to:)` with an IO-like stream. The host builds the chat and authorizes access to the thread; the gem reserves the run identity before provider work.
28
+
29
+ ## Event Order
30
+
31
+ The mapper turns RubyLLM callbacks into AG-UI events. The emitter assigns each event a sequence, persists run state, closes open text or tool events, and writes one JSON event per `data:` line through the SSE writer.
32
+
33
+ An assistant message opens on its first non-empty chunk and closes before a tool call, phase change, or terminal event. A tool call starts when its id and name are known, emits the arguments as received, and ends before the call runs. One run ends with one terminal result.
34
+
35
+ ## Outcomes
36
+
37
+ * A completed chat ends with `RUN_FINISHED` and a `success` outcome.
38
+ * A `requires_approval` pause ends with `RUN_FINISHED` and an `interrupt` outcome.
39
+ * `Chat#cancel` ends with `RUN_FINISHED` and a `cancelled` outcome.
40
+ * A failure ends with `RUN_ERROR` and a sanitized message and code.
41
+ * A pause the protocol cannot express is an error, never a success.
42
+
43
+ The run record reaches a terminal state before the terminal event is written. If the connection closes in the middle, the record remains available for a later reconnect.
44
+
45
+ ## Next Steps
46
+
47
+ * [Validating Input]({% link _core_features/input-validation.md %}) - parse untrusted request data.
48
+ * [Approval and Resume]({% link _core_features/approval-and-resume.md %}) - answer an interrupt on a new run.
49
+ * [Reconnects and Run Identity]({% link _core_features/reconnects.md %}) - serve a completed run again.
@@ -0,0 +1,57 @@
1
+ ---
2
+ layout: default
3
+ title: Configuration
4
+ nav_order: 3
5
+ description: Set run storage, input limits, logging, and redaction defaults for the application.
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * How to configure defaults once for the application.
16
+ * Which settings are available and what each one controls.
17
+
18
+ ## Configuring RubyLLM AG-UI
19
+
20
+ ```ruby
21
+ require 'ruby_llm/ag_ui'
22
+
23
+ RubyLLM::AgUI.configure do |config|
24
+ config.run_store = RubyLLM::AgUI::Store::MemoryStore.new
25
+ config.max_input_bytes = 256_000
26
+ config.redact = RubyLLM::AgUI::Redaction.method(:default)
27
+ end
28
+
29
+ p RubyLLM::AgUI.config.to_h[:redact]
30
+ # => :default
31
+ ```
32
+
33
+ ## Settings
34
+
35
+ The global configuration supplies defaults. `Run.new` can override the store, logger, redaction policy, clock, and interrupt expiry for one run. `MemoryStore` is process-local, so use a durable store when runs can move between workers.
36
+
37
+ | Setting | Default | Purpose |
38
+ | --- | --- | --- |
39
+ | `run_store` | `nil` | Store run records and interrupts. A run needs a store, supplied here or to `Run.new`. |
40
+ | `redact` | `Redaction.default` | Transform tool results before they reach the stream. |
41
+ | `logger` | `nil` | Receive host-side diagnostics without prompts or raw tool payloads. |
42
+ | `protocol_version` | `"1.0"` | Value reported by `RUN_STARTED`. |
43
+ | `max_input_bytes` | `256_000` | Maximum request body size. |
44
+ | `max_messages` | `500` | Maximum messages in one request. |
45
+ | `max_tools` | `64` | Maximum tool definitions in one request. |
46
+ | `max_context` | `64` | Maximum context entries in one request. |
47
+ | `interrupt_ttl` | `900` seconds | How long an approval interrupt can be answered. |
48
+ | `clock` | `Time.now.utc` | Clock used to set interrupt expiry. |
49
+ | `allow_client_history` | `false` | Reserved for future history behavior; client history is not trusted in this release. |
50
+
51
+ Provider keys and model selection are configured through RubyLLM and the host app. This gem does not read credentials when it loads.
52
+
53
+ ## Next Steps
54
+
55
+ * [Getting Started]({% link _getting_started/getting-started.md %}) - install and parse a request.
56
+ * [How RubyLLM AG-UI Works]({% link _getting_started/overview.md %}) - the parts of one run.
57
+ * [Security and Deployment]({% link _advanced/security-and-deployment.md %}) - host responsibilities and safe defaults.
@@ -0,0 +1,52 @@
1
+ ---
2
+ layout: default
3
+ title: Getting Started
4
+ nav_order: 1
5
+ description: Install ruby_llm-ag_ui and validate an AG-UI request before connecting a chat.
6
+ ---
7
+
8
+ # {{ page.title }}
9
+
10
+ {{ page.description }}
11
+ {: .fs-6 .fw-300 }
12
+
13
+ After reading this guide, you will know:
14
+
15
+ * How to install the release or the current development branch.
16
+ * How to parse a `RunAgentInput` into validated Ruby values.
17
+ * How to run the included offline end-to-end example.
18
+
19
+ ## Installation
20
+
21
+ ```sh
22
+ {% if site.docs_unreleased %}bundle add ruby_llm-ag_ui --git https://github.com/yorzi/ruby_llm-ag-ui --branch main{% else %}bundle add ruby_llm-ag_ui --version {{ site.version }}{% endif %}
23
+ ```
24
+
25
+ The gem requires Ruby 3.2 or newer, RubyLLM 2.1 or newer, and `ag-ui-protocol` 0.2.x. The Rails adapter is optional. Provider credentials belong to your RubyLLM application; the parser example below needs none.
26
+
27
+ The runtime dependencies select patched `cgi`, `resolv`, `uri`, and `zlib` gems. An older Ruby installation's default library versions do not override those dependency requirements.
28
+
29
+ ## Parse a Request
30
+
31
+ Before you start a chat, validate the request body. The parser bounds the input, checks required fields, and returns an immutable value.
32
+
33
+ ```ruby
34
+ require 'ruby_llm/ag_ui'
35
+
36
+ input = RubyLLM::AgUI::Input.parse({
37
+ 'threadId' => 'thread-1',
38
+ 'runId' => 'run-1',
39
+ 'messages' => [{ 'id' => 'm1', 'role' => 'user', 'content' => 'Hello' }]
40
+ })
41
+
42
+ puts "#{input.thread_id}/#{input.run_id}"
43
+ # => thread-1/run-1
44
+ ```
45
+
46
+ The host still chooses the chat, model, tools, and authenticated scope. `Input` treats client fields as data and does not grant access. To see a complete run with an approval pause and resume, run `bundle exec rake example` from the repository; it uses a scripted chat and needs no provider key.
47
+
48
+ ## Next Steps
49
+
50
+ * [How RubyLLM AG-UI Works]({% link _getting_started/overview.md %}) - the parts of one run.
51
+ * [Configuration]({% link _getting_started/configuration.md %}) - defaults and run stores.
52
+ * [Validating Input]({% link _core_features/input-validation.md %}) - request limits and strict parsing.