@plurnk/plurnk-agui 3.0.0 → 4.0.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 (74) hide show
  1. package/.env.defaults +17 -8
  2. package/README.md +37 -13
  3. package/SPEC.md +627 -268
  4. package/dist/AguiPlus.d.ts +24 -29
  5. package/dist/AguiPlus.d.ts.map +1 -1
  6. package/dist/AguiPlus.js +25 -36
  7. package/dist/AguiPlus.js.map +1 -1
  8. package/dist/AguiSurface.d.ts +2 -2
  9. package/dist/AguiSurface.d.ts.map +1 -1
  10. package/dist/AguiSurface.js +25 -16
  11. package/dist/AguiSurface.js.map +1 -1
  12. package/dist/AuthorizationDocuments.d.ts +33 -0
  13. package/dist/AuthorizationDocuments.d.ts.map +1 -0
  14. package/dist/AuthorizationDocuments.js +84 -0
  15. package/dist/AuthorizationDocuments.js.map +1 -0
  16. package/dist/BuiltinActions.d.ts +11 -5
  17. package/dist/BuiltinActions.d.ts.map +1 -1
  18. package/dist/BuiltinActions.js +84 -119
  19. package/dist/BuiltinActions.js.map +1 -1
  20. package/dist/EventRouter.d.ts +11 -3
  21. package/dist/EventRouter.d.ts.map +1 -1
  22. package/dist/EventRouter.js +17 -32
  23. package/dist/EventRouter.js.map +1 -1
  24. package/dist/MessageAddress.d.ts +7 -5
  25. package/dist/MessageAddress.d.ts.map +1 -1
  26. package/dist/MessageAddress.js +14 -12
  27. package/dist/MessageAddress.js.map +1 -1
  28. package/dist/Module.d.ts +5 -4
  29. package/dist/Module.d.ts.map +1 -1
  30. package/dist/Module.js +222 -83
  31. package/dist/Module.js.map +1 -1
  32. package/dist/OAuthServer.d.ts +30 -0
  33. package/dist/OAuthServer.d.ts.map +1 -0
  34. package/dist/OAuthServer.js +607 -0
  35. package/dist/OAuthServer.js.map +1 -0
  36. package/dist/Portal.d.ts +41 -9
  37. package/dist/Portal.d.ts.map +1 -1
  38. package/dist/Portal.js +497 -185
  39. package/dist/Portal.js.map +1 -1
  40. package/dist/ProposalHitl.d.ts +18 -4
  41. package/dist/ProposalHitl.d.ts.map +1 -1
  42. package/dist/ProposalHitl.js +31 -41
  43. package/dist/ProposalHitl.js.map +1 -1
  44. package/dist/RunHandler.d.ts +18 -8
  45. package/dist/RunHandler.d.ts.map +1 -1
  46. package/dist/RunHandler.js +256 -137
  47. package/dist/RunHandler.js.map +1 -1
  48. package/dist/ThreadAddress.d.ts +8 -0
  49. package/dist/ThreadAddress.d.ts.map +1 -0
  50. package/dist/ThreadAddress.js +18 -0
  51. package/dist/ThreadAddress.js.map +1 -0
  52. package/dist/Translator.d.ts +52 -2
  53. package/dist/Translator.d.ts.map +1 -1
  54. package/dist/Translator.js +391 -121
  55. package/dist/Translator.js.map +1 -1
  56. package/dist/client.d.ts +2 -0
  57. package/dist/client.d.ts.map +1 -0
  58. package/dist/client.js +2 -0
  59. package/dist/client.js.map +1 -0
  60. package/dist/config.d.ts +22 -2
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +65 -13
  63. package/dist/config.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/run-events.d.ts +2 -0
  69. package/dist/run-events.d.ts.map +1 -1
  70. package/dist/run-events.js +5 -2
  71. package/dist/run-events.js.map +1 -1
  72. package/dist/types.d.ts +4 -22
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +12 -8
package/SPEC.md CHANGED
@@ -13,88 +13,155 @@ does not recompute them.
13
13
  listener ({§http-host}, {§module-http-mounts}) and owns no socket. Its setup
14
14
  registers the `agui` message scheme, and its start seam is `AguiPort`, the slice of
15
15
  {§application-port} it calls. A daemon without a listener leaves it out.
16
+ - §agui-client-types **A client's contracts are runtime-neutral.** `@plurnk/plurnk-agui/client` exports,
17
+ types only, what a client of this interface consumes: the state its `STATE_SNAPSHOT` and
18
+ `STATE_DELTA` events carry (`AguiPlusState` and its parts, {§agui-state-patches}) and the outcome a
19
+ management action reports (`ActionOutcome`). A browser type-checks it with DOM libraries and no Node
20
+ globals ({§runtime-neutral-root}); lint checks its whole source graph that way
21
+ (`tsconfig.browser.json`). The package root is the server module.
16
22
  - §agui-listener-admission **Bound is not ready.** The socket is core's
17
23
  ({§startup-listener-admission}): bound before durable-state admission and answering a
18
24
  retryable `503 service-starting` until daemon activation admits it after every module has
19
25
  started ({§module-http-mounts}). This module mounts `/agui` at `start`, so no
20
26
  request reaches it before its port is installed; from `stop` it answers `503
21
27
  service-stopping`.
22
- - §agui-thread-binding **A PLURNK workspace is the world; an AG-UI thread is a conversation over it**
23
- — the lifecycle vocabulary is defined by service {§lifecycle-terms}. PLURNK's machine model ({§machine-processes}) splits the world (a workspace: one
24
- curated workspace) from the CONVERSATION (a worker: a history over that world). AG-UI's workspace
25
- concept is the workspace, selected by name, VERBATIM, via `forwardedProps.plurnk.workspace`
26
- (attach if it exists, create with exactly that name otherwise — no prefix, no forging). The
27
- workspace is REQUIRED: a worker has no existence without a world, so its absence is a contract
28
- violation the module rejects (400) - never a workspace forged from the `threadId`. The
29
- `threadId` is the conversation over that world, and the name is the identity at
30
- BOTH levels: `threadId` == the workspace name binds the workspace's model worker (the default
31
- conversation, `ensureModelWorker` — its durable default-conversation role identifies it,
32
- never a name parse or root ordering); a DISTINCT
33
- `threadId` names its own conversation worker — found by name among model-origin workers if
34
- it exists (forks and prior conversations are addressable as threads; a client connection's
35
- worker or the runtime actor `_plurnk` is never a conversation, and a thread named after one
36
- fails at minting rather than binding), minted via `createConversationWorker`
37
- if it doesn't. A name the daemon no longer holds (a fresh database, a deleted worker) is minted
38
- anew with no history under the same name; the run-start status gauge then reports `loopId: null`
39
- where the previous run's reported a loop, which is how a client tells a new conversation from a
40
- resumed one. The core workspace envelope carries only the workspace and selected client
41
- actor ({§methods-rebind}); AG-UI owns the separate per-thread conversation binding.
42
- Process-local bindings use the complete `(workspace, threadId)` identity; equal thread names
28
+ - §agui-thread-binding **An AG-UI thread represents a worker; workspace selection is a Plurnk extension.**
29
+ AG-UI itself defines no workspace. Without a workspace selection, an opaque thread string
30
+ resolves through core's `agui` worker reference ({§worker-external-reference}). Its first
31
+ worker-creating Run allocates one ordinary folderless workspace and model worker atomically;
32
+ subsequent Runs reuse that durable association, across reconnects, workspace rename, and daemon
33
+ restart. Inspection, resume and workspace actions alone never allocate an opaque conversation;
34
+ an unknown thread is `404 thread-not-found`. Neither names nor approval owners are lookup keys.
35
+ Qualified thread addresses resolve directly under {§agui-thread-address}.
36
+ An explicit `forwardedProps.plurnk.workspace` selects a workspace by exact name: attach if it exists, create with that name
37
+ otherwise when the Run permits creation. An earlier binding serves only while the workspace
38
+ still carries that name. After a rename, the old name resolves afresh and the new one reaches
39
+ the renamed workspace. Under this binding, `threadId` names a worker by its immutable name and is never
40
+ compared with the workspace's name. A rename preserves workers and their names; subsequent
41
+ requests must select the workspace's new name. The worker name resolves among model-origin workers only (forks and earlier workers are
42
+ addressable as threads; the runtime actor `_plurnk` is never bound). A worker comes into being by an act addressed to it or by its owner's session attaching
43
+ to it ({§agui-action-binding}): the first such Run creates the worker the thread names via
44
+ `createWorker`, and a thread named after the runtime actor fails at minting. Reading it,
45
+ watching it passively, or acting on the workspace never creates one. A client that names no particular worker supplies its own default name. A name the
46
+ daemon no longer holds (a fresh database, a deleted
47
+ worker) is minted anew by the next act addressed to it, with no history; the run-start status
48
+ gauge then reports `loopId: null` where the previous run's reported a loop, which is how a
49
+ client tells a new worker from a resumed one. The core workspace envelope carries only the workspace ({§methods-rebind}); AG-UI owns
50
+ the per-thread worker binding.
51
+ Process-local bindings key on the workspace's identity and the `threadId`; equal thread names
43
52
  in different workspaces never share an envelope, Worker, replay, state, or active Run.
44
- Concurrent first requests acquire one envelope and conversation binding. Workspace
53
+ `workspace.create` selects its `name` literally under the same acquisition rule; a workspace
54
+ label resembling a qualified thread address is still a label, not an instruction to attach by ID.
55
+ Concurrent first requests acquire one envelope and worker binding. Workspace
45
56
  acquisition is serialized across threads, retaining each request's create/attach contract;
46
- the creating request supplies settings, and other threads acquire their own
47
- client actors in that world. Failed acquisitions retain their cause and leave no failed
57
+ the creating request supplies settings, and other threads attach that workspace. Failed acquisitions retain their cause and leave no failed
48
58
  cache entry preventing a later retry. Explicit Core creation retains its name-conflict
49
59
  semantics; protocol binding is not a second creation request.
50
- `loop.inject`, `loop.cancel`, and `run.fork` operate on the THREAD's conversation.
51
- `loop.inject` and `loop.cancel` steer a loop already running in a world, so they attach only:
52
- a workspace name that does not exist is the client's error, answered 404
53
- `workspace-not-found` with nothing created — never a second world minted after the name
54
- and answered there. Every other request may bring the world into being.
60
+ `loop.inject`, `loop.cancel`, and `run.fork` operate on the THREAD's worker.
61
+ A Run that may create its worker, or that binds none, may create the workspace; every
62
+ other Run attaches only ({§agui-action-binding}): a workspace name that does not exist is the
63
+ client's error, answered 404 `workspace-not-found` with nothing created — never a second workspace
64
+ minted after the name and answered there.
55
65
  `log.read` and `entry.read` default there and may explicitly select another workspace worker.
56
66
  Extended context persists across AG-UI Runs because the worker's log does.
67
+ A reply to any retained thread reference of the bound worker, its qualified address, or its
68
+ explicit workspace-selected name is speech in that worker's conversation. This changes neither
69
+ the recorded message address nor its approval owner. Arrivals from another thread spelling keep
70
+ their log-coordinate identities and source labels ({§agui-snapshot}).
57
71
  - §agui-run-authority **AG-UI owns the client lifecycle** — `threadId`, `runId`,
58
72
  `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR`, interrupt outcomes, and `RunAgentInput.resume`
59
73
  follow the official `@ag-ui/core` schemas. PLURNK does not maintain a parallel client
60
74
  lifecycle dialect. PLURNK owns the internal worker/loop/turn topology and projects it
61
75
  through standard events plus namespaced `plurnk.*` extensions.
62
76
 
77
+ §agui-thread-address A qualified thread is `<workspace ID>#<worker name>`: a canonical positive,
78
+ safe decimal integer and a native model-worker name. A string beginning with decimal digits then
79
+ `#` reserves this grammar; leading zeroes, an out-of-range ID or an invalid worker name receive
80
+ `400 thread-address-invalid`, not opaque-thread allocation. Other strings remain opaque, including
81
+ Unicode and `#` in a nonnumeric prefix. Qualified IDs always select the immutable workspace ID,
82
+ independently of forwarded workspace labels or target existence. An absent workspace is 404;
83
+ an absent worker may be created only by a worker-creating Run ({§agui-action-binding}).
84
+
63
85
  ## §agui-worker-owner Approval ownership
64
86
 
65
- An owning conversation is `agui://anonymous/threads/<encoded threadId>`, scoped by
66
- workspace. It is not a socket, Run, or client bookkeeping worker. `RunAgentInput.tools`
67
- declares its supported client-tool names, and `forwardedProps.plurnk.interactive` (a boolean)
68
- states whether a person attends it; unstated, nobody does. Control attachment registers that
69
- capability set and claims only runtime-owned work under {§worker-ownership}; observation does
70
- neither. Capabilities survive disconnection and a later declaration replaces them for future
71
- requests.
87
+ An owner is its thread's address, `agui://<principal>/threads/<encoded threadId>` ({§agui-run-source}),
88
+ scoped by workspace. Resolving another thread spelling to the same worker recognizes its retained
89
+ owner only when the principal is identical and the owner's thread is a reference to that worker
90
+ ({§agui-thread-address}). It neither rewrites ownership nor grants a different principal or another
91
+ worker's observer its approvals. An owner is not a socket, a Run, or a worker. `RunAgentInput.tools`
92
+ declares the client tools it implements, and `forwardedProps.plurnk.interactive` (a boolean)
93
+ states whether a person attends it; unstated, nobody does. Every AG-UI owner reviews, because a gate
94
+ is answered by a standard interrupt resume ({§agui-proposal-resolve}): the module registers core's
95
+ review capability, `request_approval` ({§worker-ownership}), beside the declared tools, and a client
96
+ declares no tool for it. Control attachment registers that capability set and claims only
97
+ runtime-owned work under {§worker-ownership}; observation does neither. Capabilities survive
98
+ disconnection and a later declaration replaces them for future requests.
72
99
 
73
100
  | Request | Ownership effect |
74
101
  |---|---|
75
- | Prompt, interrupt resume, or client operation | Control attachment for this conversation and its administrative actor. |
102
+ | A new message ({§agui-run-input}) or an interrupt resume | Control attachment for the worker the Run binds, if any ({§agui-action-binding}). |
76
103
  | Synchronization / connection with `forwardedProps.plurnk.control = true` | Explicit control attachment without inference. |
77
- | Passive synchronization / connection; other management reads | No claim and no capability replacement. |
104
+ | Passive synchronization / connection; a client operation ({§agui-client-operation}); other management Runs | No claim and no capability replacement. |
105
+
106
+ §agui-owner-activation **An owner's arrival activates its workspace.** A control attachment starts the
107
+ workspace's Functionality activation ({§methods-workspace-activate}) and does not wait for it: the Run
108
+ proceeds, and a first prompt waits only for what remains of that activation. Observation activates
109
+ nothing. Shutdown cancellation of that background demand is expected; other activation and
110
+ cleanup failures remain diagnostics. Core owns and joins the preparation, not the connected Run
111
+ ({§functionality-preparation-cancellation}).
78
112
 
79
113
  Approval delivery matches the recorded owner; interaction delivery matches its recorded
80
114
  recipient. No connected-ancestor fallback or topology-based authority exists. An owner may
81
115
  review an independently active child while its own worker is idle. Concurrent connections
