Class: AgUi::Middleware::State
- Inherits:
-
Object
- Object
- AgUi::Middleware::State
- Defined in:
- lib/ag_ui/middleware/state.rb
Overview
Shared state (CoAgents) — the Ruby side of AG-UI's STATE_SNAPSHOT /
STATE_DELTA channel (doc 05). Bidirectional state sync between the
frontend's agent.state and the running agent.
Way in:
- seeds env[:state] from RunAgentInput.state (the state the frontend
last held, pushed via agent.setState) so downstream middleware,
tools, and the app can READ what the UI currently shows
- injects two tools the model calls to WRITE state:
AGUISendStateSnapshot({ snapshot }) — replace the whole state
AGUISendStateDelta({ delta }) — RFC 6902 JSON Patch ops
Way out, per state-tool call the model made:
- snapshot: env[:state] = snapshot; emits STATE_SNAPSHOT
- delta: patches env[:state] (Hana); emits STATE_DELTA
- both append a {"status":"ok"} :tool message and emit a
TOOL_CALL_RESULT — the same shape a server tool produces — then let
the run CONTINUE (unlike a browser/client tool) so the model can act
on the new state or confirm to the user. State tools are server-side.
The TOOL_CALL_START/ARGS/END chrome is left to ToolRouter (it advertises nothing about these being "client" vs "server" — it just streams the call), exactly as A2ui leaves its render_a2ui call to ToolRouter. Compose State OUTSIDE ToolRouter:
use Loop::ToolResult
use State, state: input.state
use ToolRouter, tools: input.tools
The frontend applies the same snapshot/patch to its own store; env is kept coherent so the agent's own later reads (and further deltas) see the current value.
Constant Summary collapse
- SNAPSHOT_TOOL =
"AGUISendStateSnapshot"- DELTA_TOOL =
"AGUISendStateDelta"- TOOL_NAMES =
[SNAPSHOT_TOOL, DELTA_TOOL].freeze
- SNAPSHOT_DEFINITION =
{ "name" => SNAPSHOT_TOOL, "description" => "Replace the shared application state with a new snapshot; the " \ "frontend re-renders from it. Send the COMPLETE next state object, " \ "not a diff. Prefer AGUISendStateDelta for small targeted changes.", "parameters" => { "type" => "object", "properties" => { "snapshot" => { "type" => "object", "description" => "The complete new application state.", }, }, "required" => %w[snapshot], }, }.freeze
- DELTA_DEFINITION =
{ "name" => DELTA_TOOL, "description" => "Apply a JSON Patch (RFC 6902) to the shared application state — an " \ "array of {op, path, value} operations. Use for small, targeted " \ "changes. Paths are JSON Pointers, e.g. " \ "\"/documentEditor/activeTabId\". ops: add, replace, remove, move, " \ "copy, test.", "parameters" => { "type" => "object", "properties" => { "delta" => { "type" => "array", "description" => "JSON Patch operations, e.g. " \ "[{\"op\":\"replace\",\"path\":\"/theme\",\"value\":\"dark\"}].", "items" => { "type" => "object" }, }, }, "required" => %w[delta], }, }.freeze
Instance Method Summary collapse
- #call(env) ⇒ Object
-
#initialize(app, state: nil) ⇒ State
constructor
A new instance of State.
Constructor Details
#initialize(app, state: nil) ⇒ State
Returns a new instance of State.
88 89 90 91 |
# File 'lib/ag_ui/middleware/state.rb', line 88 def initialize(app, state: nil) @app = app @initial_state = normalize(state) end |
Instance Method Details
#call(env) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/ag_ui/middleware/state.rb', line 93 def call(env) seed_state(env) advertise(env) before = env[:messages].length @app.call(env) # Only this iteration's assistant message — seeded history can carry # old state-tool turns that must not re-emit. appended = env[:messages][before..] || [] assistant = appended.reverse.find { |m| m.respond_to?(:tool_call?) && m.tool_call? } if assistant apply_state_calls(env, assistant) end env end |