@plurnk/plurnk-agui 3.1.0 → 4.0.1
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.
- package/.env.defaults +17 -8
- package/README.md +33 -13
- package/SPEC.md +589 -268
- package/dist/AguiPlus.d.ts +24 -29
- package/dist/AguiPlus.d.ts.map +1 -1
- package/dist/AguiPlus.js +25 -36
- package/dist/AguiPlus.js.map +1 -1
- package/dist/AguiSurface.d.ts +1 -2
- package/dist/AguiSurface.d.ts.map +1 -1
- package/dist/AguiSurface.js +23 -16
- package/dist/AguiSurface.js.map +1 -1
- package/dist/AuthorizationDocuments.d.ts +33 -0
- package/dist/AuthorizationDocuments.d.ts.map +1 -0
- package/dist/AuthorizationDocuments.js +84 -0
- package/dist/AuthorizationDocuments.js.map +1 -0
- package/dist/BuiltinActions.d.ts +11 -5
- package/dist/BuiltinActions.d.ts.map +1 -1
- package/dist/BuiltinActions.js +73 -125
- package/dist/BuiltinActions.js.map +1 -1
- package/dist/EventRouter.d.ts +11 -3
- package/dist/EventRouter.d.ts.map +1 -1
- package/dist/EventRouter.js +17 -32
- package/dist/EventRouter.js.map +1 -1
- package/dist/MessageAddress.d.ts +7 -5
- package/dist/MessageAddress.d.ts.map +1 -1
- package/dist/MessageAddress.js +14 -12
- package/dist/MessageAddress.js.map +1 -1
- package/dist/Module.d.ts +5 -4
- package/dist/Module.d.ts.map +1 -1
- package/dist/Module.js +221 -82
- package/dist/Module.js.map +1 -1
- package/dist/OAuthServer.d.ts +30 -0
- package/dist/OAuthServer.d.ts.map +1 -0
- package/dist/OAuthServer.js +607 -0
- package/dist/OAuthServer.js.map +1 -0
- package/dist/Portal.d.ts +39 -9
- package/dist/Portal.d.ts.map +1 -1
- package/dist/Portal.js +414 -141
- package/dist/Portal.js.map +1 -1
- package/dist/ProposalHitl.d.ts +17 -4
- package/dist/ProposalHitl.d.ts.map +1 -1
- package/dist/ProposalHitl.js +30 -43
- package/dist/ProposalHitl.js.map +1 -1
- package/dist/RunHandler.d.ts +18 -8
- package/dist/RunHandler.d.ts.map +1 -1
- package/dist/RunHandler.js +214 -96
- package/dist/RunHandler.js.map +1 -1
- package/dist/ThreadAddress.d.ts +8 -0
- package/dist/ThreadAddress.d.ts.map +1 -0
- package/dist/ThreadAddress.js +18 -0
- package/dist/ThreadAddress.js.map +1 -0
- package/dist/Translator.d.ts +52 -2
- package/dist/Translator.d.ts.map +1 -1
- package/dist/Translator.js +390 -117
- package/dist/Translator.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +22 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +65 -13
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/run-events.d.ts +1 -0
- package/dist/run-events.d.ts.map +1 -1
- package/dist/run-events.js +4 -2
- package/dist/run-events.js.map +1 -1
- package/dist/types.d.ts +4 -22
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -9
package/SPEC.md
CHANGED
|
@@ -13,78 +13,114 @@ 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 **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`threadId` names
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
a workspace name that does not exist is the
|
|
53
|
-
`workspace-not-found` with nothing created — never a second
|
|
54
|
-
|
|
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
|
|
66
|
-
workspace.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
|
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
|
|
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.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
the
|
|
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
|
|
|
90
126
|
## §agui-run-disconnect Run lifetime
|
|
@@ -93,8 +129,9 @@ Approval ownership does not confer responsibility for cancelling running work.
|
|
|
93
129
|
|
|
94
130
|
| Connection | Unexpected disconnect |
|
|
95
131
|
|---|---|
|
|
96
|
-
| Message Run
|
|
97
|
-
| Resume of
|
|
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. |
|
|
98
135
|
| Observation, including approval of independently started work | Detach; do not cancel work. |
|
|
99
136
|
| Other management Run | Detach. |
|
|
100
137
|
|
|
@@ -107,17 +144,21 @@ cancellation responsibility.
|
|
|
107
144
|
|
|
108
145
|
## §agui-owner-connection Persistent observation
|
|
109
146
|
|
|
110
|
-
`POST /agui/connect` accepts
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
113
153
|
child gates, is delivered without client polling or fabricated prompts. A terminal or interrupt
|
|
114
154
|
ends that observation Run; the client reconnects after handling it. Detaching never cancels work.
|
|
115
|
-
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.
|
|
116
157
|
|
|
117
158
|
§agui-shutdown **Shutdown releases every remaining Run.** Module `stop` refuses new
|
|
118
159
|
requests while admitted Runs retain terminal-event delivery. In `close`, after core
|
|
119
160
|
settlement ({§module-shutdown-order}), each still-open Run receives `service-stopping`
|
|
120
|
-
as
|
|
161
|
+
as a `RUN_ERROR` carrying that Problem ({§agui-plurnk-metadata}), then its response and heartbeat end. This
|
|
121
162
|
ends the transport Run, not a worker obligation. Already-ended Runs receive nothing
|
|
122
163
|
further; repeated closure is inert. Preparation finishing after closure cannot open
|
|
123
164
|
a new stream. Clients need not disconnect for the daemon to stop.
|
|
@@ -129,24 +170,29 @@ One accepted Run or daemon notification produces zero-or-more AG-UI events:
|
|
|
129
170
|
| plurnk wire | AG-UI events |
|
|
130
171
|
| ------------------------------------------ | ------------ |
|
|
131
172
|
| schema-valid `RunAgentInput` | `RUN_STARTED` + initial `STATE_SNAPSHOT` |
|
|
132
|
-
|
|
|
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} |
|
|
133
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. |
|
|
134
|
-
| `log/entry` WAIT (model) | A WAIT that carries a body speaks it: `TEXT_MESSAGE_START/CONTENT/END` with the body as written,
|
|
135
|
-
| Successful SEND replying to this
|
|
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}) |
|
|
136
177
|
| Other directed or unsuccessful SEND | Ordinary tool-call operation events; never assistant speech |
|
|
137
|
-
| `log/entry` other op (model) | `TOOL_CALL_START/ARGS/END`
|
|
138
|
-
| `log/entry`
|
|
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}) |
|
|
139
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}. |
|
|
140
|
-
| `log/entry`
|
|
141
|
-
|
|
|
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 |
|
|
142
188
|
| `loop/packet` | `STATE_DELTA` replacing the bound thread's loop id, lifecycle, and exact packet count |
|
|
143
|
-
| `loop/terminated`
|
|
144
|
-
| transport failure after SSE opens | `
|
|
145
|
-
| `notice/event` | `CUSTOM plurnk.notice`; routine derivation lifecycle uses only `STATE /plurnk/
|
|
146
|
-
| `workspace/preparation` | `STATE_DELTA /plurnk/
|
|
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. |
|
|
147
193
|
| `reasoning/event` | Standard live `REASONING_START` → `REASONING_MESSAGE_START` → one or more `REASONING_MESSAGE_CONTENT` → `REASONING_MESSAGE_END` → `REASONING_END` {§agui-readable-reasoning} |
|
|
148
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. |
|
|
149
|
-
| `stream/event` + `stream/concluded` | `CUSTOM plurnk.stream`
|
|
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. |
|
|
150
196
|
|
|
151
197
|
§agui-lifecycle-projection **Lifecycle, memory and speech remain distinct.** Live
|
|
152
198
|
events, replay and `log.read` preserve literal native bodies. No task inventory
|
|
@@ -155,10 +201,10 @@ or ACP plan is inferred from prose.
|
|
|
155
201
|
| Projection | Standard representation |
|
|
156
202
|
| ---------- | ----------------------- |
|
|
157
203
|
| NOTE | Ordinary tool-call operation and durable row, not assistant speech or a substitute reasoning stream |
|
|
158
|
-
| WAIT |
|
|
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 |
|
|
159
205
|
| SEND response | Assistant message through {§loop-response-messages}; delivery and completion remain independent |
|
|
160
206
|
| Completion | Standard run lifecycle without an invented message |
|
|
161
|
-
| Replay |
|
|
207
|
+
| Replay | The live projection's messages, in log order {§agui-snapshot} |
|
|
162
208
|
|
|
163
209
|
§agui-readable-reasoning **Readable provider reasoning uses AG-UI's standard
|
|
164
210
|
reasoning channel.** A core `{§notifications-reasoning-event}` for the thread's
|
|
@@ -166,60 +212,96 @@ model worker projects each exact delta immediately through a balanced standard
|
|
|
166
212
|
reasoning lifecycle identified by its durable model-call id and physical-request
|
|
167
213
|
sequence. A retried request gets a new reasoning message instead of appending to
|
|
168
214
|
the failed request's partial reasoning. Foreign-worker and BARE reasoning never
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
177
228
|
AG-UI Runs; the Run B projection inherits Run A's delivered-reasoning evidence
|
|
178
|
-
before the stopped operation is released.
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
181
232
|
reasoning ({§agui-lifecycle-projection}), and a response row still precedes its text
|
|
182
233
|
sequence.
|
|
183
234
|
|
|
184
|
-
- **An op row IS a tool call** — its
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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}).
|
|
188
241
|
- §agui-encrypted-reasoning **The encrypted-reasoning interface is not implemented.**
|
|
189
242
|
AG-UI defines encrypted-reasoning events and message values; plurnk emits none and
|
|
190
243
|
stores none ({§provider-open-reasoning}). Implementing a transport for bytes the
|
|
191
244
|
service cannot read would add surface without adding capability. Readable reasoning
|
|
192
|
-
streams and
|
|
245
|
+
streams and snapshot replay remain {§agui-readable-reasoning}.
|
|
193
246
|
- §agui-custom-namespace **The custom namespace** — plurnk-specific metadata rides
|
|
194
|
-
`CUSTOM` events named `plurnk.*` (`plurnk.
|
|
195
|
-
`plurnk.
|
|
196
|
-
|
|
197
|
-
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 }`.
|
|
198
266
|
|
|
199
267
|
§agui-status-children **Children are counted by the daemon, never inferred.** `status.children`
|
|
200
268
|
is the bound Worker's alive direct children — `queued`, `running`, or `parked` under
|
|
201
269
|
{§loop-lifecycle-vocabulary}; a parked child still owes a result and is exactly the one a user
|
|
202
270
|
hops to. The module computes it from `ApplicationPort.listWorkers({ parentWorkerId })` for every
|
|
203
|
-
snapshot and whole-gauge replacement, and
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
206
277
|
indicator and never poll the directory to keep it honest. Topology is navigation: the client
|
|
207
278
|
hops to a child to speak to it; watching a child's rows is the delegation observation a Run
|
|
208
279
|
asks for ({§agui-delegation-observation}).
|
|
209
280
|
|
|
210
|
-
§agui-status-
|
|
211
|
-
`status.
|
|
212
|
-
projection over the bound Worker
|
|
213
|
-
({§methods-worker-
|
|
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
|
|
214
285
|
requests reads as nothing spent rather than unknown, and all totals and known subtotals pass through
|
|
215
|
-
verbatim ({§agui-numbers-passthrough}).
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
promoted ({§agui-delegation-observation});
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
223
305
|
|
|
224
306
|
§agui-numbers-passthrough **Gauge numbers pass through verbatim.** The module
|
|
225
307
|
never recomputes the daemon's gauge or promotes accounting into application
|
|
@@ -227,70 +309,102 @@ state paths that could be mistaken for standard AG-UI fields.
|
|
|
227
309
|
|
|
228
310
|
§agui-state-patches Each Run's `STATE_SNAPSHOT` precedes its `STATE_DELTA`s.
|
|
229
311
|
Deltas use RFC 6902 `replace` on existing snapshot paths; replay yields the
|
|
230
|
-
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}).
|
|
231
314
|
|
|
232
315
|
| AG-UI state location | Daemon source | Meaning |
|
|
233
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. |
|
|
234
318
|
| `snapshot.plurnk.providers[*].inputCapacity` | `providers.list.aliases[*]` | Each provider alias's derived physical input capacity, or `null` when unknown. |
|
|
235
319
|
| `snapshot.plurnk.status.model` | `ApplicationPort.readWorkerModel` | The bound Worker's durable resolved model route, including its alias when selected. |
|
|
236
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}. |
|
|
237
321
|
| `snapshot.plurnk.status.packetCount` | latest `ApplicationLoopProjection.packetCount` | Exact packet-bearing Turn count; packetless Turns and provider retries do not contribute. |
|
|
238
|
-
| §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. |
|
|
239
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. |
|
|
240
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. |
|
|
241
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. |
|
|
242
|
-
| `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
|
|
243
|
-
| `snapshot.plurnk.
|
|
244
|
-
| `
|
|
245
|
-
| `snapshot.
|
|
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. |
|
|
246
333
|
| `STATE_DELTA /budget/curationWeight` | `loop/terminated.usage.curationWeight` | Latest assembled packet's model-independent curation weight, or `null` when no packet exists. |
|
|
247
334
|
| `STATE_DELTA /budget/curationBudget` | `loop/terminated.usage.curationBudget` | Latest turn's provider-derived curation calibration, or `null` when capacity is unknown. |
|
|
248
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. |
|
|
249
336
|
| `STATE_DELTA /budget/contextCapacity` | `loop/terminated.usage.contextCapacity` | That same latest emission call's request-shaped physical input capacity, or `null` when unknown. |
|
|
250
337
|
|
|
251
|
-
§agui-cost-evidence AG-UI
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
254
341
|
{§provider-accounting} verbatim: ordered physical request records, conventional
|
|
255
342
|
input/output totals with cache/reasoning details, and exact-decimal nullable
|
|
256
|
-
`costUsd`. The
|
|
343
|
+
`costUsd`. The terminal record distinguishes charged, Models.dev-estimated,
|
|
257
344
|
unknown, and exact-zero evidence without fabricating a value. Generic clients
|
|
258
|
-
ignore the
|
|
345
|
+
ignore the metadata; PLURNK clients consume the same contracts-owned shape as
|
|
259
346
|
every other daemon surface.
|
|
260
347
|
|
|
261
|
-
- §agui-
|
|
262
|
-
carrying the complete client-facing row
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
271
359
|
|
|
272
360
|
- §agui-replay **Unoriented reattach replays once** — a rediscovered thread (the module restarted, a second
|
|
273
361
|
frontend arrived) whose Run input carries none of the log's durable assistant identities attaches
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Everything else stays
|
|
280
|
-
reachable via live `plurnk.row`. Pending proposals and client interactions remain durable while
|
|
281
|
-
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
|
|
282
367
|
resumed; a days-old question is discoverable, never converted into a mystery hang.
|
|
283
368
|
|
|
284
|
-
- §agui-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
287
375
|
emits the normal `RUN_STARTED` and initial `STATE_SNAPSHOT`, then emits an authoritative
|
|
288
|
-
`MESSAGES_SNAPSHOT`
|
|
289
|
-
|
|
290
|
-
|
|
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,
|
|
291
384
|
creates no turn, and performs no model inference. Disconnecting this observer never cancels
|
|
292
385
|
independently-owned work.
|
|
293
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
|
+
|
|
294
408
|
## §agui-first-party-client-conformance First-party client conformance
|
|
295
409
|
|
|
296
410
|
§agui-official-client-conformance The ordinary integration gate runs the official
|
|
@@ -304,6 +418,12 @@ Deterministic daemon fixtures use the isolated mock-tier bootstrap
|
|
|
304
418
|
({§operator-config-real-model-profile}); operator configuration must not start
|
|
305
419
|
external resources during the integration gate.
|
|
306
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.
|
|
426
|
+
|
|
307
427
|
The one-shot CLI and interactive terminal consume one AG-UI+
|
|
308
428
|
semantic contract. Each surface verifies the contracts-owned conformance corpus
|
|
309
429
|
through its production transport and separately verifies the presentation its
|
|
@@ -334,28 +454,44 @@ explicit path fails rather than falling back or skipping conformance.
|
|
|
334
454
|
|
|
335
455
|
## §agui-proposal-resolve Client-owned stop-the-world interactions
|
|
336
456
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`Interrupt.
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
345
481
|
cancellation responsibility ({§agui-run-disconnect}). Unknown presentations require
|
|
346
482
|
rediscovery; pending proposals and interactions remain durable in Core. The module
|
|
347
483
|
resolves the complete pending interrupt set for one worker, restores the thread's live projection
|
|
348
484
|
state, and only then releases it. The pending Worker's persisted Worker and Loop remain the exact
|
|
349
|
-
resolution identity. Owner-addressed gates route under {§agui-worker-owner}: when an
|
|
350
|
-
|
|
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
|
|
351
487
|
releasing the exact requesting worker. Reconnect discovers pending gates from Core by recorded
|
|
352
488
|
owner, not inferred ancestry. A terminal published after a
|
|
353
489
|
descendant gate remains ordered behind that gate's topology resolution and presentation; it cannot
|
|
354
490
|
settle Run A before the interrupt is emitted. Concurrent descendant gates are
|
|
355
491
|
presented one Worker at a time; resolving one re-surfaces the next without admitting a new prompt.
|
|
356
|
-
Sibling and unrelated
|
|
492
|
+
Sibling and unrelated threads never receive one another's gates. A resume containing foreign, partial, unknown, duplicate, or multi-worker
|
|
357
493
|
interrupt sets fails before any stopped operation is released. Mixing presentations or
|
|
358
|
-
|
|
494
|
+
threads produces `interrupt-binding-invalid`; a connection closed before validated
|
|
359
495
|
resolution produces `connection-closed` without releasing the stopped work.
|
|
360
496
|
Loop terminals received while a resume is being validated or resolved are held:
|
|
361
497
|
successful resolution delivers the bound Loop's terminal once; rejected resolution
|
|
@@ -380,19 +516,16 @@ initial state, and a new `STEP_STARTED` for that same durable turn before releas
|
|
|
380
516
|
operation. The continued loop later closes that Run B step normally. No AG-UI Run may finish
|
|
381
517
|
while a step remains active.
|
|
382
518
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`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
|
|
386
521
|
unchanged under {§proposal-outcome-terse-error}; invalid outcome types refuse the resume
|
|
387
522
|
before any proposal is released. An omitted outcome leaves Core's existing default intact.
|
|
388
|
-
|
|
389
|
-
interrupt carries its optional `message` and `responseSchema`. A resolved resume payload becomes
|
|
390
|
-
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
|
|
391
524
|
never sees or reconstructs an upstream protocol's continuation state.
|
|
392
525
|
|
|
393
526
|
§agui-proposal-disposition AG-UI consumes Core's `ProposalProjection` identically
|
|
394
|
-
for live delivery and reconnect. A `review` disposition becomes a
|
|
395
|
-
|
|
527
|
+
for live delivery and reconnect. A `review` disposition becomes a standard interrupt
|
|
528
|
+
only for the matching owner. `accept` and `reject` remain
|
|
396
529
|
Core's automatic settlements and never become client review work. Operation,
|
|
397
530
|
attrs and stale-target facts remain evidence, not authority inputs.
|
|
398
531
|
|
|
@@ -404,32 +537,34 @@ selection.
|
|
|
404
537
|
|
|
405
538
|
## §agui-management-plane The action surface
|
|
406
539
|
|
|
407
|
-
|
|
408
|
-
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.
|
|
409
542
|
|
|
410
543
|
```mermaid
|
|
411
544
|
flowchart LR
|
|
412
545
|
client["AG-UI client"] --> run["POST RunAgentInput"]
|
|
413
|
-
run --> classify{"
|
|
414
|
-
classify -->|"
|
|
546
|
+
run --> classify{"What the conversation doesn't hold"}
|
|
547
|
+
classify -->|"new user message"| loop["ApplicationPort.runLoop"]
|
|
415
548
|
classify -->|"RunAgentInput.resume"| resume["ApplicationPort proposal / interaction resolution"]
|
|
416
549
|
classify -->|"forwardedProps.plurnk.action"| actions{"AG-UI action registry"}
|
|
550
|
+
classify -->|"none of them"| sync["Synchronization"]
|
|
417
551
|
actions --> builtIn["Advertised schema admission<br/>and typed ApplicationPort call"]
|
|
418
552
|
actions --> extension["Scoped ApplicationPort.invokeModuleAction"]
|
|
419
553
|
loop --> events["Core event source"]
|
|
420
554
|
resume --> events
|
|
555
|
+
sync --> events
|
|
421
556
|
builtIn --> events
|
|
422
557
|
events --> projection["AG-UI projection"]
|
|
423
|
-
extension --> result["
|
|
558
|
+
extension --> result["RUN_FINISHED result"]
|
|
424
559
|
projection --> client
|
|
425
560
|
result --> client
|
|
426
561
|
```
|
|
427
562
|
|
|
428
563
|
A management-action Run carries
|
|
429
|
-
`forwardedProps.plurnk.action = { kind, ...params }`. Once settled,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
interrupt
|
|
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
|
|
433
568
|
action preserves exact RFC 9457 Problem Details and is the result of a
|
|
434
569
|
successfully transported management Run; it does not turn the Run into
|
|
435
570
|
`RUN_ERROR` or invent a parallel error string. Unknown kinds return an honest
|
|
@@ -437,38 +572,66 @@ successfully transported management Run; it does not turn the Run into
|
|
|
437
572
|
|
|
438
573
|
| Public action | Scope | Parameters | Owner / effect |
|
|
439
574
|
|--------------------------|-----------|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
|
440
|
-
| `ping` |
|
|
441
|
-
| `discover` |
|
|
442
|
-
| `providers.list` |
|
|
443
|
-
| `models.list` |
|
|
444
|
-
| `workspace.list` |
|
|
445
|
-
| `workspace.create` |
|
|
446
|
-
| `workspace.attach` |
|
|
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. |
|
|
447
582
|
| `workspace.workers` | Workspace | `id?` | `ApplicationPort.listWorkers`; an explicit id overrides the bound workspace. |
|
|
448
|
-
| `log.read` | Workspace | `workerId?`, log-coordinate filters | `ApplicationPort.readLog`; defaults to the thread's
|
|
449
|
-
| `loop.inject` | Workspace | `prompt` | `ApplicationPort.runLoop` on the thread's
|
|
450
|
-
| `loop.cancel` | Workspace | `reason?` | `ApplicationPort.cancelDrain` on the thread's
|
|
451
|
-
| `workspace.prompts` | Workspace | `workerId?`, `limit?` | `ApplicationPort.listPrompts` scoped like `log.read`: the thread's
|
|
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. |
|
|
452
587
|
| `workspace.rename` | Workspace | `name` | `ApplicationPort.renameWorkspace`. |
|
|
453
588
|
| `workspace.share` | Workspace | `folder` | `ApplicationPort.shareWorkspace` shares the bound workspace ({§share}) into an absolute `folder` on the service host; returns `{ folder }`. |
|
|
454
|
-
| `workspace.rollback` | Workspace | `to`, `workerId?` | `ApplicationPort.rollback` ({§rollback}) reverses the settled file mutations after log coordinate `to` of the thread's
|
|
455
|
-
| `entry.read` | Workspace | `target`, `workerId?`, `channel?`, `offset?` | Calls `ApplicationPort.readEntry` from the explicit worker perspective or the thread
|
|
456
|
-
| `op.exec` | Workspace | `command` | Constructs one execution statement on the default `sh` runtime and
|
|
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}). |
|
|
457
592
|
| `op.parse` | Workspace | `text` | Parses and projects PLURNK text under {§agui-op-parse}. |
|
|
458
|
-
| `
|
|
459
|
-
| `
|
|
460
|
-
| `worker.model.
|
|
461
|
-
| `worker.
|
|
462
|
-
| `worker.
|
|
463
|
-
| `worker.effort.
|
|
464
|
-
| `
|
|
465
|
-
| `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}. |
|
|
466
600
|
| `workspace.capabilities.set` | Workspace | `policy` | `ApplicationPort.setWorkspaceCapabilities` replaces the workspace policy and returns the complete resolver-owned projection {§capability-policy-projection}. |
|
|
467
|
-
| Registered module action | Owner-declared | owner-defined | `ApplicationPort.invokeModuleAction`; AG-UI enforces the owner's input/output schemas and passes a
|
|
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.
|
|
468
631
|
|
|
469
632
|
§agui-worker-model-actions **Worker model selection is server-backed.**
|
|
470
633
|
`worker.model.get`, `worker.model.set`, and `worker.child.set` operate on the
|
|
471
|
-
thread's bound
|
|
634
|
+
thread's bound worker under {§worker-model-selection}. They are
|
|
472
635
|
the client's durable `/model` and `/child`; a client must not reassert a
|
|
473
636
|
model on every loop. The get action returns the worker's resolved durable
|
|
474
637
|
model and spawn override (`null` for an unset worker or inherit); the set
|
|
@@ -486,19 +649,19 @@ loop is active or parked.
|
|
|
486
649
|
§agui-module-action-scope **An extension action uses the same management plane,
|
|
487
650
|
not a private endpoint.** `ApplicationPort.listModuleActions()` returns its exact
|
|
488
651
|
`{ name, scope, inputSchema, outputSchema }` descriptors under
|
|
489
|
-
{§agui-discovery-contract}. A
|
|
652
|
+
{§agui-discovery-contract}. A daemon action follows the control path and
|
|
490
653
|
cannot establish a workspace. A workspace action follows ordinary thread
|
|
491
654
|
binding, resolves the client envelope first, and invokes core with
|
|
492
655
|
`{ scope: "workspace", workspaceId: envelope.workspaceId }`; an `id`,
|
|
493
656
|
`workspaceId`, or similarly named owner parameter cannot override that
|
|
494
|
-
identity. A Worker action additionally receives the thread's bound
|
|
657
|
+
identity. A Worker action additionally receives the thread's bound
|
|
495
658
|
Worker as `{ scope: "worker", workspaceId, workerId }`; client parameters cannot
|
|
496
659
|
select either authority coordinate. `discover.actions` advertises all three
|
|
497
660
|
kinds without importing the extension owner.
|
|
498
661
|
|
|
499
662
|
### §agui-op-parse Parsed-operation projection
|
|
500
663
|
|
|
501
|
-
`op.parse` dispatches all trusted-prefix statements as one client
|
|
664
|
+
`op.parse` dispatches all trusted-prefix statements as one client operation ({§agui-client-operation}) and
|
|
502
665
|
preserves the parser's source order in its `results` array:
|
|
503
666
|
|
|
504
667
|
| Parser output | AG-UI result |
|
|
@@ -511,29 +674,6 @@ No statement at or beyond the tail boundary is dispatched. Every parse failure
|
|
|
511
674
|
uses `stage: "parsing"` and `retryable: false`. Outside text is ignored by the
|
|
512
675
|
parser under {§whitespace-contract} and produces no result.
|
|
513
676
|
|
|
514
|
-
### §agui-op-look Single-statement observation admission
|
|
515
|
-
|
|
516
|
-
`op.look` admits a parser result only when it contains exactly one LOOK
|
|
517
|
-
statement, no other item, and no `unparsedTail`. Outside text and whitespace
|
|
518
|
-
produce no item under {§whitespace-contract}. The action never selects a trusted prefix or silently discards
|
|
519
|
-
a parser fact.
|
|
520
|
-
|
|
521
|
-
| Parser result | AG-UI outcome |
|
|
522
|
-
|-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
|
523
|
-
| First positioned diagnostic | 400 `agui/action/parse-failed`, preserving parser detail, line, column, source, and severity |
|
|
524
|
-
| No diagnostic, with `unparsedTail` | 400 `agui/action/parse-failed`, preserving its reason and position with `source: "grammar"` and `severity: "error"` |
|
|
525
|
-
| Zero or multiple statements, or non-LOOK | 400 `agui/action/invalid-action-parameters` naming the observed admission fact |
|
|
526
|
-
| 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 |
|
|
527
|
-
|
|
528
|
-
Parser failures use `stage: "parsing"`; action-shape failures use
|
|
529
|
-
`stage: "action-validation"`. Both are non-retryable.
|
|
530
|
-
|
|
531
|
-
The observation resolves `log:///` as the thread's conversation worker; `workerId`
|
|
532
|
-
pins another workspace worker. Explicit resource authorities retain their identity
|
|
533
|
-
under {§turn-source-resources}. The closed observation segment stays on the connection's own
|
|
534
|
-
worker: a look never adds a loop to the conversation, so its lifecycle, packet count, and
|
|
535
|
-
history are unchanged by inspection.
|
|
536
|
-
|
|
537
677
|
§agui-module-actions **One public action namespace.** Core rejects empty and
|
|
538
678
|
duplicate extension registrations ({§module-action-registration}). At module
|
|
539
679
|
startup AG-UI rejects any extension name colliding with a built-in before it
|
|
@@ -552,7 +692,7 @@ The `discover` action returns the complete schema-bearing
|
|
|
552
692
|
type Discovery = {
|
|
553
693
|
schemaVersion: 1;
|
|
554
694
|
actions: Record<string, {
|
|
555
|
-
scope: "
|
|
695
|
+
scope: "daemon" | "workspace" | "worker";
|
|
556
696
|
inputSchema: JsonSchema;
|
|
557
697
|
outputSchema: JsonSchema;
|
|
558
698
|
}>;
|
|
@@ -590,15 +730,15 @@ contract.
|
|
|
590
730
|
|
|
591
731
|
§agui-stream-producer Stream updates route to `workerId`, the initiating
|
|
592
732
|
Worker. Resource reads use the canonical workspace address, not a separate
|
|
593
|
-
read perspective. Shared resources neither broadcast to unrelated
|
|
733
|
+
read perspective. Shared resources neither broadcast to unrelated workers
|
|
594
734
|
nor hide updates from the producer ({§notifications-stream-event-on-channel-change}).
|
|
595
735
|
|
|
596
736
|
`loop.inject`'s steered effect streams on the original worker's open SSE.
|
|
597
737
|
`loop.cancel` is its counterpart: the addressable spelling of the SSE-hangup
|
|
598
738
|
abort. An action that opens a stream remains one live AG-UI Run; its result is
|
|
599
739
|
held until every observed stream emits `stream/concluded`, then the result and
|
|
600
|
-
`RUN_FINISHED` close that Run.
|
|
601
|
-
responsibility
|
|
740
|
+
`RUN_FINISHED` close that Run. An executing operation Run retains its cancellation
|
|
741
|
+
responsibility under {§agui-run-disconnect}.
|
|
602
742
|
|
|
603
743
|
## §agui-topology-scope Topology scope
|
|
604
744
|
|
|
@@ -610,19 +750,39 @@ lineage or commons attention ({§actor-boundary}), or under the delegation
|
|
|
610
750
|
observation the Run asked for ({§agui-delegation-observation}). AG-UI does not
|
|
611
751
|
synthesize a second topology by rebroadcasting sibling rows.
|
|
612
752
|
|
|
613
|
-
§agui-delegation-observation **A
|
|
614
|
-
|
|
753
|
+
§agui-delegation-observation **A message Run may observe its delegation.** A
|
|
754
|
+
message Run whose `forwardedProps.plurnk.descendants` is `true` also receives the
|
|
615
755
|
rows (`log/entry`) and stream events (`stream/event`, `stream/concluded`) of every Worker
|
|
616
|
-
beneath its bound Worker in Core's authoritative topology
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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.
|
|
626
786
|
|
|
627
787
|
## §agui-broadcast-fan Event fan and AG-UI Run settlement
|
|
628
788
|
|
|
@@ -633,9 +793,8 @@ interrupt-resume Run restores its predecessor's projection and notification
|
|
|
633
793
|
scope. A same-worker interrupt remains bound to the pending item's Worker and
|
|
634
794
|
Loop. A foreign-worker interrupt preserves the owner's controlling Run binding,
|
|
635
795
|
using the pending item's Worker and Loop only as its release identity. Only the
|
|
636
|
-
controlling Run's bound Loop terminal may
|
|
637
|
-
|
|
638
|
-
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
|
|
639
798
|
the Problem `type` as the AG-UI error code and `detail` as its message. The
|
|
640
799
|
module never reconstructs a failure from a status, summary, or exception text.
|
|
641
800
|
Gate routing follows {§agui-worker-owner}; topology does not confer authority.
|
|
@@ -647,8 +806,8 @@ this Run.
|
|
|
647
806
|
|
|
648
807
|
| AG-UI Run | Core notifications admitted | Settlement owner |
|
|
649
808
|
| ---------- | --------------------------- | ---------------- |
|
|
650
|
-
| 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
|
|
651
|
-
| `op.exec` / `op.parse` |
|
|
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. |
|
|
652
811
|
| Every other action | None; the Run carries only its direct state snapshot and action result. | This action result. |
|
|
653
812
|
|
|
654
813
|
An operation Run owns an execution from the row that announces it: a started or
|
|
@@ -657,12 +816,138 @@ proposed address opens no stream. An admitted address stays open for the Run
|
|
|
657
816
|
until its `stream/concluded`, so the action result is deferred even when the
|
|
658
817
|
command writes late or never (a `stream/event` is a race the result must not
|
|
659
818
|
win). A detached execution (`<-1>`) is nobody's obligation and never defers the
|
|
660
|
-
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.
|
|
661
824
|
|
|
662
825
|
`workerId` (or `entry.worker_id`) is the actor owner; `loopId` (or
|
|
663
826
|
`entry.loop_id`) refines it wherever the event is loop-scoped. Opening a
|
|
664
827
|
read-only management Run can therefore never replay reasoning, operation rows,
|
|
665
|
-
or turn disposition from an active
|
|
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.
|
|
666
951
|
|
|
667
952
|
## §agui-configuration Module configuration
|
|
668
953
|
|
|
@@ -675,28 +960,51 @@ address is the daemon's.
|
|
|
675
960
|
| Input | Owner | Empty or absent | Accepted value | Effect |
|
|
676
961
|
| ---------------------------------- | ------- | -------------------------------------- | ----------------------------------- | ------ |
|
|
677
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. |
|
|
678
|
-
| `
|
|
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. |
|
|
679
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}). |
|
|
680
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. |
|
|
681
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. |
|
|
682
972
|
|
|
683
973
|
## §agui-run-endpoint The AG-UI Run endpoint
|
|
684
974
|
|
|
685
|
-
`POST /agui` accepts a schema-valid AG-UI `RunAgentInput
|
|
686
|
-
`
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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}).
|
|
692
998
|
|
|
693
999
|
§agui-run-source The run's user message is the causal actor behind its inbound SEND
|
|
694
1000
|
({§message-causal-source}). The module supplies its canonical address as the loop's `source`:
|
|
695
1001
|
`agui://<principal>/threads/<threadId>/messages/<messageId>`, every segment
|
|
696
|
-
URI-encoded, the principal
|
|
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
|
|
697
1005
|
self-identification never enters the address ({§client-metadata}). The arrival row renders that
|
|
698
1006
|
source, so a model tells an operator's message from a worker's or a schedule's by address
|
|
699
|
-
rather than by absence. Message identity belongs to the
|
|
1007
|
+
rather than by absence. Message identity belongs to the thread, not a transport run;
|
|
700
1008
|
the run ID remains envelope provenance. Accepted text remains available through ordinary
|
|
701
1009
|
READ, FIND and COPY after log curation, run conclusion, disconnect and daemon restart.
|
|
702
1010
|
`loop.inject` normalizes its prompt into a user message with a server-generated message ID
|
|
@@ -706,32 +1014,39 @@ SEND to this exact address answers this message under {§send-response-receipt};
|
|
|
706
1014
|
not create another inbound request. Standard AG-UI assistant messages and history replay
|
|
707
1015
|
carry the reply, independently of whether the loop can yet conclude.
|
|
708
1016
|
|
|
709
|
-
§agui-http-authorization
|
|
710
|
-
non-preflight request
|
|
711
|
-
`authorization: Bearer <token
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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.
|
|
720
1033
|
|
|
721
1034
|
§agui-cors Every response carries `access-control-allow-origin` with the value
|
|
722
1035
|
of `PLURNK_AGUI_ALLOW_ORIGIN`, and allows the `content-type` and
|
|
723
1036
|
`authorization` request headers; an empty value sends neither header, so no
|
|
724
|
-
|
|
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
|
|
725
1040
|
not authenticate a request and is no substitute for the bearer
|
|
726
1041
|
({§agui-http-authorization}). A preflight is answered `204` before
|
|
727
1042
|
authorization.
|
|
728
1043
|
|
|
729
1044
|
§agui-http-failure Failures before SSE headers are sent use
|
|
730
1045
|
`application/problem+json` with exact
|
|
731
|
-
RFC 9457 Problem Details. Once SSE has opened, `
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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.
|
|
735
1050
|
Consumers preserve the exact Problem from the lossless surface and never
|
|
736
1051
|
reconstruct one from `RUN_ERROR`.
|
|
737
1052
|
|
|
@@ -740,14 +1055,20 @@ reconstruct one from `RUN_ERROR`.
|
|
|
740
1055
|
| code | status | contract |
|
|
741
1056
|
|---|---:|---|
|
|
742
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. |
|
|
743
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}). |
|
|
744
1063
|
| `route-not-found` | 404 | The requested HTTP route does not exist. |
|
|
745
1064
|
| `request-failed` | 500 | The AG-UI request failed unexpectedly. |
|
|
746
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>`. |
|
|
747
1068
|
| `interrupt-duplicate` | 400 | The resume contains the same interrupt more than once. Recovery: Include each pending interrupt exactly once. |
|
|
748
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. |
|
|
749
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. |
|
|
750
|
-
| `interrupt-binding-invalid` | 400 | The resume combines interrupts from different Runs, or does not match its interrupt's
|
|
1071
|
+
| `interrupt-binding-invalid` | 400 | The resume combines interrupts from different Runs, or does not match its interrupt's binding. |
|
|
751
1072
|
| `connection-closed` | 499 | The connection closed before the interrupt was resolved. |
|
|
752
1073
|
| `worker-scope-invalid` | 400 | One resume must address interrupts for exactly one worker. Recovery: Resume each worker separately. |
|
|
753
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. |
|
|
@@ -755,9 +1076,10 @@ reconstruct one from `RUN_ERROR`.
|
|
|
755
1076
|
| `invalid-run-input` | 400 | The request body does not satisfy the AG-UI RunAgentInput contract. |
|
|
756
1077
|
| `control-invalid` | 400 | control must be a boolean. |
|
|
757
1078
|
| `interactive-invalid` | 400 | interactive must be a boolean. |
|
|
758
|
-
| `
|
|
759
|
-
| `
|
|
760
|
-
| `
|
|
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. |
|
|
761
1083
|
| `run-failed` | 500 | The AG-UI Run failed unexpectedly. |
|
|
762
1084
|
|
|
763
1085
|
§pinned-wording-agui **Pinned wording.** Verbatim sentences tests pin: each is contract, and a change here is a change of contract.
|
|
@@ -766,5 +1088,4 @@ reconstruct one from `RUN_ERROR`.
|
|
|
766
1088
|
|---|---|
|
|
767
1089
|
| Action '*name*' is not registered. Recovery: Use an action advertised by discover. | an action call naming no registered action |
|
|
768
1090
|
| Action '*name*' rejected parameters. *schema path*: *reason*. | an action call whose parameters fail the action's schema |
|
|
769
|
-
| *action* parsed *n* statements; exactly one *OP* statement is required. | a client op action whose block holds other than one statement |
|
|
770
1091
|
| The action failed unexpectedly. | an action handler that threw |
|