82
- to one owner can see the same gate; Core settles it at most once. A matching worker's Run
83
- receives its own gate; only when none is connected does delivery use the owner's conversation
84
- Runs. Unrelated operation Runs do not receive it.
85
- Resurfacing a gate preserves its originating Run's continuation. Successful resume emits
86
- the standard `TOOL_CALL_RESULT` for each settled interrupt before continuing; this acknowledges
87
- the decision, not completion of the approved operation.
116
+ to one owner can see the same gate; Core settles it at most once. The owner's Runs are the Runs
117
+ on its thread, a passive observation included: claiming is not presenting. A matching worker's Run
118
+ receives its own gate; only when none is connected does delivery use the owner's Runs that
119
+ observe its worker. An operation or other management Run never receives one.
120
+ Each presentation of a gate retains its own Run continuation. A resumed client interaction is
121
+ acknowledged with the standard `TOOL_CALL_RESULT` of its call before the Run continues; a decision
122
+ on a proposal is no result of its own, since the operation's settlement answers its call
123
+ ({§agui-proposal-resolve}).
88
124
  Server approval policy belongs to Core.
89
125
 
126
+ ## §agui-run-disconnect Run lifetime
127
+
128
+ Approval ownership does not confer responsibility for cancelling running work.
129
+
130
+ | Connection | Unexpected disconnect |
131
+ |---|---|
132
+ | Message Run | Cancel its bound worker with `client_disconnected`. |
133
+ | Resume of a message Run's interrupt | Retain the same cancellation responsibility and worker binding. |
134
+ | Executing `op.exec` / `op.parse` Run | Cancel exactly its operation with `client_disconnected` ({§methods-loop-cancel}): its dispatch, and the executions it started that still run; once its dispatch opens the operation's loop if it disconnected before. |
135
+ | Observation, including approval of independently started work | Detach; do not cancel work. |
136
+ | Other management Run | Detach. |
137
+
138
+ An authored terminal or interrupt ends the connection normally without cancellation.
139
+ The disconnect handler is installed before asynchronous Run preparation; a disconnected
140
+ request must not subsequently start work. Interrupt continuations are adapter-owned,
141
+ not worker ownership or a second core lifecycle. Rediscovery, including after module
142
+ restart, presents a fresh observer continuation rather than inferring a prior connection's
143
+ cancellation responsibility.
144
+
90
145
  ## §agui-owner-connection Persistent observation
91
146
 
92
- `POST /agui/connect` accepts the standard `RunAgentInput`, without messages, actions, or
93
- resumes. It uses the same snapshots, events, and interrupt representation as synchronization
94
- ({§agui-conversation-sync}), but waits when the worker is idle. New work, including owner-addressed
147
+ `POST /agui/connect` accepts a standard `RunAgentInput` that submits nothing ({§agui-run-input}): its messages are
148
+ the client's copy, and one that carries a new message, an action or a resume is refused, 400 `invalid-connect-input`.
149
+ It binds as synchronization does: an owner's control connection creates the worker the
150
+ thread names when there is none, and a passive one creates nothing. It uses the same snapshots,
151
+ events, and interrupt representation
152
+ ({§agui-sync}), but waits when the worker is idle. New work, including owner-addressed
95
153
  child gates, is delivered without client polling or fabricated prompts. A terminal or interrupt
96
154
  ends that observation Run; the client reconnects after handling it. Detaching never cancels work.
97
- This is the transport implementation of the AG-UI `connect()` extension, not another event protocol.
155
+ This is the transport implementation of the AG-UI `connect()` extension, not another event protocol: `HttpAgent`
156
+ leaves `connect()` to its subclasses, and a client without one reattaches by running with nothing new.
157
+
158
+ §agui-shutdown **Shutdown releases every remaining Run.** Module `stop` refuses new
159
+ requests while admitted Runs retain terminal-event delivery. In `close`, after core
160
+ settlement ({§module-shutdown-order}), each still-open Run receives `service-stopping`
161
+ as a `RUN_ERROR` carrying that Problem ({§agui-plurnk-metadata}), then its response and heartbeat end. This
162
+ ends the transport Run, not a worker obligation. Already-ended Runs receive nothing
163
+ further; repeated closure is inert. Preparation finishing after closure cannot open
164
+ a new stream. Clients need not disconnect for the daemon to stop.
98
165
 
99
166
  ## §agui-projection The projection
100
167
 
@@ -103,24 +170,29 @@ One accepted Run or daemon notification produces zero-or-more AG-UI events:
103
170
  | plurnk wire | AG-UI events |
104
171
  | ------------------------------------------ | ------------ |
105
172
  | schema-valid `RunAgentInput` | `RUN_STARTED` + initial `STATE_SNAPSHOT` |
106
- | `forwardedProps.plurnk.mode = "sync"` | Durable conversation `MESSAGES_SNAPSHOT`, then pending interrupt, live observation, or `RUN_FINISHED` {§agui-conversation-sync} |
173
+ | A Run that carries nothing new ({§agui-run-input}) | Durable `MESSAGES_SNAPSHOT` of the bound worker, then pending interrupt, live observation, or `RUN_FINISHED` {§agui-sync} |
107
174
  | `log/entry` advancing to a newer turn | `STEP_FINISHED` + `STEP_STARTED` (`turn-<id>`); late updates to earlier receipts retain their row identity without rewinding the step or clearing delivered reasoning. |
108
- | `log/entry` WAIT (model) | A WAIT that carries a body speaks it: `TEXT_MESSAGE_START/CONTENT/END` with the body as written, then `CUSTOM plurnk.send` with the actual wait result; a bodiless WAIT is `CUSTOM plurnk.send` alone. No invented plan, no invented speech (#1003). |
109
- | Successful SEND replying to this conversation, including an addressed reply delivered from another worker | Optional readable-reasoning sequence {§agui-readable-reasoning}, then `TEXT_MESSAGE_START/CONTENT/END` + `CUSTOM plurnk.send` (signal/status) |
175
+ | `log/entry` WAIT (model) | A WAIT that carries a body speaks it: `TEXT_MESSAGE_START/CONTENT/END` with the body as written, its receipt in the speech's metadata ({§agui-plurnk-metadata}); a bodiless WAIT is an operation like any other, `TOOL_CALL_START/ARGS/END` + its receipt. No invented plan, no invented speech (#1003). |
176
+ | Successful SEND replying to this thread, including an addressed reply delivered from another worker | Optional readable-reasoning sequence {§agui-readable-reasoning}, then `TEXT_MESSAGE_START/CONTENT/END`, the row's signal and status in its metadata ({§agui-plurnk-metadata}) |
110
177
  | Other directed or unsuccessful SEND | Ordinary tool-call operation events; never assistant speech |
111
- | `log/entry` other op (model) | `TOOL_CALL_START/ARGS/END` (+ `TOOL_CALL_RESULT` when rx exists) |
112
- | `log/entry` actionless `kind=emissionAttempt` | Forensic row only; no assistant speech or reasoning replay. |
178
+ | `log/entry` other op (model) | The turn's readable-reasoning sequence when the row carries it {§agui-readable-reasoning}, then `TOOL_CALL_START/ARGS/END` + `TOOL_CALL_RESULT` carrying its receipt, except for a call that opened an obligation, which what discharges it answers {§agui-call-result}, and a gated call the Run's client already holds, which its settlement updates and answers ({§agui-proposal-resolve}) |
179
+ | §agui-call-result `log/entry` that discharges an obligation, naming its invocation ({§methods-invocation}) | `TOOL_CALL_RESULT` answering the call that opened it, under the discharging row's own coordinate, with what the worker received: a stream observation's result (the output's first page, terminal status and exit code, {§exec-stream}), a child's conclusion, or a completion reply's words. A call whose row opened an obligation (a receipt below 400 with `attrs.stream` or `attrs.message`, {§worker-obligations}) is answered only by what discharges it, whenever that lands, one result per observed channel of a stream; its receipt rides the row. Never `plurnk.ambient`. |
180
+ | `log/entry` actionless `kind=emissionAttempt` | `CUSTOM plurnk.ambient` ({§agui-ambient}); no assistant speech or reasoning replay. |
181
+ | §agui-parse-refusal `log/entry` model `error` row, a parse refusal | The call the worker attempted ({§parse-attempt}): `TOOL_CALL_START` named by the attempt's verb, its arguments `{ text }` the statement as written, then `TOOL_CALL_RESULT` with the refusal, its status the failure; a refusal that names no attempt is no call and rides `CUSTOM plurnk.ambient` ({§agui-ambient}) |
113
182
  | `log/entry` READ of `reasoning://<worker>/…` or `ops://<worker>/…` | An ordinary operation receipt, not a reasoning stream. Standard reasoning delivery and replay retain original provider evidence under {§agui-readable-reasoning}. |
114
- | `log/entry` origin≠model | `CUSTOM plurnk.ambient` (foists, deltas, narrations) |
115
- | client-owned `loop/proposal` | `TOOL_CALL_START/ARGS/END`, `STEP_FINISHED` when a turn step is active, then `RUN_FINISHED` with an interrupt outcome; its resume Run reopens the continued turn with `STEP_STARTED` after `RUN_STARTED` and initial state |
183
+ | `log/entry` arrival (`_plurnk` SEND, `kind=message`) | A user `TEXT_MESSAGE_START/CONTENT/END`, as on replay ({§agui-snapshot}): a message a client on this thread sent keeps its message id, and the Run that submitted it, which already holds it, receives only `TEXT_MESSAGE_START/END` carrying its row's facts ({§agui-plurnk-metadata}); any other is named by its source |
184
+ | §agui-progress-reply `log/entry` reply reporting progress on a message this worker's operation sent ({§methods-answering}) | A user `TEXT_MESSAGE_START/CONTENT/END` named by its source, as on replay; a reply that completes the message answers its call instead ({§agui-call-result}), and one to a message this worker did not send rides `CUSTOM plurnk.ambient` |
185
+ | `log/entry` other origin≠model | `CUSTOM plurnk.ambient` (foists, deltas, narrations, {§agui-ambient}) |
186
+ | `log/entry` and `loop/terminated` of a subagent's loop | Its subagent run's projection ({§agui-subagents}): the rows above in its worker's namespace, each event attributed by `subagentRunId`, its segment bounded by `SUBAGENT_STARTED` and its conclusion |
187
+ | client-owned `loop/proposal` | The gated operation's own call, unless the Run's client holds it or the Run holds no conversation of its worker ({§agui-proposal-resolve}), `STEP_FINISHED` when a turn step is active, then `RUN_FINISHED` with an interrupt outcome; its resume Run reopens the continued turn with `STEP_STARTED` after `RUN_STARTED` and initial state |
116
188
  | `loop/packet` | `STATE_DELTA` replacing the bound thread's loop id, lifecycle, and exact packet count |
117
- | `loop/terminated` | `STATE_DELTA` (latest-turn gauge only) + `CUSTOM plurnk.terminated` (the complete daemon terminal, including physical-request accounting and top-level attribution) + `RAW` (the provider's opaque metadata bag, `source: provider`, §475) + `RUN_FINISHED` (`result.status === 200`) or `RUN_ERROR` (otherwise, from the exact RFC 9457 Problem Details) |
118
- | transport failure after SSE opens | `CUSTOM plurnk.problem` (exact Problem Details) + `RUN_ERROR` (`code` = Problem `type`, `message` = Problem `detail`) |
119
- | `notice/event` | `CUSTOM plurnk.notice`; routine derivation lifecycle uses only `STATE /plurnk/status/activity`, and the drain's lifecycle beat ({§loop-status-notice}) only `STATE /plurnk/status/lifecycle` and `waitUntil`, while warnings/errors also retain their diagnostic Notice. Client discovery and passive synchronization also emit startup configuration Notices ({§configuration-repair-path}) without requiring a working model. An unavailable provider catalog is omitted from the state snapshot and reported as `CUSTOM plurnk.problem`; it does not terminate attachment or pretend the catalog is empty. |
120
- | `workspace/preparation` | `STATE_DELTA /plurnk/status/preparation`, scoped to attached conversations in that workspace; no chat or diagnostic duplication. |
189
+ | §agui-run-outcome `loop/terminated` | `STATE_DELTA` (latest-turn gauge only) + `RAW` (the provider's opaque metadata bag, `source: provider`, §475), then the Run's end: a loop that completed (below 400) is `RUN_FINISHED` with `outcome: success` and its exact terminal result as `result`; one stopped without failing (499) is `RUN_FINISHED` with `outcome: cancelled`; any other is `RUN_ERROR`, from the exact RFC 9457 Problem Details. The Run's end, finished or failed, carries its loop's token `usage`: AG-UI's own AI SDK mapping and per-model aggregation of the counts each physical request reported, absent when none did ({§agui-cost-evidence}). It also carries the complete daemon terminal record as its metadata, physical-request accounting and top-level attribution included ({§agui-plurnk-metadata}). |
190
+ | transport failure after SSE opens | `RUN_ERROR` (`code` = Problem `type`, `message` = Problem `detail`, the exact Problem Details as its metadata, {§agui-plurnk-metadata}) |
191
+ | `notice/event` | `CUSTOM plurnk.notice`; routine derivation lifecycle uses only `STATE /plurnk/workspace/activity`, and the drain's lifecycle beat ({§loop-status-notice}) only `STATE /plurnk/status/lifecycle` and `waitUntil`, while warnings/errors also retain their diagnostic Notice. Client discovery and passive synchronization also emit startup configuration Notices ({§configuration-repair-path}) without requiring a working model. An unavailable provider catalog is omitted from the state snapshot and reported as `CUSTOM plurnk.problem`; it does not terminate attachment or pretend the catalog is empty. |
192
+ | `workspace/preparation` | `STATE_DELTA /plurnk/workspace/preparation`, scoped to the Runs attached to that workspace; no chat or diagnostic duplication. |
121
193
  | `reasoning/event` | Standard live `REASONING_START` → `REASONING_MESSAGE_START` → one or more `REASONING_MESSAGE_CONTENT` → `REASONING_MESSAGE_END` → `REASONING_END` {§agui-readable-reasoning} |
122
194
  | §agui-outside-text `outside/event` | `CUSTOM plurnk.outside` with `{ coordinate, text, tokens }` exactly as core stored and weighed it ({§notifications-outside-event}): the thread's model worker's text outside every operation, once per admitted emission, after the turn's `STEP_STARTED`. Never assistant speech, reasoning or a NOTE; a foreign worker's text never enters the thread. |
123
- | `stream/event` + `stream/concluded` | `CUSTOM plurnk.stream` + `ACTIVITY_SNAPSHOT` (the standard background-activity channel: `activityType` = the scheme, replace-snapshot, §475). A conclusion preserves its exact universal `result`, including RFC 9457 Problem Details; AG-UI does not reconstruct failure from a status or summary. |
195
+ | `stream/event` + `stream/concluded` | `CUSTOM plurnk.stream` alone: execution metadata, the stream's progress and its exact universal `result`, including RFC 9457 Problem Details, as they happen. Never a conversation message; the execution's result reaches the conversation through its observation {§agui-call-result}. AG-UI does not reconstruct failure from a status or summary. |
124
196
 
125
197
  §agui-lifecycle-projection **Lifecycle, memory and speech remain distinct.** Live
126
198
  events, replay and `log.read` preserve literal native bodies. No task inventory
@@ -129,10 +201,10 @@ or ACP plan is inferred from prose.
129
201
  | Projection | Standard representation |
130
202
  | ---------- | ----------------------- |
131
203
  | NOTE | Ordinary tool-call operation and durable row, not assistant speech or a substitute reasoning stream |
132
- | WAIT | Durable row and lifecycle signal; its text does not become a response |
204
+ | WAIT | Ordinary tool-call operation and durable row, its receipt the result; a WAIT that carries a body speaks it instead ({§agui-projection}) and never becomes a response |
133
205
  | SEND response | Assistant message through {§loop-response-messages}; delivery and completion remain independent |
134
206
  | Completion | Standard run lifecycle without an invented message |
135
- | Replay | Delivered messages in chronological order |
207
+ | Replay | The live projection's messages, in log order {§agui-snapshot} |
136
208
 
137
209
  §agui-readable-reasoning **Readable provider reasoning uses AG-UI's standard
138
210
  reasoning channel.** A core `{§notifications-reasoning-event}` for the thread's
@@ -140,60 +212,96 @@ model worker projects each exact delta immediately through a balanced standard
140
212
  reasoning lifecycle identified by its durable model-call id and physical-request
141
213
  sequence. A retried request gets a new reasoning message instead of appending to
142
214
  the failed request's partial reasoning. Foreign-worker and BARE reasoning never
143
- enter the thread. Failed or rejected requests may therefore leave honest
144
- transient reasoning that is not replayed.
145
-
146
- Core also derives optional complete `reasoning` on admitted SEND, NOTE and disposition
147
- rows from their durable packet ({§methods-readable-reasoning}). When that value
148
- was not already delivered by the completed live stream, projection emits it
149
- atomically before the turn's first corresponding row under `<OP identity>/reasoning`.
150
- Subsequent rows in the same turn do not repeat it. A standard interrupt may divide one durable Loop across consecutive
215
+ stream into the thread; a subagent's arrives with its rows, attributed to its run
216
+ ({§agui-subagents}). Failed or rejected requests may therefore leave honest
217
+ transient reasoning that is not replayed. A Run ended before its request, revoked
218
+ ({§agui-oauth-revocation}), stopped ({§agui-shutdown}) or failed, closes the reasoning message and
219
+ span it opened before its `RUN_ERROR`; the request streams on to every Run still observing it. A Run
220
+ that joins after a request's reasoning began presents none of that request's deltas.
221
+
222
+ Core also derives a turn's optional complete `reasoning` onto its first model operation
223
+ row from the durable packet ({§methods-readable-reasoning}). When that value was not
224
+ already delivered by the completed live stream, projection emits it atomically before
225
+ that row under the identity of the request that streamed it, `model-call-<call>/request-<n>/reasoning`
226
+ ({§methods-readable-reasoning}); history a FORK inherited, which no request of this worker streamed,
227
+ is `<coordinate>/reasoning`. A standard interrupt may divide one durable Loop across consecutive
151
228
  AG-UI Runs; the Run B projection inherits Run A's delivered-reasoning evidence
152
- before the stopped operation is released. Reattach replaces transient attempt presentation with the durable
153
- accepted `ReasoningMessage` once per turn in
154
- `MESSAGES_SNAPSHOT`. Empty evidence emits nothing, NOTE never substitutes for
229
+ before the stopped operation is released. The accepted request's reasoning keeps its live identity in
230
+ `MESSAGES_SNAPSHOT`, once per turn, so a snapshot leaves it where the live stream put it and drops only the
231
+ transient reasoning of failed or rejected requests. Empty evidence emits nothing, NOTE never substitutes for
155
232
  reasoning ({§agui-lifecycle-projection}), and a response row still precedes its text
156
233
  sequence.
157
234
 
158
- - **An op row IS a tool call** — its `coordinate` is the `toolCallId`, its tx the args (one
159
- delta: a dispatched plurnk op is atomic), its rx the result. The log-shaped richness the
160
- core vocabulary can't hold (curation metadata, tags, coordinates) stays on the row inside
161
- `plurnk.ambient`/`TOOL_CALL_RESULT` payloads.
235
+ - **An op row IS a tool call** — its coordinate `L/T/S`, from the row's ordinals
236
+ ({§methods-log-coordinate}), is the `toolCallId`; its durable statement is the args, every
237
+ operand exactly as parsed (host, scope, pattern, aside, both COPY/MOVE selections) without the
238
+ verb that is the tool's name or its source position, in one delta because a dispatched plurnk op
239
+ is atomic; its rx is the result, except for a call that opened an obligation, whose discharge
240
+ is ({§agui-call-result}). The row's facts ride the call's metadata ({§agui-plurnk-metadata}).
162
241
  - §agui-encrypted-reasoning **The encrypted-reasoning interface is not implemented.**
163
242
  AG-UI defines encrypted-reasoning events and message values; plurnk emits none and
164
243
  stores none ({§provider-open-reasoning}). Implementing a transport for bytes the
165
244
  service cannot read would add surface without adding capability. Readable reasoning
166
- streams and conversation replay remain {§agui-readable-reasoning}.
245
+ streams and snapshot replay remain {§agui-readable-reasoning}.
167
246
  - §agui-custom-namespace **The custom namespace** — plurnk-specific metadata rides
168
- `CUSTOM` events named `plurnk.*` (`plurnk.send`, `plurnk.ambient`,
169
- `plurnk.notice`, `plurnk.outside`, `plurnk.stream`, `plurnk.terminated` — the full loop
170
- outcome and {§provider-accounting} the gauge `STATE_DELTA` cannot represent). Generic frontends skip unknown customs; plurnk-aware frontends render
171
- them richly. Nothing plurnk-specific ever masquerades as a core event.
247
+ `CUSTOM` events named `plurnk.*` (`plurnk.ambient`, `plurnk.notice`, `plurnk.outside`, `plurnk.stream`,
248
+ `plurnk.descendant` for a descendant outside any subagent, and `plurnk.problem` for a Problem that ends no
249
+ Run, such as an unavailable provider catalog): facts with no standard subject. Generic frontends skip unknown
250
+ customs; plurnk-aware frontends render them richly. Nothing plurnk-specific ever masquerades as a core event.
251
+ - §agui-plurnk-metadata **plurnk facts ride standard metadata.** A plurnk fact about something the
252
+ standard conversation or Run holds rides that thing's `metadata` under the one key `plurnk`, on the event
253
+ that builds it and on its snapshot message alike, so the official reducer folds the same metadata live and
254
+ on refresh. A fact with no standard subject stays a declared `CUSTOM plurnk.*` extension
255
+ ({§agui-custom-namespace}).
256
+
257
+ | Carrier | `metadata.plurnk` |
258
+ |---|---|
259
+ | `RUN_FINISHED` or `RUN_ERROR` that the bound Loop's terminal ends | `terminal`: the Loop's complete terminal record ({§notifications-loop-terminated}) with its `workspaceId`, numbers verbatim ({§agui-numbers-passthrough}): the full loop outcome and {§provider-accounting} the gauge `STATE_DELTA` cannot represent |
260
+ | `RUN_ERROR` of a Run that fails without its Loop's terminal | `problem`: the exact RFC 9457 Problem Details |
261
+ | What a row builds: a tool call (`TOOL_CALL_START`, folded onto the call), its result (`TOOL_CALL_RESULT`, onto the tool message), speech or an arrival (`TEXT_MESSAGE_START`, onto the message), and the same parts of a snapshot | The row's facts: `{ id, coordinate, worker, origin, op, source, address, signal, status, attrs, state }`, its durable id, log coordinate `L/T/S` ({§methods-log-coordinate}), worker, origin, operation, causal source, the address it recorded (`scheme://host[:port]path[?query][#fragment]`, or a file's bare path), signal, receipt status (the failure flag), attributes and lifecycle state ({§methods-log-entry-wire}). A gated call's settlement folds its settled facts onto the call ({§agui-proposal-resolve}). Speech, which holds only its body, also carries its heading's `aside` and the row's `receipt` |
262
+ | A proposal's `Interrupt` ({§agui-proposal-resolve}) | `proposal`: Core's `ProposalProjection` ({§proposal-projection}), what the reviewer reviews |
263
+ | `SUBAGENT_STARTED` of a subagent run ({§agui-subagents}) | Its worker's identity and generation, `{ workerId, name, parentWorkerId, depth }` ({§agui-delegation-observation}) |
264
+
265
+ An action Run's `RUN_FINISHED.result` is the action's whole outcome, `{ kind, ok, result | problem }`.
172
266
 
173
267
  §agui-status-children **Children are counted by the daemon, never inferred.** `status.children`
174
268
  is the bound Worker's alive direct children — `queued`, `running`, or `parked` under
175
269
  {§loop-lifecycle-vocabulary}; a parked child still owes a result and is exactly the one a user
176
270
  hops to. The module computes it from `ApplicationPort.listWorkers({ parentWorkerId })` for every
177
- snapshot and whole-gauge replacement, and republishes `STATE_DELTA /plurnk/status/children` only
178
- when the count changes: after another worker's `loop/terminated` or `loop/packet`, or after the
179
- bound Worker's own `WORK`, `FORK`, or `KILL` row lands. Clients render the number as their child
271
+ snapshot and whole-gauge replacement, and reads it again after another worker's `loop/terminated` or
272
+ `loop/packet`, after any `turn_generated` notice, or after the bound Worker's own `WORK`, `FORK`, or
273
+ `KILL` row lands. It republishes `STATE_DELTA /plurnk/status/children` only when the count
274
+ differs from the one the Run's client holds (its snapshot's, its latest whole-gauge replacement's,
275
+ then each delta's), and never before the snapshot: a change that raced the snapshot is published
276
+ after it. Clients render the number as their child
180
277
  indicator and never poll the directory to keep it honest. Topology is navigation: the client
181
278
  hops to a child to speak to it; watching a child's rows is the delegation observation a Run
182
279
  asks for ({§agui-delegation-observation}).
183
280
 
184
- §agui-status-descendants **The descendants' spend is the daemon's, republished, never promoted.**
185
- `status.descendants` is `{ requests, usage, knownUsage, costUsd, knownCostUsd }`: the daemon's {§provider-accounting}
186
- projection over the bound Worker's descendant tree on this delegation
187
- ({§methods-worker-descendants}), without its request list — `requests` counts them, so zero
281
+ §agui-status-spend **The spend is the daemon's, from the Worker's beginning, republished, never promoted.**
282
+ `status.spend` is `{ requests, usage, knownUsage, costUsd, knownCostUsd }`: the daemon's {§provider-accounting}
283
+ projection over every settled request the bound Worker and its descendants have made since the Worker
284
+ began ({§methods-worker-accounting}), without its request list — `requests` counts them, so zero
188
285
  requests reads as nothing spent rather than unknown, and all totals and known subtotals pass through
189
- verbatim ({§agui-numbers-passthrough}). The module reads it beside `children` for every snapshot
190
- and whole-gauge replacement, and republishes `STATE_DELTA /plurnk/status/descendants` only when
191
- the projection changes: after another worker's `loop/packet` or `loop/terminated`, after another
192
- worker's `turn_generated` notice (a descendant's spend just settled), or after the bound Worker's
193
- own `WORK`, `FORK` or `KILL` row. Descendant notices, packets and terminals themselves are not
194
- promoted ({§agui-delegation-observation}); the parent's own `turn_generated` and
195
- `plurnk.terminated` accounting keep their meaning. With the lifecycle beat
196
- ({§loop-status-notice}) a parked parent's clock and its children's cost both move.
286
+ verbatim ({§agui-numbers-passthrough}). A fresh observer therefore starts from the Worker's spend so far.
287
+ The module reads it beside `children`, at the same moments, and republishes `STATE_DELTA
288
+ /plurnk/status/spend` under the same rule ({§agui-status-children}): only when the projection differs
289
+ from the one the Run's client holds, never before the snapshot; a `turn_generated` notice is the moment
290
+ a turn's spend settles. Descendant notices, packets and terminals themselves
291
+ are not promoted ({§agui-delegation-observation}); one loop's own `turn_generated` and terminal-record
292
+ accounting keep their meaning. With the lifecycle beat ({§loop-status-notice}) a parked parent's clock
293
+ and its children's cost both move.
294
+
295
+ §agui-status-timing **A Loop's timing is the daemon's, and it travels with the Loop's identity.**
296
+ `status.claimedAt` is when the latest Loop first ran ({§loop-claim-latency}) and `status.terminatedAt`
297
+ when it concluded, both Unix milliseconds like `waitUntil` and null until they happen. A snapshot
298
+ reads them from `ApplicationPort.listWorkerLoops` ({§application-loop-observation}); live, they arrive
299
+ with the events that change the Loop. A lifecycle beat ({§loop-status-notice}) replaces `lifecycle`,
300
+ `loopId`, `packetCount`, `waitUntil`, `claimedAt` and `terminatedAt` in one delta, and the terminal
301
+ ({§notifications-loop-terminated}) replaces `lifecycle`, `loopId` and `terminatedAt` in one delta
302
+ before the Run's own terminal. A connected observer therefore never holds one Loop's stamp beside
303
+ another Loop's number, and a fresh observer measures a running Loop's elapsed time from
304
+ `claimedAt`, never from its own attachment.
197
305
 
198
306
  §agui-numbers-passthrough **Gauge numbers pass through verbatim.** The module
199
307
  never recomputes the daemon's gauge or promotes accounting into application
@@ -201,78 +309,120 @@ state paths that could be mistaken for standard AG-UI fields.
201
309
 
202
310
  §agui-state-patches Each Run's `STATE_SNAPSHOT` precedes its `STATE_DELTA`s.
203
311
  Deltas use RFC 6902 `replace` on existing snapshot paths; replay yields the
204
- current gauge without clients inventing missing fields.
312
+ current gauge without clients inventing missing fields. `plurnk.status` and `budget` are the
313
+ bound Worker's gauge and ride only a Run that binds one ({§agui-action-binding}).
205
314
 
206
315
  | AG-UI state location | Daemon source | Meaning |
207
316
  |:--------------------------------------------|:-----------------------------------------|:--------|
317
+ | §agui-provider-routes `snapshot.plurnk.providers[*]` | `providers.list.aliases[*]` | Each declared alias as the route a worker seeded from it gets: `alias`, `provider`, `model` and, when the model has an effort dimension, the alias's configured `effort` with `effortSource: "default"`, which is what that worker reads back ({§worker-effort}); then `active` and `inputCapacity`. A client presents the active alias's route for a worker no act has created yet, and creates nothing to learn it. |
208
318
  | `snapshot.plurnk.providers[*].inputCapacity` | `providers.list.aliases[*]` | Each provider alias's derived physical input capacity, or `null` when unknown. |
209
319
  | `snapshot.plurnk.status.model` | `ApplicationPort.readWorkerModel` | The bound Worker's durable resolved model route, including its alias when selected. |
210
320
  | `snapshot.plurnk.status.loopId` | representative `ApplicationLoopProjection.id` | Selected under {§application-worker-observation}: live work precedes terminal history; `null` when none exists. Maintenance-only loops are excluded by Core under {§application-loop-observation}. |
211
321
  | `snapshot.plurnk.status.packetCount` | latest `ApplicationLoopProjection.packetCount` | Exact packet-bearing Turn count; packetless Turns and provider retries do not contribute. |
212
- | §agui-status-preparation `snapshot.plurnk.status.preparation` | `ApplicationPort.workspacePreparationStatus(workspaceId)` | Current workspace capability preparation ({§functionality-preparation-visibility}), also sent as `STATE_DELTA /plurnk/status/preparation`. Initial and reattachment snapshots carry the same state. An empty array clears it; no polling or model inference is required. |
213
322
  | `snapshot.plurnk.status.lifecycle` | latest `ApplicationLoopProjection.status` | Queued `100` is `queued`, not executing or WAITing. Running `102`, parked `202`, and terminal states retain their ordinary lifecycle meanings. |
214
323
  | §agui-status-wait `snapshot.plurnk.status.waitUntil` | `ApplicationLoopProjection.waitUntil` | Absolute Unix-millisecond observation deadline while parked, otherwise `null`. The lifecycle beat replaces it atomically with lifecycle; waking or concluding clears it. Clients may render the remaining time locally without polling, guessing from WAIT receipts, or implying work will finish at that time. |
215
324
  | `STATE_DELTA /plurnk/status/lifecycle` | `loop/packet` (running), `loop/terminated` (completed, failed), and the drain's lifecycle beat ({§loop-status-notice}: running on claim, parked on park, queued on wake) | A parked delegation and its wake reach the client the moment the daemon decides them; a client never infers parked from a WAIT row. |
216
- | `snapshot.plurnk.status.children` | `ApplicationPort.listWorkers({ parentWorkerId })` | The bound Worker's alive direct children — `queued`, `running`, or `parked` under {§loop-lifecycle-vocabulary}; a parked child still owes a result. Computed by the daemon for every snapshot and whole-gauge replacement, and republished as `STATE_DELTA /plurnk/status/children` only when it changes: after another worker's `loop/terminated` or `loop/packet` (it may be a child), or after the bound Worker's own `WORK`, `FORK`, or `KILL` row lands (it spawned or killed one). Clients render the number and never poll the directory for it ({§agui-status-children}). |
217
- | `snapshot.plurnk.status.descendants` | `ApplicationPort.descendantAccounting({ workspaceId, workerId, loopId })` | `{ requests, usage, knownUsage, costUsd, knownCostUsd }`: the daemon's {§provider-accounting} projection of the bound Worker's descendants' settled spend on this delegation, without the request list. Computed for every snapshot and whole-gauge replacement, republished as `STATE_DELTA /plurnk/status/descendants` only when it changes ({§agui-status-descendants}). |
218
- | `STATE_DELTA /plurnk/status/*` | packet, termination, and derivation events | Replaceable lifecycle, packet chronology, and transient activity. Reattachment snapshots carry current derivation activity through the same projection, without polling or duplicate routine Notices. Clients never reconstruct packet count from row or STEP traffic. |
219
- | `snapshot.budget` | Run initialization | Creates all four gauge fields as `null`, so subsequent RFC 6902 `replace` operations always address existing values. |
325
+ | `snapshot.plurnk.status.children` | `ApplicationPort.listWorkers({ parentWorkerId })` | The bound Worker's alive direct children — `queued`, `running`, or `parked` under {§loop-lifecycle-vocabulary}; a parked child still owes a result. Computed for every snapshot and whole-gauge replacement, republished as `STATE_DELTA /plurnk/status/children` only when it differs from what the Run's client holds, never before its snapshot ({§agui-status-children}). Clients render the number and never poll the directory for it. |
326
+ | §agui-workspace-state `snapshot.plurnk.workspace` | the Run's client envelope | `{ id, name, projectRoot, projectLabel, preparation, activity }`: the bound workspace, its folder and the folder's label as the daemon spells it ({§workspace-label}), both null without a folder, and the activity that belongs to the workspace rather than to a Worker. Every Run bound to a workspace carries it, whether or not it binds a Worker. Clients display the label and never derive one. |
327
+ | `snapshot.plurnk.workspace.preparation` | `ApplicationPort.workspacePreparationStatus(workspaceId)` | Current workspace capability preparation ({§functionality-preparation-visibility}), also sent as `STATE_DELTA /plurnk/workspace/preparation`. Initial and reattachment snapshots carry the same state. An empty array clears it; no polling or model inference is required. |
328
+ | `snapshot.plurnk.workspace.activity` | `ApplicationPort.workspaceDerivationStatus(workspaceId)` | Current workspace derivation: phase, counts, percent and message while preparing, indexing or failed, and `null` when none is under way or the last pass completed. Live `search_progress` Notices replace it as `STATE_DELTA /plurnk/workspace/activity`; a Loop's terminal leaves it as derivation last reported it. Reattachment snapshots carry the current derivation through the same projection, without polling or duplicate routine Notices. |
329
+ | `snapshot.plurnk.status.claimedAt`, `.terminatedAt` | latest `ApplicationLoopProjection.claimedAt`, `.terminatedAt` | When the latest Loop first ran and when it concluded, Unix milliseconds, null until they happen; live, the lifecycle beat and the terminal replace them in the same delta as the Loop's identity ({§agui-status-timing}). |
330
+ | `snapshot.plurnk.status.spend` | `ApplicationPort.workerAccounting({ workspaceId, workerId })` | `{ requests, usage, knownUsage, costUsd, knownCostUsd }`: the daemon's {§provider-accounting} projection of the bound Worker's and its descendants' settled spend since the Worker began, without the request list. Computed for every snapshot and whole-gauge replacement, republished as `STATE_DELTA /plurnk/status/spend` only when it differs from what the Run's client holds, never before its snapshot ({§agui-status-spend}). |
331
+ | `STATE_DELTA /plurnk/status/*` | packet, termination, and lifecycle events | The bound Worker's replaceable lifecycle and packet chronology. Clients never reconstruct packet count from row or STEP traffic. |
332
+ | `snapshot.budget` | initialization of a Run that binds a Worker | Creates all four gauge fields as `null`, so subsequent RFC 6902 `replace` operations always address existing values. |
220
333
  | `STATE_DELTA /budget/curationWeight` | `loop/terminated.usage.curationWeight` | Latest assembled packet's model-independent curation weight, or `null` when no packet exists. |
221
334
  | `STATE_DELTA /budget/curationBudget` | `loop/terminated.usage.curationBudget` | Latest turn's provider-derived curation calibration, or `null` when capacity is unknown. |
222
335
  | `STATE_DELTA /budget/contextTokens` | `loop/terminated.usage.contextTokens` | Latest emission call's last physical request input occupancy, or `null` when that call issued none or reported none. |
223
336
  | `STATE_DELTA /budget/contextCapacity` | `loop/terminated.usage.contextCapacity` | That same latest emission call's request-shaped physical input capacity, or `null` when unknown. |
224
337
 
225
- §agui-cost-evidence AG-UI defines lifecycle, messages, tools, state transport,
226
- `RAW`, and `CUSTOM`, but no standard token-usage or monetary-accounting event.
227
- `plurnk.terminated.usage.accounting` therefore preserves the daemon's complete
338
+ §agui-cost-evidence AG-UI carries token usage on `RUN_FINISHED` ({§agui-run-outcome}), but it defines
339
+ no monetary accounting and no per-request record. The terminal record's `usage.accounting` ({§agui-plurnk-metadata}) therefore
340
+ preserves the daemon's complete
228
341
  {§provider-accounting} verbatim: ordered physical request records, conventional
229
342
  input/output totals with cache/reasoning details, and exact-decimal nullable
230
- `costUsd`. The custom event distinguishes charged, Models.dev-estimated,
343
+ `costUsd`. The terminal record distinguishes charged, Models.dev-estimated,
231
344
  unknown, and exact-zero evidence without fabricating a value. Generic clients
232
- ignore the extension; PLURNK clients consume the same contracts-owned shape as
345
+ ignore the metadata; PLURNK clients consume the same contracts-owned shape as
233
346
  every other daemon surface.
234
347
 
235
- - §agui-row-channel **The row channel** — every log row ALSO rides `CUSTOM plurnk.row`
236
- carrying the complete client-facing row (curation metadata, durable tags, coordinate)
237
- alongside its core projection. An emission row ({§emission-row}) rides it like any other; the
238
- operations it announces arrive as their own rows, so a client that renders operations omits it. Literal bodies follow {§agui-lifecycle-projection}; rich clients
239
- never receive the internal Plan extension. Generic clients ignore this metadata
240
- channel.
241
- - **The gauge starts true** — `RUN_STARTED` is followed by a `STATE_SNAPSHOT` carrying the
242
- daemon's provider, bound-Worker model, latest Loop, and exact packet-count truth, then
243
- `STATE_DELTA`s. A dropped SSE stream cancels the loop (`loop.cancel`) — the frontend hanging
244
- up IS the abort signal; no worker is orphaned unwatched.
348
+ - §agui-ambient **A row outside the conversation rides whole** — a row that builds no part of the
349
+ conversation rides `CUSTOM plurnk.ambient` carrying the complete client-facing row: a foist, delta or
350
+ narration, another worker's row, a descendant's row outside any subagent ({§agui-subagents}), an
351
+ emission the daemon rejected, and a parse refusal that names no attempt ({§agui-parse-refusal}). An emission row ({§emission-row}) rides it like any other; the operations
352
+ it announces arrive as their own parts, so a client that renders operations omits it. Literal bodies
353
+ follow {§agui-lifecycle-projection}; rich clients never receive the internal Plan extension. Generic
354
+ clients ignore this channel.
355
+ - §agui-run-start **The gauge starts true** — `RUN_STARTED` is followed by a `STATE_SNAPSHOT`
356
+ carrying the daemon's providers, the bound workspace and, when the Run binds a Worker, that
357
+ Worker's model, latest Loop, and exact packet-count truth, then `STATE_DELTA`s. Nothing precedes them: what reaches a Run while its snapshot is read
358
+ follows them, in the order it arrived; a Run that ends first ends with its terminal alone.
245
359
 
246
360
  - §agui-replay **Unoriented reattach replays once** — a rediscovered thread (the module restarted, a second
247
361
  frontend arrived) whose Run input carries none of the log's durable assistant identities attaches
248
- to its existing workspace by name→id and opens ORIENTED: durable
249
- arrival rows replay as user messages, named by their source when another actor caused them ({§message-causal-source}), the model worker's SEND speech
250
- replays chronologically, and the one validated current Run user message follows them under a
251
- Run-owned identity in the authoritative `MESSAGES_SNAPSHOT`. Client-claimed earlier history is not
252
- imported. A client already carrying any durable assistant identity receives no repeated snapshot.
253
- Everything else stays
254
- reachable via live `plurnk.row`. Pending proposals and client interactions remain durable while
255
- their operation owners are live and are presented as interrupts when the owning conversation is
362
+ through its durable reference or explicit address and opens ORIENTED: the authoritative `MESSAGES_SNAPSHOT`
363
+ carries the bound worker's messages ({§agui-snapshot}) and then the Run's new message ({§agui-run-input})
364
+ under a Run-owned identity. Client-claimed earlier history is not imported. A client already
365
+ carrying any durable assistant identity receives no repeated snapshot. Pending proposals and client interactions remain durable while
366
+ their operation owners are live and are presented as interrupts when the owning thread is
256
367
  resumed; a days-old question is discoverable, never converted into a mystery hang.
257
368
 
258
- - §agui-conversation-sync **Conversation synchronization is an inference-free AG-UI Run.**
259
- `forwardedProps.plurnk.mode = "sync"` on the existing Run endpoint accepts no messages,
260
- management action, or interrupt resume. It binds the named workspace and conversation,
369
+ - §agui-sync **Synchronization is an inference-free AG-UI Run.**
370
+ A Run that carries no new message, action or interrupt resume synchronizes ({§agui-run-input}); its messages
371
+ are the client's copy. A passive observation (`forwardedProps.plurnk.control`
372
+ absent or false) resolves an existing worker under {§agui-thread-binding}, creates
373
+ nothing and claims nothing. An owner's control observation is the owner's session
374
+ attaching: it binds, creates and claims as a message Run does ({§agui-action-binding}). It
261
375
  emits the normal `RUN_STARTED` and initial `STATE_SNAPSHOT`, then emits an authoritative
262
- `MESSAGES_SNAPSHOT` from the daemon-owned log. A durable pending interrupt is re-presented
263
- through the standard terminate-resume contract; an active Loop remains observed through its
264
- terminal; an idle conversation emits `RUN_FINISHED`. Synchronization admits no prompt,
376
+ `MESSAGES_SNAPSHOT` of the daemon-owned log ({§agui-snapshot}). What reaches the Run while its log is
377
+ read follows that snapshot, in the order it arrived, except the parts of a row the snapshot already
378
+ holds as it arrived, which are not built a second time; of a row it holds at an earlier version, only
379
+ the later facts folded onto the call it holds, a repeated `TOOL_CALL_START` and `TOOL_CALL_END`, and
380
+ its result follow. A durable pending gate addressed to the
381
+ Run's thread ({§agui-worker-owner}) is re-presented through the standard terminate-resume contract,
382
+ passive or not; an active Loop remains observed through its
383
+ terminal; an idle worker emits `RUN_FINISHED`. Synchronization admits no prompt,
265
384
  creates no turn, and performs no model inference. Disconnecting this observer never cancels
266
385
  independently-owned work.
267
386
 
387
+ §agui-snapshot **A snapshot is the messages the live projection built**, in log order, under
388
+ the live identities and with the same metadata ({§agui-plurnk-metadata}), each tool result after its call where the official reducer puts one that arrives
389
+ live, so a client that applies it keeps every message it built live and a reloaded
390
+ client rebuilds them. It is built from the bound worker's whole log, read page by page
391
+ ({§methods-log-read}), and each subagent run's under {§agui-subagents}, because AG-UI defines a snapshot as the
392
+ complete set:
393
+
394
+ | Durable row | Snapshot messages |
395
+ |---|---|
396
+ | Arrival | `user`: a message a client on this thread sent, under its message id as that client sent it; any other, identified by its coordinate and named by its source ({§message-causal-source}) |
397
+ | A turn's first model operation, carrying the turn's reasoning | `reasoning` under the identity of the request that streamed it, or `<coordinate>/reasoning` for history a FORK inherited, before that row's own messages ({§agui-readable-reasoning}) |
398
+ | SEND or KILL replying to this thread; a model WAIT that carries a body | `assistant` speech identified by the coordinate |
399
+ | Any other model operation, a bodiless WAIT and a parse refusal's attempt included ({§agui-parse-refusal}) | `assistant` with one tool call, both identified by the coordinate, then `tool` `<coordinate>/result` carrying the receipt; a call that opened an obligation carries none ({§agui-call-result}), nor does a proposal awaiting its decision ({§agui-proposal-resolve}) |
400
+ | A row that discharges an obligation | `tool` identified by its coordinate, answering the call it names: after that call and the results already beside it, or last when the call is not in view ({§agui-call-result}) |
401
+ | A reply reporting progress on a message this worker sent | `user` identified by its coordinate and named by its source ({§agui-progress-reply}) |
402
+ | Emission attempt, a parse refusal that names no attempt, ambient row | No message of its own |
403
+
404
+ Interrupts are not snapshot messages: a pending gate is re-presented as an interrupt naming the call
405
+ the snapshot holds, and a settled one's evidence is its operation's call and result. Transient reasoning of failed or rejected
406
+ requests is not retained ({§agui-readable-reasoning}).
407
+
268
408
  ## §agui-first-party-client-conformance First-party client conformance
269
409
 
270
410
  §agui-official-client-conformance The ordinary integration gate runs the official
271
- `@ag-ui/client` 0.0.59 against the real daemon and a deterministic HTTP model fixture.
411
+ `@ag-ui/client` against the real daemon and a deterministic HTTP model fixture.
272
412
  Its verifier and message builder exercise lifecycle, state, streaming reasoning,
273
- assistant speech, and provider refusals. The optional real-model test adds provider
274
- evidence; it does not substitute for the deterministic gate. Wire shapes derive
413
+ assistant speech, and provider refusals. The explicit real-model tier adds provider
414
+ evidence through the shared service live runner; it does not substitute for the
415
+ deterministic gate. Wire shapes derive
275
416
  from the installed `@ag-ui/core` schemas ({§agui-run-authority}).
417
+ Deterministic daemon fixtures use the isolated mock-tier bootstrap
418
+ ({§operator-config-real-model-profile}); operator configuration must not start
419
+ external resources during the integration gate.
420
+
421
+ An independent stock `HttpAgent` witness supplies no Plurnk forwarded properties or metadata
422
+ handling. It exercises first contact, continuation, replay, accepted-message retry, independent
423
+ thread isolation, concurrent first contact, restart and rename continuity, and standard tool-call
424
+ interrupt/resume. Unknown-thread inspection creates nothing. Qualified-address witnesses supply
425
+ only the standard `threadId` field; test-host inspection is not client protocol participation.
276
426
 
277
427
  The one-shot CLI and interactive terminal consume one AG-UI+
278
428
  semantic contract. Each surface verifies the contracts-owned conformance corpus
@@ -304,23 +454,45 @@ explicit path fails rather than falling back or skipping conformance.
304
454
 
305
455
  ## §agui-proposal-resolve Client-owned stop-the-world interactions
306
456
 
307
- Every client-owned daemon proposal—file edits and `[300]` operator questions—and
308
- every contracts-owned client interaction emits an AG-UI tool call and terminates AG-UI Run A with
309
- `RUN_FINISHED.outcome.type = "interrupt"`. The durable loop remains paused indefinitely
310
- by default; absence is not an answer. AG-UI Run B on the same thread supplies standard
311
- `RunAgentInput.resume` entries. Each entry correlates through `interruptId = toolCallId`, using
312
- `prop:<logEntryId>` for proposals and `int:<interactionId>` for client interactions. The module
457
+ A client-owned gate, a proposal its owner reviews ({§worker-owner-resolution}) or a contracts-owned
458
+ client interaction, terminates AG-UI Run A with `RUN_FINISHED.outcome.type = "interrupt"`. The
459
+ durable loop remains paused until the decision, indefinitely unless the operator sets a decision
460
+ deadline, which a proposal's interrupt carries as `expiresAt` ({§proposal-projection}); absence is
461
+ not an answer. AG-UI Run B on the same thread supplies standard `RunAgentInput.resume` entries, each
462
+ echoing the opaque `Interrupt.id` it answers.
463
+
464
+ | Gate | What Run A presents before its interrupt | The interrupt |
465
+ |---|---|---|
466
+ | A proposal of the Run's bound worker | The operation's own call, `TOOL_CALL_START/ARGS/END` as its row builds it, unanswered, unless the Run's client holds it | `toolCallId` its coordinate |
467
+ | A proposal of a worker whose subagent segment the Run has open ({§agui-subagents}) | That call in the subagent's namespace | `toolCallId` the namespaced coordinate, and `subagentRunId` |
468
+ | A proposal of any other worker the owner reviews | Nothing: the Run holds no conversation of that worker | No `toolCallId` |
469
+ | A client interaction | Its request's tool call, `int:<interactionId>`, with the request's exact `toolName` and `arguments` | `toolCallId` that call, and the request's `message` and `responseSchema` |
470
+
471
+ A proposal's interrupt carries `reason: "tool_call"`, a review `message`, the `responseSchema` of its
472
+ decision and Core's `ProposalProjection` as `metadata.plurnk.proposal` ({§proposal-projection}), what
473
+ the reviewer reviews, whether or not it names a call. The call it names is the operation itself: no other call stands for its approval, and
474
+ the decision is no result of its own. The operation's settlement answers it: the Run that holds the
475
+ call receives a repeated `TOOL_CALL_START` and `TOOL_CALL_END` under the call's id carrying the settled
476
+ facts, which the official reducer folds onto the call it holds, its arguments untouched, then the
477
+ call's result, unless the operation opened an obligation, which what discharges it answers
478
+ ({§agui-call-result}). A snapshot holds a pending proposal as its call without a result
479
+ ({§agui-snapshot}), so a client that resumed holds exactly what a refreshed one builds. Separate
480
+ presentations of one gate have separate interrupt IDs, so an observer cannot inherit another Run's
481
+ cancellation responsibility ({§agui-run-disconnect}). Unknown presentations require
482
+ rediscovery; pending proposals and interactions remain durable in Core. The module
313
483
  resolves the complete pending interrupt set for one worker, restores the thread's live projection
314
484
  state, and only then releases it. The pending Worker's persisted Worker and Loop remain the exact
315
- resolution identity. When that Worker is a descendant without its own live Run, its nearest live
316
- ancestor conversation is the controlling Run: Run A presents the descendant gate and Run B remains
317
- bound to the ancestor conversation while releasing the exact descendant. Reconnect discovers the
318
- same descendant set from Core's authoritative Worker topology. A terminal published after a
485
+ resolution identity. Owner-addressed gates route under {§agui-worker-owner}: when an owner's
486
+ thread presents another worker's gate, its resume stays bound to that thread while
487
+ releasing the exact requesting worker. Reconnect discovers pending gates from Core by recorded
488
+ owner, not inferred ancestry. A terminal published after a
319
489
  descendant gate remains ordered behind that gate's topology resolution and presentation; it cannot
320
490
  settle Run A before the interrupt is emitted. Concurrent descendant gates are
321
491
  presented one Worker at a time; resolving one re-surfaces the next without admitting a new prompt.
322
- Sibling and unrelated conversations never receive one another's gates. A resume containing foreign, partial, unknown, duplicate, or multi-worker
323
- interrupt sets fails before any stopped operation is released.
492
+ Sibling and unrelated threads never receive one another's gates. A resume containing foreign, partial, unknown, duplicate, or multi-worker
493
+ interrupt sets fails before any stopped operation is released. Mixing presentations or
494
+ threads produces `interrupt-binding-invalid`; a connection closed before validated
495
+ resolution produces `connection-closed` without releasing the stopped work.
324
496
  Loop terminals received while a resume is being validated or resolved are held:
325
497
  successful resolution delivers the bound Loop's terminal once; rejected resolution
326
498
  emits its exact `RUN_ERROR`, never success from the previously interrupted Loop.
@@ -344,19 +516,16 @@ initial state, and a new `STEP_STARTED` for that same durable turn before releas
344
516
  operation. The continued loop later closes that Run B step normally. No AG-UI Run may finish
345
517
  while a step remains active.
346
518
 
347
- Proposal tool-call arguments and resume payloads retain the proposal review contract.
348
- Resolved proposal payloads carry `decision`, optional edited `body`, and optional string
349
- `outcome`; a cancelled entry may also carry `payload.outcome`. The outcome reaches Core
519
+ A proposal's resume payload carries the review contract: a resolved one carries `decision`,
520
+ optional edited `body`, and optional string `outcome`; a cancelled entry may also carry `payload.outcome`. The outcome reaches Core
350
521
  unchanged under {§proposal-outcome-terse-error}; invalid outcome types refuse the resume
351
522
  before any proposal is released. An omitted outcome leaves Core's existing default intact.
352
- Client-interaction tool calls use the request's exact `toolName` and `arguments`; their standard
353
- interrupt carries its optional `message` and `responseSchema`. A resolved resume payload becomes
354
- the generic resolved payload, while AG-UI cancellation becomes interaction cancellation. AG-UI
523
+ An interaction's resolved resume payload becomes the generic resolved payload, while AG-UI cancellation becomes interaction cancellation. AG-UI
355
524
  never sees or reconstructs an upstream protocol's continuation state.
356
525
 
357
526
  §agui-proposal-disposition AG-UI consumes Core's `ProposalProjection` identically
358
- for live delivery and reconnect. A `review` disposition becomes a tool call and
359
- standard interrupt only for the matching owner. `accept` and `reject` remain
527
+ for live delivery and reconnect. A `review` disposition becomes a standard interrupt
528
+ only for the matching owner. `accept` and `reject` remain
360
529
  Core's automatic settlements and never become client review work. Operation,
361
530
  attrs and stale-target facts remain evidence, not authority inputs.
362
531
 
@@ -368,32 +537,34 @@ selection.
368
537
 
369
538
  ## §agui-management-plane The action surface
370
539
 
371
- PLURNK has three inputs through the one AG-UI Run endpoint. A normal user message
372
- and a standard interrupt resume are not management actions.
540
+ A Run's input ({§agui-run-input}) is one of three, and a Run that carries none of them synchronizes. A new user
541
+ message and a standard interrupt resume are not management actions.
373
542
 
374
543
  ```mermaid
375
544
  flowchart LR
376
545
  client["AG-UI client"] --> run["POST RunAgentInput"]
377
- run --> classify{"Input form"}
378
- classify -->|"textual user message"| loop["ApplicationPort.runLoop"]
546
+ run --> classify{"What the conversation doesn't hold"}
547
+ classify -->|"new user message"| loop["ApplicationPort.runLoop"]
379
548
  classify -->|"RunAgentInput.resume"| resume["ApplicationPort proposal / interaction resolution"]
380
549
  classify -->|"forwardedProps.plurnk.action"| actions{"AG-UI action registry"}
550
+ classify -->|"none of them"| sync["Synchronization"]
381
551
  actions --> builtIn["Advertised schema admission<br/>and typed ApplicationPort call"]
382
552
  actions --> extension["Scoped ApplicationPort.invokeModuleAction"]
383
553
  loop --> events["Core event source"]
384
554
  resume --> events
555
+ sync --> events
385
556
  builtIn --> events
386
557
  events --> projection["AG-UI projection"]
387
- extension --> result["plurnk.action.result"]
558
+ extension --> result["RUN_FINISHED result"]
388
559
  projection --> client
389
560
  result --> client
390
561
  ```
391
562
 
392
563
  A management-action Run carries
393
- `forwardedProps.plurnk.action = { kind, ...params }`. Once settled, it returns
394
- one `CUSTOM plurnk.action.result` shaped as `{ kind, ok, result | problem }`,
395
- followed by `RUN_FINISHED`. A proposal-gated action may first end with an
396
- interrupt and deliver that settled result on the standard resume Run. A failed
564
+ `forwardedProps.plurnk.action = { kind, ...params }`. Once settled, the Run that asked finishes with
565
+ `RUN_FINISHED` whose `result` is the outcome, shaped as `{ kind, ok, result | problem }`
566
+ ({§agui-plurnk-metadata}); no action presents an
567
+ interrupt ({§agui-client-operation}). A failed
397
568
  action preserves exact RFC 9457 Problem Details and is the result of a
398
569
  successfully transported management Run; it does not turn the Run into
399
570
  `RUN_ERROR` or invent a parallel error string. Unknown kinds return an honest
@@ -401,37 +572,66 @@ successfully transported management Run; it does not turn the Run into
401
572
 
402
573
  | Public action | Scope | Parameters | Owner / effect |
403
574
  |--------------------------|-----------|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
404
- | `ping` | Worldless | none | AG-UI-local liveness; returns `{}`. |
405
- | `discover` | Worldless | none | AG-UI-local public membership manifest ({§discovery}). |
406
- | `providers.list` | Worldless | none | `ApplicationPort.listProviders`. |
407
- | `models.list` | Worldless | `provider?`, `search?`, `availability?`, `offset?`, `limit?` | Returns one bounded {§model-catalog-wire} page from `ApplicationPort.listModels` under {§model-catalog}. |
408
- | `workspace.list` | Worldless | none | `ApplicationPort.listWorkspaces`. |
409
- | `workspace.create` | Worldless | `name?`, `projectRoot?`, `settings?` | Creates or attaches the exact named world, or asks core to create an automatically named world. |
410
- | `workspace.attach` | Worldless | `id`, `workerId?` | `ApplicationPort.attachWorkspace`; returns the selected envelope. |
575
+ | `ping` | Daemon | none | AG-UI-local liveness; returns `{}`. |
576
+ | `discover` | Daemon | none | AG-UI-local public membership manifest ({§discovery}). |
577
+ | `providers.list` | Daemon | none | `ApplicationPort.listProviders`: each alias as the route a worker seeded from it gets ({§agui-provider-routes}). |
578
+ | `models.list` | Daemon | `provider?`, `search?`, `availability?`, `offset?`, `limit?` | Returns one bounded {§model-catalog-wire} page from `ApplicationPort.listModels` under {§model-catalog}. |
579
+ | `workspace.list` | Daemon | none | `ApplicationPort.listWorkspaces`: each workspace with its folder and the folder's label ({§workspace-label}). |
580
+ | `workspace.create` | Daemon | `name?`, `projectRoot?`, `settings?` | Creates or attaches the exact named workspace, or asks core to name it ({§workspace-name}): the folder-derived default, selected when it exists, else a generated name ({§workspace-auto-name}). |
581
+ | `workspace.attach` | Daemon | exactly one of `id`, `name`, `projectRoot` | Read-only selection under {§methods-workspace-attach}; returns `{ id, name }`, creates nothing and rebinds no thread. `name` is literal; `projectRoot` selects the daemon's folder-derived default name. |
411
582
  | `workspace.workers` | Workspace | `id?` | `ApplicationPort.listWorkers`; an explicit id overrides the bound workspace. |
412
- | `log.read` | Workspace | `workerId?`, log-coordinate filters | `ApplicationPort.readLog`; defaults to the thread's conversation worker. |
413
- | `loop.inject` | Workspace | `prompt` | `ApplicationPort.runLoop` on the thread's conversation worker; folds into live work or enqueues a loop. |
414
- | `loop.cancel` | Workspace | `reason?` | `ApplicationPort.cancelDrain` on the thread's conversation worker. |
415
- | `workspace.prompts` | Workspace | `workerId?`, `limit?` | `ApplicationPort.listPrompts` scoped like `log.read`: the thread's conversation worker unless `workerId` pins another. |
583
+ | `log.read` | Workspace | `workerId?`, log-coordinate filters | `ApplicationPort.readLog`; defaults to the thread's bound worker. |
584
+ | `loop.inject` | Workspace | `prompt` | `ApplicationPort.runLoop` on the thread's bound worker; folds into live work or enqueues a loop. |
585
+ | `loop.cancel` | Workspace | `reason?` | `ApplicationPort.cancelDrain` on the thread's bound worker. |
586
+ | `workspace.prompts` | Workspace | `workerId?`, `limit?` | `ApplicationPort.listPrompts` scoped like `log.read`: the thread's bound worker unless `workerId` pins another. |
416
587
  | `workspace.rename` | Workspace | `name` | `ApplicationPort.renameWorkspace`. |
417
588
  | `workspace.share` | Workspace | `folder` | `ApplicationPort.shareWorkspace` shares the bound workspace ({§share}) into an absolute `folder` on the service host; returns `{ folder }`. |
418
- | `entry.read` | Workspace | `target`, `workerId?`, `channel?`, `offset?` | Calls `ApplicationPort.readEntry` from the explicit worker perspective or the thread conversation by default, preserving validated {§entry-read-result}. |
419
- | `op.exec` | Workspace | `command` | Constructs one execution statement on the default `sh` runtime and calls `ApplicationPort.dispatchClientAction` on the client worker, attached to the conversation Worker (`conversationWorkerId`, else the workspace's model worker) for Functionality. |
589
+ | `workspace.rollback` | Workspace | `to`, `workerId?` | `ApplicationPort.rollback` ({§rollback}) reverses the settled file mutations after log coordinate `to` of the thread's bound worker (or `workerId`), latest first, as one client operation of the thread's owner ({§agui-client-operation}); returns the `RollbackReport`. |
590
+ | `entry.read` | Workspace | `target`, `workerId?`, `channel?`, `offset?` | Calls `ApplicationPort.readEntry` from the explicit worker perspective or the thread's bound worker by default, preserving validated {§entry-read-result}. |
591
+ | `op.exec` | Workspace | `command` | Constructs one execution statement on the default `sh` runtime and dispatches it as one client operation ({§agui-client-operation}). |
420
592
  | `op.parse` | Workspace | `text` | Parses and projects PLURNK text under {§agui-op-parse}. |
421
- | `op.look` | Workspace | `text`, `workerId?` | Admits one LOOK under {§agui-op-look}, rewrites it to READ, and calls core's no-log `look` projection as the conversation worker. |
422
- | `run.fork` | Workspace | `name?` | `ApplicationPort.forkWorker` from the thread's conversation worker. |
423
- | `worker.model.get` | Workspace | none | `ApplicationPort.readWorkerModel` on the thread's conversation worker; returns `{ model, spawnModel }` as resolved specs or `null`. |
424
- | `worker.model.set` | Workspace | `selector` | `ApplicationPort.setWorkerModel` on the thread's conversation worker; persists the resolved selection and returns it. |
425
- | `worker.child.set` | Workspace | `selector` | `ApplicationPort.setWorkerSpawnModel` on the thread's conversation worker; persists the override (`null` means inherit) and returns it. |
426
- | `worker.effort.get` | Workspace | none | `ApplicationPort.readWorkerEffort` on the thread's conversation worker; returns its durable policy and the policies supported by both its model and optional spawn model. |
427
- | `worker.effort.set` | Workspace | `policy` | `ApplicationPort.setWorkerEffort` on the thread's conversation worker; validates and persists the policy between loops. |
428
- | `workspace.capabilities.get` | Workspace | none | `ApplicationPort.readWorkspaceCapabilities` on the bound workspace, without creating a conversation worker; returns the complete resolver-owned projection {§capability-policy-projection}. |
593
+ | `run.fork` | Workspace | `name?` | `ApplicationPort.forkWorker` from the thread's bound worker. |
594
+ | `worker.model.get` | Workspace | none | `ApplicationPort.readWorkerModel` on the thread's bound worker; returns `{ model, spawnModel }` as resolved specs or `null`. |
595
+ | `worker.model.set` | Workspace | `selector` | `ApplicationPort.setWorkerModel` on the thread's bound worker; persists the resolved selection and returns it. |
596
+ | `worker.child.set` | Workspace | `selector` | `ApplicationPort.setWorkerSpawnModel` on the thread's bound worker; persists the override (`null` means inherit) and returns it. |
597
+ | `worker.effort.get` | Workspace | none | `ApplicationPort.readWorkerEffort` on the thread's bound worker; returns its durable policy and the policies supported by both its model and optional spawn model. |
598
+ | `worker.effort.set` | Workspace | `policy` | `ApplicationPort.setWorkerEffort` on the thread's bound worker; validates and persists the policy between loops. |
599
+ | `workspace.capabilities.get` | Workspace | none | `ApplicationPort.readWorkspaceCapabilities` on the bound workspace, without creating a worker; returns the complete resolver-owned projection {§capability-policy-projection}. |
429
600
  | `workspace.capabilities.set` | Workspace | `policy` | `ApplicationPort.setWorkspaceCapabilities` replaces the workspace policy and returns the complete resolver-owned projection {§capability-policy-projection}. |
430
- | Registered module action | Owner-declared | owner-defined | `ApplicationPort.invokeModuleAction`; AG-UI enforces the owner's input/output schemas and passes a worldless, bound-workspace, or bound-conversation-Worker context outside supplied params. The owner retains semantic validation and the effect. |
601
+ | Registered module action | Owner-declared | owner-defined | `ApplicationPort.invokeModuleAction`; AG-UI enforces the owner's input/output schemas and passes a daemon, bound-workspace, or bound-Worker context outside supplied params. The owner retains semantic validation and the effect. |
602
+
603
+ §agui-action-binding **What a Run binds.** A message Run and an action addressed to the thread's
604
+ worker resolve the thread's worker ({§agui-thread-binding}); a workspace-level action
605
+ binds none. Opaque threads allocate only for worker-creating Runs. Qualified addresses always
606
+ require their workspace to exist. For explicit workspace selection, only a Run that may create its
607
+ worker, or binds none, may create the workspace:
608
+
609
+ | The thread's worker | The workspace | Runs and actions |
610
+ |---|---|---|
611
+ | Created when the workspace has none by that name | Created when absent | message Run; an owner's control observation ({§agui-sync}, {§agui-owner-connection}); `worker.model.set`, `worker.child.set`, `worker.effort.set` |
612
+ | Required: 404 `worker-not-found` when there is none | Attached only | passive observation ({§agui-sync}); `loop.inject`, `loop.cancel`, `run.fork`, `worker.model.get`, `worker.effort.get`, worker-scoped module actions; `log.read`, `entry.read`, `workspace.prompts` and `workspace.rollback` unless `workerId` names another worker |
613
+ | The one the interrupted Run bound, if it bound one | Attached only | interrupt resume ({§agui-proposal-resolve}) |
614
+ | Not bound | Created when absent | `workspace.workers`, `workspace.rename`, `workspace.share`, `workspace.capabilities.get`, `workspace.capabilities.set`, `op.exec`, `op.parse`, workspace-scoped module actions |
615
+ | Not bound | Attached only | `log.read`, `entry.read`, `workspace.prompts` and `workspace.rollback` naming a `workerId` |
616
+
617
+ A daemon action ({§agui-module-action-scope}), and an action the module does not register or
618
+ whose input its schema refuses, binds neither: it is answered on the control path, the refusal in
619
+ its result. A Run that binds a worker carries that worker's gauge, `plurnk.status` and `budget`, and
620
+ its deltas ({§agui-state-patches}); one that binds none carries neither. Every Run bound to a
621
+ workspace carries that workspace's state ({§agui-workspace-state}).
622
+
623
+ §agui-client-operation **`op.exec` and `op.parse` are the thread owner's client operations.** Each
624
+ dispatches one client operation ({§connection-lifecycle}) as the thread's owner address
625
+ ({§agui-worker-owner}), acting for the model worker the thread names when one exists
626
+ ({§actor-boundary-client-operations}) and never creating it. The Run binds no worker, claims nothing and
627
+ declares no capability set; it observes the operation's loop from the moment the dispatch opens it
628
+ ({§agui-broadcast-fan}), and a disconnect cancels exactly that operation ({§agui-run-disconnect}). A
629
+ person's operation is never routed for review ({§proposal-disposition}), so its Run presents no
630
+ interrupt. `workspace.rollback`'s compensations are such an operation too, without an observing Run.
431
631
 
432
632
  §agui-worker-model-actions **Worker model selection is server-backed.**
433
633
  `worker.model.get`, `worker.model.set`, and `worker.child.set` operate on the
434
- thread's bound conversation worker under {§worker-model-selection}. They are
634
+ thread's bound worker under {§worker-model-selection}. They are
435
635
  the client's durable `/model` and `/child`; a client must not reassert a
436
636
  model on every loop. The get action returns the worker's resolved durable
437
637
  model and spawn override (`null` for an unset worker or inherit); the set
@@ -449,19 +649,19 @@ loop is active or parked.
449
649
  §agui-module-action-scope **An extension action uses the same management plane,
450
650
  not a private endpoint.** `ApplicationPort.listModuleActions()` returns its exact
451
651
  `{ name, scope, inputSchema, outputSchema }` descriptors under
452
- {§agui-discovery-contract}. A worldless action follows the control path and
652
+ {§agui-discovery-contract}. A daemon action follows the control path and
453
653
  cannot establish a workspace. A workspace action follows ordinary thread
454
654
  binding, resolves the client envelope first, and invokes core with
455
655
  `{ scope: "workspace", workspaceId: envelope.workspaceId }`; an `id`,
456
656
  `workspaceId`, or similarly named owner parameter cannot override that
457
- identity. A Worker action additionally receives the thread's bound conversation
657
+ identity. A Worker action additionally receives the thread's bound
458
658
  Worker as `{ scope: "worker", workspaceId, workerId }`; client parameters cannot
459
659
  select either authority coordinate. `discover.actions` advertises all three
460
660
  kinds without importing the extension owner.
461
661
 
462
662
  ### §agui-op-parse Parsed-operation projection
463
663
 
464
- `op.parse` dispatches all trusted-prefix statements as one client action and
664
+ `op.parse` dispatches all trusted-prefix statements as one client operation ({§agui-client-operation}) and
465
665
  preserves the parser's source order in its `results` array:
466
666
 
467
667
  | Parser output | AG-UI result |
@@ -474,29 +674,6 @@ No statement at or beyond the tail boundary is dispatched. Every parse failure
474
674
  uses `stage: "parsing"` and `retryable: false`. Outside text is ignored by the
475
675
  parser under {§whitespace-contract} and produces no result.
476
676
 
477
- ### §agui-op-look Single-statement observation admission
478
-
479
- `op.look` admits a parser result only when it contains exactly one LOOK
480
- statement, no other item, and no `unparsedTail`. Outside text and whitespace
481
- produce no item under {§whitespace-contract}. The action never selects a trusted prefix or silently discards
482
- a parser fact.
483
-
484
- | Parser result | AG-UI outcome |
485
- |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
486
- | First positioned diagnostic | 400 `agui/action/parse-failed`, preserving parser detail, line, column, source, and severity |
487
- | No diagnostic, with `unparsedTail` | 400 `agui/action/parse-failed`, preserving its reason and position with `source: "grammar"` and `severity: "error"` |
488
- | Zero or multiple statements, or non-LOOK | 400 `agui/action/invalid-action-parameters` naming the observed admission fact |
489
- | Exactly one LOOK and no other parser fact | Change only `op` to READ and call `ApplicationPort.look` under {§op-look}; return its exact `OperationResult` through the action envelope |
490
-
491
- Parser failures use `stage: "parsing"`; action-shape failures use
492
- `stage: "action-validation"`. Both are non-retryable.
493
-
494
- The observation resolves `log:///` as the thread's conversation worker; `workerId`
495
- pins another workspace worker. Explicit resource authorities retain their identity
496
- under {§turn-source-resources}. The closed observation segment stays on the connection's own
497
- worker: a look never adds a loop to the conversation, so its lifecycle, packet count, and
498
- history are unchanged by inspection.
499
-
500
677
  §agui-module-actions **One public action namespace.** Core rejects empty and
501
678
  duplicate extension registrations ({§module-action-registration}). At module
502
679
  startup AG-UI rejects any extension name colliding with a built-in before it
@@ -515,7 +692,7 @@ The `discover` action returns the complete schema-bearing
515
692
  type Discovery = {
516
693
  schemaVersion: 1;
517
694
  actions: Record<string, {
518
- scope: "worldless" | "workspace" | "worker";
695
+ scope: "daemon" | "workspace" | "worker";
519
696
  inputSchema: JsonSchema;
520
697
  outputSchema: JsonSchema;
521
698
  }>;
@@ -553,16 +730,15 @@ contract.
553
730
 
554
731
  §agui-stream-producer Stream updates route to `workerId`, the initiating
555
732
  Worker. Resource reads use the canonical workspace address, not a separate
556
- read perspective. Shared resources neither broadcast to unrelated conversations
733
+ read perspective. Shared resources neither broadcast to unrelated workers
557
734
  nor hide updates from the producer ({§notifications-stream-event-on-channel-change}).
558
735
 
559
736
  `loop.inject`'s steered effect streams on the original worker's open SSE.
560
737
  `loop.cancel` is its counterpart: the addressable spelling of the SSE-hangup
561
738
  abort. An action that opens a stream remains one live AG-UI Run; its result is
562
739
  held until every observed stream emits `stream/concluded`, then the result and
563
- `RUN_FINISHED` close that Run. A client disconnect before settlement cancels
564
- the action worker with reason `client_disconnected`; it never silently converts
565
- the action into detached background work.
740
+ `RUN_FINISHED` close that Run. An executing operation Run retains its cancellation
741
+ responsibility under {§agui-run-disconnect}.
566
742
 
567
743
  ## §agui-topology-scope Topology scope
568
744
 
@@ -574,19 +750,39 @@ lineage or commons attention ({§actor-boundary}), or under the delegation
574
750
  observation the Run asked for ({§agui-delegation-observation}). AG-UI does not
575
751
  synthesize a second topology by rebroadcasting sibling rows.
576
752
 
577
- §agui-delegation-observation **A conversation Run may observe its delegation.** A
578
- conversation Run whose `forwardedProps.plurnk.descendants` is `true` also receives the
753
+ §agui-delegation-observation **A message Run may observe its delegation.** A
754
+ message Run whose `forwardedProps.plurnk.descendants` is `true` also receives the
579
755
  rows (`log/entry`) and stream events (`stream/event`, `stream/concluded`) of every Worker
580
- beneath its bound Worker in Core's authoritative topology, as foreign rows only: `CUSTOM
581
- plurnk.row` and `plurnk.ambient`, `plurnk.stream` and its activity snapshot, never the core
582
- vocabulary, a step, reasoning, or this Run's terminal. The first event of each descendant is
583
- preceded once by `CUSTOM plurnk.descendant` `{workerId, name, parentWorkerId, depth}`, depth
584
- counted from the bound Worker, so a client renders identity and generation without a second
585
- directory read. Descendants' events are ordered among themselves on the workspace's delivery
586
- chain; the bound Worker's own events keep their own order. A descendant's terminal reaches the
587
- Run as its parent's collect row, never as a terminal; a descendant's open stream never defers
588
- this Run's settlement; a sibling or unrelated Worker's events never arrive. Without the
589
- request, the Run observes its bound Worker alone.
756
+ beneath its bound Worker in Core's authoritative topology. A row of a loop that a call in view
757
+ opened is its subagent run's ({§agui-subagents}); every other descendant row is a foreign row,
758
+ `CUSTOM plurnk.ambient` ({§agui-ambient}), and every descendant stream event is `plurnk.stream`.
759
+ None enters the bound Worker's own steps or reasoning. Each descendant is introduced once with its
760
+ identity and generation `{workerId, name, parentWorkerId, depth}`, depth counted from the bound Worker,
761
+ so a client renders them without a second directory read: by the metadata of its subagent's
762
+ `SUBAGENT_STARTED` ({§agui-subagents}), or, when its first event belongs to no subagent, by a
763
+ `CUSTOM plurnk.descendant` before that event. A workspace's events reach the Run in the order the daemon published them:
764
+ while a descendant's topology or loop is being read, every later event waits for it. A
765
+ descendant's terminal never settles this Run, and its conclusion reaches the bound Worker as its
766
+ parent's collect row; a descendant's open stream never defers this Run's settlement; a sibling or
767
+ unrelated Worker's events never arrive. Without the request, the Run observes its bound Worker
768
+ alone.
769
+
770
+ §agui-subagents **An observed delegation is a subagent run of the call that opened it.** Under
771
+ {§agui-delegation-observation}, a descendant loop is a subagent of the Run when a call in view
772
+ opened it ({§methods-worker-loops}): a call of the bound Worker, or one a subagent of the Run
773
+ made. The Run judges each loop once, from the daemon's record of its opener, for its live
774
+ projection and its snapshot alike.
775
+
776
+ | Subagent run | AG-UI |
777
+ |---|---|
778
+ | Identity | `subagentRunId` is its loop's address `ops://<worker>/<loop>` ({§worker-loop-result}); `name` is its worker's name |
779
+ | Its rows | Its own worker's projection ({§agui-projection}, {§agui-snapshot}): each message and tool-call id is a coordinate in that worker's namespace, `worker://<name>#L/T/S`, and every event and message is attributed by `subagentRunId`, its `CUSTOM` metadata included, except the Run-scoped `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` and `MESSAGES_SNAPSHOT` |
780
+ | Start | `SUBAGENT_STARTED` the first time the Run presents its work, a live row or a snapshot that holds its messages, after its parent subagent's: `parentToolCallId` is the opening call's id, and `parentSubagentRunId` names the subagent that made it |
781
+ | Conclusion | Its loop's conclusion ends the segment once its step closes, in the order the daemon published it: below 400, `SUBAGENT_FINISHED` with `outcome: success` and the exact terminal result as `result`; any other, a stop included, `SUBAGENT_ERROR` with the Problem's `detail` as `message` and `type` as `code`. A loop that concluded before the Run could see it ends as it starts. |
782
+ | The Run's end | Before `RUN_FINISHED`, whether the bound Loop's terminal, an idle synchronization's or an interrupt's, each segment still open ends `SUBAGENT_FINISHED` with `outcome: suspended`: the work goes on, and a later Run presents it again. `RUN_ERROR` ends the stream as it stands. |
783
+
784
+ A snapshot is one conversation in log order, the bound Worker's messages and each subagent's, and is
785
+ followed by each subagent run it holds, presented parents first.
590
786
 
591
787
  ## §agui-broadcast-fan Event fan and AG-UI Run settlement
592
788
 
@@ -597,9 +793,8 @@ interrupt-resume Run restores its predecessor's projection and notification
597
793
  scope. A same-worker interrupt remains bound to the pending item's Worker and
598
794
  Loop. A foreign-worker interrupt preserves the owner's controlling Run binding,
599
795
  using the pending item's Worker and Loop only as its release identity. Only the
600
- controlling Run's bound Loop terminal may emit
601
- `plurnk.terminated` and close the SSE.
602
- The custom event preserves the daemon's exact universal `result`; failures use
796
+ controlling Run's bound Loop terminal may end the Run with its terminal record and close the SSE.
797
+ The terminal record preserves the daemon's exact universal `result`; failures use
603
798
  the Problem `type` as the AG-UI error code and `detail` as its message. The
604
799
  module never reconstructs a failure from a status, summary, or exception text.
605
800
  Gate routing follows {§agui-worker-owner}; topology does not confer authority.
@@ -611,8 +806,8 @@ this Run.
611
806
 
612
807
  | AG-UI Run | Core notifications admitted | Settlement owner |
613
808
  | ---------- | --------------------------- | ---------------- |
614
- | Message or interrupt-resume | Bound Worker; exact Loop once known. Workspace derivation progress may carry no Worker. Under {§agui-delegation-observation}, descendants' rows and streams as foreign rows. | Exact Loop terminal or interrupt. |
615
- | `op.exec` / `op.parse` | Client-operation Worker's log and stream events. A proposal resume retains this scope. | This action result, deferred through its owned streams. |
809
+ | Message or interrupt-resume | Bound Worker; exact Loop once known. Workspace derivation progress may carry no Worker. Under {§agui-delegation-observation}, descendants' rows and streams: a subagent's ({§agui-subagents}) or foreign. | Exact Loop terminal or interrupt. |
810
+ | `op.exec` / `op.parse` | Its operation's Loop in the runtime Worker, from the moment the dispatch opens it: that Loop's rows and notices, and the streams those rows announce. Workspace derivation progress may carry no Worker. | This action result, deferred through its owned streams; never the Loop's terminal. |
616
811
  | Every other action | None; the Run carries only its direct state snapshot and action result. | This action result. |
617
812
 
618
813
  An operation Run owns an execution from the row that announces it: a started or
@@ -621,12 +816,138 @@ proposed address opens no stream. An admitted address stays open for the Run
621
816
  until its `stream/concluded`, so the action result is deferred even when the
622
817
  command writes late or never (a `stream/event` is a race the result must not
623
818
  win). A detached execution (`<-1>`) is nobody's obligation and never defers the
624
- result.
819
+ result. The runtime Worker hosts every person's operations and the daemon's own work, so an
820
+ operation Run never owns a stream its Loop did not announce. Stream activity of that Worker
821
+ which no row of the Run's Loop has announced yet is held for it: the row that announces the
822
+ address delivers the latest progress and then any conclusion, so a stream that concluded
823
+ before its row defers nothing; activity no row of its Loop ever announces never reaches it.
625
824
 
626
825
  `workerId` (or `entry.worker_id`) is the actor owner; `loopId` (or
627
826
  `entry.loop_id`) refines it wherever the event is loop-scoped. Opening a
628
827
  read-only management Run can therefore never replay reasoning, operation rows,
629
- or turn disposition from an active conversation.
828
+ or turn disposition from an active worker.
829
+
830
+ ## §agui-oauth Authentication
831
+
832
+ AG-UI is the OAuth 2.1 authorization server of the daemon's one credential authority
833
+ ({§credential-authority}): it issues every credential the listener admits and verifies them for
834
+ `/agui` ({§agui-http-authorization}). The daemon authenticates under the shared `PLURNK_AUTH=1`
835
+ ({§operator-config-shared-keys}), the shipped posture, and never by inference from what else is
836
+ set: then the module publishes the authority's face at setup and serves the endpoints below. Under
837
+ `PLURNK_AUTH=0` it publishes nothing, serves none of them, and every resource admits anonymously.
838
+
839
+ §agui-oauth-authority **One authority, scoped by resource.** A credential carries the scopes of the
840
+ resources it may reach; `agui` names the client interface and `a2a` the hosted A2A exposure
841
+ ({§a2a-hosted-credentials}), and a mounted resource admits only a token carrying its own scope. Grants are authorization code with PKCE (`S256`, required) for public
842
+ clients, `client_credentials` for confidential clients, and `refresh_token`; no password or implicit
843
+ grant exists (RFC 9700). Nothing about a credential is interpreted by a consumer: every answer about
844
+ one comes from the authority.
845
+
846
+ §agui-oauth-endpoints **The endpoints live on the daemon's listener, under the issuer.** The issuer
847
+ is `PLURNK_AGUI_ISSUER`, or, when that is empty, the origin each request reached the daemon at
848
+ ({§http-request-origin}), answered from setup: the host has bound its listener by then ({§http-host}). The module claims and
849
+ mounts them beside `/agui` ({§module-http-mounts}):
850
+
851
+ | Path | Standard | Contract |
852
+ |---|---|---|
853
+ | `/.well-known/oauth-authorization-server` | RFC 8414 | The issuer, its endpoints, `code` as the response type, the three grant types, `S256`, the client authentication methods (`client_secret_basic`, `client_secret_post`, `none`) and the scopes. |
854
+ | `/.well-known/oauth-protected-resource` | RFC 9728 | The resource `<issuer>/agui`, this issuer as its authorization server, its scopes and `header` as the bearer method; every 401 from `/agui` names this document. |
855
+ | `GET`/`POST /oauth/authorize` | RFC 6749 §4.1, RFC 7636 | The sign-in page ({§agui-oauth-login}) and the authorization code: sixty seconds, bound to the client, the redirect, the challenge, the scopes and a family, the browser session's for a module's own client and a fresh one for any other ({§agui-oauth-session}), spent by its first presentation whatever the outcome. |
856
+ | `POST /oauth/token` | RFC 6749 §5, RFC 8707 | The grants; `resource`, when present, must be `<issuer>/agui`. Errors use the §5.2 JSON (`invalid_request`, `invalid_client` as 401 with a `Basic` challenge when Basic was used, `invalid_grant`, `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`, `invalid_target`). |
857
+ | `POST /oauth/revoke` | RFC 7009 | Revokes the family of a live token the authenticated client owns; any other token answers 200 and revokes nothing. |
858
+ | `POST /oauth/register` | RFC 7591 | Registers a public client (`token_endpoint_auth_method: none`) with absolute http(s) redirect URIs; a confidential client is declared, never registered. |
859
+ | `GET`/`POST /oauth/session` | — | The browser's standing ({§agui-oauth-session}): `GET` says whether this browser holds a session; `POST` with `action=end` revokes it and clears the cookie. |
860
+
861
+ Every response is uncacheable; OPTIONS answers 204 and the CORS headers follow {§agui-cors}. A redirect
862
+ URI must match a registered one exactly, except that a registered public client's loopback redirect
863
+ may change its port (RFC 8252 §7.3). A module's own client is answered only at the issuer followed
864
+ by a path it registered ({§credential-module-client}), with no exception: under a configured issuer
865
+ no other origin is its address. A request naming an unregistered client or redirect is answered to the person,
866
+ never redirected; any other malformed request is redirected with its `error` and `state`.
867
+
868
+ §agui-oauth-clients **Four kinds of client.** The operator's own client, `operator`, is confidential:
869
+ its secret is the authority's operator secret ({§credential-authority}), the shared `PLURNK_SECRET`
870
+ when set and otherwise the one the daemon mints at boot into the file beside its database, and it
871
+ holds every scope; the terminal client, scripts and the bench exchange it for access tokens with
872
+ `client_credentials`. An authority without an operator secret has no `operator` client, and the
873
+ operator's tools have no way in but the sign-in. A declared client,
874
+ `PLURNK_AGUI_CLIENT_<id>={"secret":"…","scopes":[…]}`, is confidential too and holds only the scopes
875
+ declared; its tokens name it as their subject, and removing its line and restarting revokes them. A
876
+ registered client is public and holds nothing to keep: it authenticates by its id alone, uses only
877
+ the code grant with PKCE and refresh, and its tokens name the operator who signed in. A public client
878
+ may not use `client_credentials` (`unauthorized_client`). A module's own client is the one a module
879
+ of this daemon registered on the authority ({§credential-module-client}): confidential, as RFC 10017
880
+ §6.2.3.1 requires of a backend that holds tokens for a page, authenticated by `client_secret_basic`
881
+ or `client_secret_post` with the secret the authority minted and checks, using the code grant with
882
+ PKCE and refresh, its tokens naming the operator who authorized it. It may not use
883
+ `client_credentials` (`unauthorized_client`): it stands for the operator, never for itself. A scope
884
+ it registered that this server does not issue is the module's fault and fails the request as a
885
+ server error, never as a narrowed grant; a declared client may not take a module client's id.
886
+
887
+ §agui-oauth-session **A browser is the operator only once it signs in.**
888
+ The browser's standing is a `session` credential of the authority ({§credential-authority}),
889
+ presented as the cookie `plurnk_session` (`HttpOnly`, `SameSite=Lax`, `Path=/oauth`, `Secure` under
890
+ an https issuer), living as long as a refresh token and revoked by `POST /oauth/session` with
891
+ `action=end`, which also clears the cookie; a session is never a bearer. Signing in with
892
+ `PLURNK_PASS` ({§agui-oauth-login}) is the only way a session is established, wherever the browser
893
+ runs: while it is empty, every browser is told so, and where to set it, at 503 and no session
894
+ exists. A module's own client ({§agui-oauth-clients}) is this daemon's by
895
+ registration, and the operator's session authorizes it silently; having no standing apart from that
896
+ sign-in, its grant joins the session's family, so revoking the family from either side, by
897
+ `/oauth/session` or by the module's own sign-out, ends the browser's standing here and the module's
898
+ credentials together, and a code not yet exchanged ends with them; the next visit after a sign-out
899
+ meets the sign-in form. Any other client, wherever its
900
+ redirect points, is shown a consent page naming it and its scopes, and only a `POST` carrying the session that preceded the
901
+ request issues the code (`consent=allow`) or declines it (`consent=deny`, `access_denied` to the
902
+ redirect), so a page elsewhere, whose cross-site POST carries no cookie and whose top-level GET
903
+ cannot click, obtains nothing. Every request under the authorization server's paths must name this
904
+ daemon: a `Host` that is neither loopback nor the issuer's host is refused at 421 before any route,
905
+ so a name a page rebinds to the loopback address reaches nothing. Every page it serves carries
906
+ `frame-ancestors 'none'`.
907
+
908
+ §agui-oauth-documents **The authorization server's documents are plain semantic HTML.** Every page it
909
+ shows a person (sign-in, a refused password, too many attempts, no password, consent, a refused
910
+ request, and the session page) is HTML with no script and no class: a `main` holding one `article`
911
+ whose `header` carries the `h1`, paragraphs, and native form controls each named by its `label`; a
912
+ page that reports a fault puts a warning glyph (`role="img"`, `aria-label="Warning"`) in its heading,
913
+ and a refused password is an alert (`role="alert"`). A page carries its own small inline style,
914
+ readable alone in either color scheme. On a request whose `client_id` names a module client that
915
+ registered a stylesheet ({§credential-module-client}), the page links that stylesheet in place of its
916
+ own style, and its policy admits same-origin styles only (`style-src 'self'`); the session page takes
917
+ an optional `client_id` for the same purpose and carries it through its sign-out. A request can name a
918
+ registered client and nothing else: no page links a stylesheet a request supplies, and a client that
919
+ is unknown, not a module's or registered none gets the standalone page.
920
+ `authorizationDocumentSpecimens()` renders every one of these documents, for a consumer whose
921
+ production CSS pruning must keep the selectors they use.
922
+
923
+ §agui-oauth-login **The operator signs in at the authorization endpoint.** The sign-in page is plain
924
+ HTML with no script: it names the client and the scopes it asks for and posts the operator's
925
+ shared password, `PLURNK_PASS`, compared in constant time; a wrong password re-presents the form at
926
+ 401; the right one establishes the browser's session ({§agui-oauth-session}) and redirects with the
927
+ code and the `state`. There is one local operator: the password selects no account, and the
928
+ session's subject is the operator's whatever the password is, so changing it changes who can sign
929
+ in, never who they are or what they own. `PLURNK_PASS` ships empty: while it is empty, every
930
+ browser is told so, and the operator's configuration file to set it in, at 503 and issued nothing,
931
+ and client credentials are unaffected. The daemon does not throttle failed attempts: limiting
932
+ requests by their real source is the job of a front such as the example nginx configuration
933
+ ({§nginx-example}), which sees addresses a proxied daemon cannot.
934
+
935
+ §agui-oauth-tokens **Credentials are opaque and bounded.** An access token lives
936
+ `PLURNK_AGUI_ACCESS_TTL_SEC`, a refresh token `PLURNK_AGUI_REFRESH_TTL_SEC`; both are random strings the
937
+ authority holds in memory, so a daemon restart revokes every credential and a client simply
938
+ acquires again. A refresh token rotates: each use consumes it and answers a new access token and a
939
+ new refresh token of the same family, with the same or a narrower scope; a consumed refresh token
940
+ presented again is reuse, and reuse revokes the whole family (RFC 9700 §4.14). A code grant and a
941
+ refresh grant answer a refresh token; `client_credentials` answers an access token alone, renewed
942
+ with the secret.
943
+
944
+ §agui-oauth-revocation **Revocation reaches what is open.** Revoking a family, by the revocation
945
+ endpoint, by refresh-token reuse, by a code presented twice or by ending a browser's session, which
946
+ for a module's own client is the same family ({§agui-oauth-session}), ends every Run admitted under that
947
+ family with the 401 `credential-revoked` Problem and `RUN_ERROR`, exactly as shutdown ends a Run
948
+ ({§agui-shutdown}); the loops those Runs observed continue, because a revoked observer is not a
949
+ cancelling one ({§agui-run-disconnect}). Expiry is checked at admission only: an admitted stream
950
+ outlives its token's expiry and is never cut by it.
630
951
 
631
952
  ## §agui-configuration Module configuration
632
953
 
@@ -639,28 +960,51 @@ address is the daemon's.
639
960
  | Input | Owner | Empty or absent | Accepted value | Effect |
640
961
  | ---------------------------------- | ------- | -------------------------------------- | ----------------------------------- | ------ |
641
962
  | `PLURNK_HOST` / `PLURNK_PORT` | Service | Invalid at service boot | Service-valid host and port | The daemon's one listener ({§http-host}); AG-UI reads neither. |
642
- | `PLURNK_AGUI_TOKEN` | AG-UI | No module-level bearer requirement | Any string | A non-empty value requires the exact bearer on every non-preflight request. |
963
+ | `PLURNK_AUTH` | Shared | Invalid; the shared panel is required | `1` or `0` | The daemon's posture ({§credential-authority}, {§operator-config-shared-keys}): authenticate, or admit anonymously. |
964
+ | `PLURNK_SECRET` | Shared | The daemon mints one at boot | Any non-empty string | The operator's client secret ({§agui-oauth-clients}), what the operator's own tools exchange for access tokens. |
965
+ | `PLURNK_AGUI_ISSUER` | AG-UI | The origin each request reached ({§http-request-origin}) | An absolute http(s) origin | The authorization server's issuer, the public origin clients reach ({§agui-oauth-endpoints}). |
966
+ | `PLURNK_PASS` | Shared | Empty: no browser signs in until it is set ({§agui-oauth-session}) | Any string | The operator's browser sign-in password ({§agui-oauth-login}). |
967
+ | `PLURNK_AGUI_ACCESS_TTL_SEC` / `PLURNK_AGUI_REFRESH_TTL_SEC` | AG-UI | Invalid; the package floor is required | Positive safe integers | Credential lifetimes ({§agui-oauth-tokens}). |
968
+ | `PLURNK_AGUI_CLIENT_<id>` | AG-UI | No declared clients | `{"secret": "…", "scopes": ["agui"]}` | One confidential client per line ({§agui-oauth-clients}); `operator` is reserved. |
643
969
  | `PLURNK_AGUI_ALLOW_ORIGIN` | AG-UI | Empty: no CORS headers; absent: invalid | `*` or one origin | The origin whose pages a browser lets read replies ({§agui-cors}). |
644
970
  | `PLURNK_AGUI_MAX_TURNS` | AG-UI | No module-level default | `-1` or a non-negative safe integer | Supplies `maxTurns` only when the Run does not carry its own value. |
645
971
  | `PLURNK_AGUI_HEARTBEAT_MS` | AG-UI | Invalid; the package floor is required | Integer `0` through `2147483647` | SSE comment-frame cadence in milliseconds; `0` disables it. |
646
972
 
647
973
  ## §agui-run-endpoint The AG-UI Run endpoint
648
974
 
649
- `POST /agui` accepts a schema-valid AG-UI `RunAgentInput`: the last textual
650
- `user` message becomes the
651
- `ApplicationPort.runLoop` prompt (`maxTurns` from
652
- the forwarded PLURNK properties); the response is `text/event-stream`,
653
- one `data:` line per event, ending after `RUN_FINISHED`/`RUN_ERROR`. Multimodal user content
654
- is rejected until the model-loop seam supports it deliberately. Proposal
655
- disposition remains Core policy; this module routes review to the worker's owner.
975
+ `POST /agui` accepts a schema-valid AG-UI `RunAgentInput`; the response is `text/event-stream`, one `data:` line
976
+ per event, ending after `RUN_FINISHED`/`RUN_ERROR`. Proposal disposition remains Core policy; this module routes
977
+ review to the worker's owner.
978
+
979
+ §agui-run-input **A Run's input is what its conversation doesn't hold.** A Run's `messages` are the client's copy of
980
+ the conversation its thread names, which the official `HttpAgent` sends whole on every Run. Its last message is new
981
+ when it is a user message of that conversation (no `subagentRunId`) that is neither one the daemon presented, its id a
982
+ coordinate the worker's log holds ({§agui-snapshot}), nor one a client already sent: its address is an idempotency key
983
+ core decides ({§message-idempotency}), so the same words attach to what that message started. Every other message is
984
+ the client's copy, never imported ({§agui-replay}). Each check is one keyed read. The Run is decided before it binds
985
+ anything, and a refused Run binds, creates and claims nothing:
986
+
987
+ | The Run carries | It |
988
+ |---|---|
989
+ | An interrupt resume | Resolves its interrupts ({§agui-proposal-resolve}) |
990
+ | `forwardedProps.plurnk.action` | Acts ({§agui-management-plane}) |
991
+ | A new message | Submits it: its text is the `ApplicationPort.runLoop` prompt, `maxTurns` from the forwarded PLURNK properties |
992
+ | None of them | Synchronizes ({§agui-sync}) |
993
+ | More than one of them | Is refused, 400 `ambiguous-run-input` |
994
+
995
+ A new message that is not non-empty text is refused, 400 `user-message-text-required`: multimodal content waits until
996
+ the model-loop seam supports it deliberately. A client's address that core holds with other words is core's refusal,
997
+ 422 `message-address-reused`. A connection submits nothing ({§agui-owner-connection}).
656
998
 
657
999
  §agui-run-source The run's user message is the causal actor behind its inbound SEND
658
1000
  ({§message-causal-source}). The module supplies its canonical address as the loop's `source`:
659
1001
  `agui://<principal>/threads/<threadId>/messages/<messageId>`, every segment
660
- URI-encoded, the principal `anonymous` until the authorization layer names one. The frontend's
1002
+ URI-encoded, the principal being the admitted credential's subject ({§agui-http-authorization}):
1003
+ `operator` for the operator's own credentials, a declared client's id for its own, and `anonymous`
1004
+ when the listener authenticates nothing. The frontend's
661
1005
  self-identification never enters the address ({§client-metadata}). The arrival row renders that
662
1006
  source, so a model tells an operator's message from a worker's or a schedule's by address
663
- rather than by absence. Message identity belongs to the conversation, not a transport run;
1007
+ rather than by absence. Message identity belongs to the thread, not a transport run;
664
1008
  the run ID remains envelope provenance. Accepted text remains available through ordinary
665
1009
  READ, FIND and COPY after log curation, run conclusion, disconnect and daemon restart.
666
1010
  `loop.inject` normalizes its prompt into a user message with a server-generated message ID
@@ -670,32 +1014,39 @@ SEND to this exact address answers this message under {§send-response-receipt};
670
1014
  not create another inbound request. Standard AG-UI assistant messages and history replay
671
1015
  carry the reply, independently of whether the loop can yet conclude.
672
1016
 
673
- §agui-http-authorization When `PLURNK_AGUI_TOKEN` is non-empty, every
674
- non-preflight request must carry that exact value as an
675
- `authorization: Bearer <token>` header. Authorization precedes request-body
676
- reading. A missing or mismatched credential returns the stable 401
677
- `bearer-token-required` Problem. A fresh install **mints its own** into the
678
- operator's file ({§operator-config-discovery}), which every client reads
679
- through the same cascade, so the shipped posture is authenticated: the empty
680
- value that means *no check* is an operator's deliberate choice, never a
681
- default. This is the perimeter because a daemon is one trust domain and on
682
- loopback the operator's browser is inside it — any page they visit can reach
683
- the port, and only the bearer tells the two apart.
1017
+ §agui-http-authorization **`/agui` is a protected resource of the daemon's authority.** When the
1018
+ daemon authenticates ({§agui-oauth}), every non-preflight request carries an access token the daemon
1019
+ issued as `authorization: Bearer <token>`, and admission precedes request-body reading. The principal of the admitted request is the credential's subject ({§agui-run-source}).
1020
+ Each refusal is an RFC 6750 challenge, a `WWW-Authenticate: Bearer` header naming the realm and the
1021
+ RFC 9728 resource metadata, beside its Problem: no bearer at all is 401 `bearer-token-required`; a
1022
+ token that is unknown, expired, consumed, revoked or not an access token is 401
1023
+ `bearer-token-invalid` with `error="invalid_token"`; an access token without the scope `agui` is 403
1024
+ `bearer-scope-insufficient` with `error="insufficient_scope"`. The shipped posture is authenticated
1025
+ with nothing configured: the daemon **mints its operator secret at every boot** into the file beside
1026
+ its database ({§credential-authority}), which the operator's own tools read, and the seeded operator
1027
+ file ({§operator-config-discovery}) teaches the keys without setting one. A daemon under
1028
+ `PLURNK_AUTH=0` serves no authorization server and admits every request as `anonymous`: that posture
1029
+ is the operator's deliberate configuration, never a default. This is the perimeter because a daemon is one trust
1030
+ domain and on loopback the operator's browser is inside it — any page they visit can reach the
1031
+ port, and only the credential tells the two apart. The secret itself is never a bearer: presented
1032
+ directly, it is an unknown token.
684
1033
 
685
1034
  §agui-cors Every response carries `access-control-allow-origin` with the value
686
1035
  of `PLURNK_AGUI_ALLOW_ORIGIN`, and allows the `content-type` and
687
1036
  `authorization` request headers; an empty value sends neither header, so no
688
- browser page can read a reply. CORS narrows which pages may *read*; it does
1037
+ page of another origin can read a reply, while a page the daemon itself serves
1038
+ reads its own origin's replies as any same-origin page does. CORS narrows which
1039
+ other origins' pages may *read*; it does
689
1040
  not authenticate a request and is no substitute for the bearer
690
1041
  ({§agui-http-authorization}). A preflight is answered `204` before
691
1042
  authorization.
692
1043
 
693
1044
  §agui-http-failure Failures before SSE headers are sent use
694
1045
  `application/problem+json` with exact
695
- RFC 9457 Problem Details. Once SSE has opened, `CUSTOM plurnk.problem` preserves
696
- that same object and `RUN_ERROR` maps it to the standard AG-UI fields. The custom
697
- event and `plurnk.terminated.result.problem` are lossless; `RUN_ERROR.code` and
698
- `message` are the required standard projection, not a second failure object.
1046
+ RFC 9457 Problem Details. Once SSE has opened, `RUN_ERROR` maps that same object to the standard AG-UI fields
1047
+ and carries it whole as its metadata ({§agui-plurnk-metadata}). That metadata and the terminal record's
1048
+ `result.problem` are lossless; `RUN_ERROR.code` and `message` are the required standard projection, not a second
1049
+ failure object.
699
1050
  Consumers preserve the exact Problem from the lossless surface and never
700
1051
  reconstruct one from `RUN_ERROR`.
701
1052
 
@@ -704,22 +1055,31 @@ reconstruct one from `RUN_ERROR`.
704
1055
  | code | status | contract |
705
1056
  |---|---:|---|
706
1057
  | `parse-failed` | 400 | A client-authored block did not parse; the detail is the parser's own diagnostic, verbatim, with its line, column and source ({§agui-outside-text}). |
1058
+ | `bearer-token-required` | 401 | The request did not provide a bearer token. Recovery: Present an access token issued by this daemon as `Authorization: Bearer <token>`. |
1059
+ | `bearer-token-invalid` | 401 | The access token is unknown, expired or revoked. Recovery: Obtain a new access token from this daemon's token endpoint. |
1060
+ | `bearer-scope-insufficient` | 403 | The access token does not carry the scope *scope*. Recovery: Obtain an access token with the scope *scope*. |
1061
+ | `credential-revoked` | 401 | The credential this stream was admitted under has been revoked. |
707
1062
  | `service-stopping` | 503 | The service is shutting down. New requests are refused while existing runs retain terminal-event delivery until observer closure ({§module-shutdown-order}). |
708
1063
  | `route-not-found` | 404 | The requested HTTP route does not exist. |
709
1064
  | `request-failed` | 500 | The AG-UI request failed unexpectedly. |
710
1065
  | `workspace-required` | 400 | forwardedProps.plurnk.workspace must name a workspace. Recovery: Provide a non-empty workspace name. |
1066
+ | `thread-not-found` | 404 | The thread has no conversation. |
1067
+ | `thread-address-invalid` | 400 | The qualified thread address is not `<workspace ID>#<worker name>`. |
711
1068
  | `interrupt-duplicate` | 400 | The resume contains the same interrupt more than once. Recovery: Include each pending interrupt exactly once. |
712
1069
  | `interrupt-invalid` | 400 | The resume contains an invalid client interrupt. Recovery: Resume with the interrupt IDs and response shapes supplied by the pending tool calls. |
713
- | `interrupt-not-pending` | 409 | The resume addresses an interrupt that is not pending. Recovery: Refresh pending interrupts before resuming. |
1070
+ | `interrupt-not-pending` | 409 | The resume addresses an interrupt that is not pending, or an unknown interrupt presentation. Recovery: Refresh pending interrupts before resuming. |
1071
+ | `interrupt-binding-invalid` | 400 | The resume combines interrupts from different Runs, or does not match its interrupt's binding. |
1072
+ | `connection-closed` | 499 | The connection closed before the interrupt was resolved. |
714
1073
  | `worker-scope-invalid` | 400 | One resume must address interrupts for exactly one worker. Recovery: Resume each worker separately. |
715
1074
  | `interrupt-set-incomplete` | 409 | The resume does not address every pending interrupt for worker *id*. Recovery: Resolve every pending interrupt for this worker in one resume. |
716
1075
  | `invalid-json` | 400 | The request body is not valid JSON. |
717
1076
  | `invalid-run-input` | 400 | The request body does not satisfy the AG-UI RunAgentInput contract. |
718
1077
  | `control-invalid` | 400 | control must be a boolean. |
719
1078
  | `interactive-invalid` | 400 | interactive must be a boolean. |
720
- | `unsupported-run-mode` | 400 | forwardedProps.plurnk.mode must be "sync" when present. |
721
- | `invalid-sync-input` | 400 | Conversation synchronization cannot include messages, an action, or an interrupt resume. |
722
- | `user-message-required` | 400 | A new AG-UI Run requires a non-empty textual user message. Recovery: Provide a non-empty user message. |
1079
+ | `invalid-connect-input` | 400 | A connection cannot include a new message, an action, or an interrupt resume. |
1080
+ | `ambiguous-run-input` | 400 | The Run carries more than one of a new message, an action, and an interrupt resume. Recovery: Send each in its own Run. |
1081
+ | `worker-not-found` | 404 | No model worker named *name* exists in workspace *workspace*. Recovery: Name an existing model worker, or create one by sending it a message. |
1082
+ | `user-message-text-required` | 400 | A new user message must be non-empty text. Recovery: Send the message as non-empty text. |
723
1083
  | `run-failed` | 500 | The AG-UI Run failed unexpectedly. |
724
1084
 
725
1085
  §pinned-wording-agui **Pinned wording.** Verbatim sentences tests pin: each is contract, and a change here is a change of contract.
@@ -728,5 +1088,4 @@ reconstruct one from `RUN_ERROR`.
728
1088
  |---|---|
729
1089
  | Action '*name*' is not registered. Recovery: Use an action advertised by discover. | an action call naming no registered action |
730
1090
  | Action '*name*' rejected parameters. *schema path*: *reason*. | an action call whose parameters fail the action's schema |
731
- | *action* parsed *n* statements; exactly one *OP* statement is required. | a client op action whose block holds other than one statement |
732
1091
  | The action failed unexpectedly. | an action handler that threw |