@plurnk/plurnk-agui 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +12 -8
package/dist/Translator.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { TurnDisposition } from "@plurnk/plurnk-contracts";
|
|
2
1
|
// The projection — plurnk's log-shaped wire onto AG-UI's event vocabulary. PURE: one daemon
|
|
3
2
|
// notification in, zero-or-more AG-UI events out, with per-worker turn tracking as the only state.
|
|
4
3
|
// The mapping ({§agui-projection}):
|
|
5
4
|
// log/entry response (model) → optional standard reasoning lifecycle, then TEXT_MESSAGE triple
|
|
6
|
-
// (assistant speech
|
|
7
|
-
// log/entry rejected emission →
|
|
5
|
+
// (assistant speech, a reply or a WAIT's body)
|
|
6
|
+
// log/entry rejected emission → CUSTOM plurnk.ambient, no message event
|
|
8
7
|
// log/entry other (model) → TOOL_CALL_START/ARGS/END + TOOL_CALL_RESULT (an op row IS a
|
|
9
|
-
// tool call: tx is the args, rx the result, coordinate the id)
|
|
10
|
-
// log/entry
|
|
11
|
-
//
|
|
8
|
+
// tool call: tx is the args, rx the result, coordinate the id; a bodiless WAIT too)
|
|
9
|
+
// log/entry arrival (plurnk) → TEXT_MESSAGE triple, role user, named by its source (another client's
|
|
10
|
+
// prompt, a worker's message), except the Run's own submission
|
|
11
|
+
// log/entry (plurnk) → CUSTOM plurnk.ambient (deltas, narrations — the environment
|
|
12
|
+
// speaking; generic UIs skip, rich UIs render)
|
|
12
13
|
// outside/event → CUSTOM plurnk.outside (the thread's worker only: text the model
|
|
13
14
|
// emitted outside every operation, never assistant speech)
|
|
14
15
|
// turn_id advances → STEP_FINISHED/STEP_STARTED
|
|
@@ -17,11 +18,13 @@ import { TurnDisposition } from "@plurnk/plurnk-contracts";
|
|
|
17
18
|
// Numbers are passed through verbatim, never recomputed — the daemon's gauge is the gauge
|
|
18
19
|
// ({§agui-numbers-passthrough}).
|
|
19
20
|
import { EventType, } from "./types.js";
|
|
21
|
+
import { aggregateTokenUsage, tokenUsageFromAiSdkUsage } from "@ag-ui/core";
|
|
20
22
|
import { derivationActivity } from "./AguiPlus.js";
|
|
21
23
|
import MessageAddress from "./MessageAddress.js";
|
|
22
24
|
import { Validator, lifecycleOfLoopStatus } from "@plurnk/plurnk-contracts";
|
|
23
25
|
export default class Translator {
|
|
24
26
|
#threadId;
|
|
27
|
+
#replyThreads;
|
|
25
28
|
#runId; // AG-UI's Run id (echoed from RunAgentInput.runId) — the standard face
|
|
26
29
|
#currentTurn = null;
|
|
27
30
|
#stepOpen = false;
|
|
@@ -29,12 +32,23 @@ export default class Translator {
|
|
|
29
32
|
#workspaceId;
|
|
30
33
|
#activeReasoning = new Map();
|
|
31
34
|
#completedReasoning = new Map();
|
|
35
|
+
// {§agui-snapshot} The messages this Run submitted: its client already holds them.
|
|
36
|
+
#submitted = new Set();
|
|
37
|
+
// {§agui-call-result} The calls this Run's client holds without their result: a gate's call it presented, or a
|
|
38
|
+
// pending one its snapshot held. A later delivery of that row only answers it.
|
|
39
|
+
#held = new Set();
|
|
40
|
+
// {§agui-subagents} A delegation this Run observes: the subagent run its events are attributed to, and the worker
|
|
41
|
+
// whose rows' identities it namespaces; null for the Run's own worker.
|
|
42
|
+
#subagent;
|
|
32
43
|
constructor(args) {
|
|
44
|
+
this.#subagent = args.subagent ?? null;
|
|
33
45
|
this.#threadId = args.threadId;
|
|
46
|
+
this.#replyThreads = args.replyThreads ?? [args.threadId];
|
|
34
47
|
this.#runId = args.runId;
|
|
35
48
|
this.#currentTurn = args.continuation?.currentTurn ?? null;
|
|
36
49
|
this.#modelWorkerId = args.continuation?.modelWorkerId ?? args.modelWorkerId ?? null;
|
|
37
50
|
this.#completedReasoning = new Map(args.continuation?.completedReasoning.map(([turnId, values]) => [turnId, [...values]]) ?? []);
|
|
51
|
+
this.#held = args.held ?? new Set(args.continuation?.held ?? []);
|
|
38
52
|
this.#workspaceId = args.workspaceId ?? null;
|
|
39
53
|
}
|
|
40
54
|
#continuation() {
|
|
@@ -45,6 +59,7 @@ export default class Translator {
|
|
|
45
59
|
currentTurn: this.#currentTurn,
|
|
46
60
|
modelWorkerId: this.#modelWorkerId,
|
|
47
61
|
completedReasoning: [...this.#completedReasoning].map(([turnId, values]) => [turnId, [...values]]),
|
|
62
|
+
held: [...this.#held],
|
|
48
63
|
};
|
|
49
64
|
}
|
|
50
65
|
runStarted(state) {
|
|
@@ -62,6 +77,10 @@ export default class Translator {
|
|
|
62
77
|
}
|
|
63
78
|
return events;
|
|
64
79
|
}
|
|
80
|
+
// A message this Run submits arrives in its worker's log like any other; its own client already holds it.
|
|
81
|
+
submitted(messageId) {
|
|
82
|
+
this.#submitted.add(messageId);
|
|
83
|
+
}
|
|
65
84
|
// {§agui-gate-deferral} — a stopped-world cannot split this lifecycle; the Portal holds it until the end.
|
|
66
85
|
get reasoningOpen() { return this.#activeReasoning.size > 0; }
|
|
67
86
|
interrupt() {
|
|
@@ -86,79 +105,136 @@ export default class Translator {
|
|
|
86
105
|
this.#currentTurn = null;
|
|
87
106
|
this.#stepOpen = false;
|
|
88
107
|
this.#completedReasoning.clear();
|
|
89
|
-
return events;
|
|
108
|
+
return this.#attributed(events);
|
|
109
|
+
}
|
|
110
|
+
// {§agui-readable-reasoning} A Run ended before its request is (revoked, stopped, failed) still
|
|
111
|
+
// balances what it opened; the request itself streams on to the Runs still observing it.
|
|
112
|
+
fail() {
|
|
113
|
+
const events = [];
|
|
114
|
+
for (const { messageId } of this.#activeReasoning.values()) {
|
|
115
|
+
events.push({ type: EventType.REASONING_MESSAGE_END, messageId }, { type: EventType.REASONING_END, messageId });
|
|
116
|
+
}
|
|
117
|
+
this.#activeReasoning.clear();
|
|
118
|
+
return [...events, ...this.finish()];
|
|
90
119
|
}
|
|
91
120
|
logEntry(n) {
|
|
121
|
+
return this.#attributed(this.#project(n));
|
|
122
|
+
}
|
|
123
|
+
// {§agui-subagents} The translator of a subagent run this Run presents: its own namespace, this Run's client.
|
|
124
|
+
subagentTranslator(args) {
|
|
125
|
+
return new Translator({ threadId: this.#threadId, runId: this.#runId, modelWorkerId: args.modelWorkerId, workspaceId: this.#workspaceId,
|
|
126
|
+
subagent: args.subagent, held: this.#held, replyThreads: this.#replyThreads });
|
|
127
|
+
}
|
|
128
|
+
// {§agui-proposal-resolve} A gate's call: the operation awaiting its decision, built as its row builds it and so
|
|
129
|
+
// unanswered. Its client then holds it, and the settled row updates and answers it ({§agui-call-result}).
|
|
130
|
+
gateCall(entry) {
|
|
131
|
+
if (this.#held.has(this.#id(entry)))
|
|
132
|
+
return [];
|
|
133
|
+
const events = this.#project({ entry });
|
|
134
|
+
this.#held.add(this.#id(entry));
|
|
135
|
+
return this.#attributed(events);
|
|
136
|
+
}
|
|
137
|
+
#project(n) {
|
|
92
138
|
const e = n.entry;
|
|
93
139
|
const events = [];
|
|
94
|
-
const clientEntry = e;
|
|
95
140
|
// {§agui-topology-scope} — the workspace broadcast carries EVERY worker's rows (workers, the
|
|
96
141
|
// plurnk worker, siblings); only the THREAD's model worker projects onto the core vocabulary.
|
|
97
|
-
//
|
|
98
|
-
// never interleaved into the conversation a generic frontend renders.
|
|
142
|
+
// Another worker's row is workspace activity, never interleaved into the messages a generic frontend renders.
|
|
99
143
|
const workerId = e.worker_id;
|
|
100
|
-
// Lazy binding:
|
|
101
|
-
//
|
|
102
|
-
// model worker (workers spawn FROM it later; reattach seeds it from workspace.workers instead).
|
|
144
|
+
// Lazy binding: a Run that binds no model worker adopts its first model-origin row's worker as the
|
|
145
|
+
// model worker (workers spawn FROM it later).
|
|
103
146
|
if (this.#modelWorkerId === null && e.origin === "model" && typeof workerId === "number")
|
|
104
147
|
this.#modelWorkerId = workerId;
|
|
105
148
|
const foreign = this.#modelWorkerId !== null && typeof workerId === "number" && workerId !== this.#modelWorkerId;
|
|
106
|
-
// {§agui-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (foreign) {
|
|
111
|
-
events.push(row);
|
|
112
|
-
events.push({ type: EventType.CUSTOM, name: "plurnk.ambient", value: clientEntry });
|
|
113
|
-
return events;
|
|
114
|
-
}
|
|
115
|
-
// A family client renders the SEND from plurnk.row rather than duplicating
|
|
116
|
-
// TEXT_MESSAGE. Delay that one mirror until after the standard reasoning
|
|
117
|
-
// lifecycle so both generic and family clients observe reasoning before speech.
|
|
118
|
-
const response = Translator.isResponse(e, this.#threadId);
|
|
119
|
-
const lifecycle = typeof e.op === "string" && TurnDisposition.isOp(e.op);
|
|
120
|
-
const reasoningRow = response || lifecycle || e.op === "NOTE";
|
|
121
|
-
const delayedSendRow = e.origin === "model" && reasoningRow;
|
|
122
|
-
if (!delayedSendRow)
|
|
123
|
-
events.push(row);
|
|
149
|
+
// {§agui-ambient} A row that builds nothing in the conversation rides whole.
|
|
150
|
+
const ambient = { type: EventType.CUSTOM, name: "plurnk.ambient", value: e };
|
|
151
|
+
if (foreign)
|
|
152
|
+
return [ambient];
|
|
124
153
|
if (typeof e.turn_id === "number")
|
|
125
154
|
events.push(...this.#enterTurn(e.turn_id));
|
|
126
|
-
|
|
127
|
-
|
|
155
|
+
const arrival = this.#arrival(e);
|
|
156
|
+
if (arrival !== null) {
|
|
157
|
+
// {§agui-plurnk-metadata} The Run that submitted a message already holds it: it receives only the row's facts,
|
|
158
|
+
// which the reducer folds onto that message.
|
|
159
|
+
if (this.#submitted.has(arrival.id)) {
|
|
160
|
+
events.push({ type: EventType.TEXT_MESSAGE_START, messageId: arrival.id, role: "user", metadata: Translator.#rowMetadata(e) });
|
|
161
|
+
events.push({ type: EventType.TEXT_MESSAGE_END, messageId: arrival.id });
|
|
162
|
+
return events;
|
|
163
|
+
}
|
|
164
|
+
events.push({ type: EventType.TEXT_MESSAGE_START, messageId: arrival.id, role: "user", ...(arrival.name === undefined ? {} : { name: arrival.name }), metadata: Translator.#rowMetadata(e) });
|
|
165
|
+
events.push({ type: EventType.TEXT_MESSAGE_CONTENT, messageId: arrival.id, delta: arrival.content });
|
|
166
|
+
events.push({ type: EventType.TEXT_MESSAGE_END, messageId: arrival.id });
|
|
128
167
|
return events;
|
|
129
168
|
}
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
events
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
events.push({ type: EventType.
|
|
169
|
+
const response = Translator.isResponse(e, this.#replyThreads);
|
|
170
|
+
if (e.origin !== "model" && !response) {
|
|
171
|
+
const answered = this.#answered(e);
|
|
172
|
+
if (answered !== null) {
|
|
173
|
+
events.push({ type: EventType.TOOL_CALL_RESULT, toolCallId: answered, messageId: this.#id(e), content: Translator.#delivered(e), metadata: Translator.#rowMetadata(e) });
|
|
174
|
+
return events;
|
|
175
|
+
}
|
|
176
|
+
// {§agui-progress-reply} Progress on a message this worker sent is a message from the one who reported it.
|
|
177
|
+
if (typeof e.answering === "string") {
|
|
178
|
+
const id = this.#id(e);
|
|
179
|
+
const text = Translator.#txBody(e.tx);
|
|
180
|
+
events.push({ type: EventType.TEXT_MESSAGE_START, messageId: id, role: "user", ...(typeof e.source === "string" ? { name: e.source } : {}), metadata: Translator.#rowMetadata(e) });
|
|
181
|
+
if (text.length > 0)
|
|
182
|
+
events.push({ type: EventType.TEXT_MESSAGE_CONTENT, messageId: id, delta: text });
|
|
142
183
|
events.push({ type: EventType.TEXT_MESSAGE_END, messageId: id });
|
|
184
|
+
return events;
|
|
143
185
|
}
|
|
144
|
-
|
|
145
|
-
|
|
186
|
+
events.push(ambient);
|
|
187
|
+
return events;
|
|
188
|
+
}
|
|
189
|
+
const id = this.#id(e);
|
|
190
|
+
// {§agui-proposal-resolve} The client holds this gated call: its settlement is a repeated START and END, which the
|
|
191
|
+
// reducer folds onto the call it holds, its arguments untouched, then its answer unless something later answers it
|
|
192
|
+
// ({§agui-call-result}).
|
|
193
|
+
if (this.#held.has(id)) {
|
|
194
|
+
if (e.state === "proposed")
|
|
146
195
|
return events;
|
|
196
|
+
this.#held.delete(id);
|
|
197
|
+
const metadata = Translator.#rowMetadata(e);
|
|
198
|
+
events.push({ type: EventType.TOOL_CALL_START, toolCallId: id, toolCallName: Translator.#callOf(e).name, metadata });
|
|
199
|
+
events.push({ type: EventType.TOOL_CALL_END, toolCallId: id });
|
|
200
|
+
if (!Translator.#answeredLater(e)) {
|
|
201
|
+
events.push({ type: EventType.TOOL_CALL_RESULT, toolCallId: id, messageId: `${id}/result`, content: Translator.#asText(e.rx), metadata });
|
|
147
202
|
}
|
|
203
|
+
return events;
|
|
204
|
+
}
|
|
205
|
+
if (e.op !== null) {
|
|
206
|
+
events.push(...Translator.#readableReasoningEvents(Translator.#reasoningId(e, id), Translator.#claimReasoning(this.#completedReasoning, e.turn_id, e.reasoning)));
|
|
207
|
+
}
|
|
208
|
+
// {§agui-projection} — a reply SEND speaks; so does a WAIT that carries a body (#1003): the card
|
|
209
|
+
// teaches both as the worker's words to the user. A bodiless WAIT is an operation like any other.
|
|
210
|
+
const text = Translator.#txBody(e.tx);
|
|
211
|
+
if (response || Translator.#waitSpeech(e, text)) {
|
|
212
|
+
events.push({ type: EventType.TEXT_MESSAGE_START, messageId: id, role: "assistant", metadata: Translator.#speechMetadata(e) });
|
|
213
|
+
events.push({ type: EventType.TEXT_MESSAGE_CONTENT, messageId: id, delta: text });
|
|
214
|
+
events.push({ type: EventType.TEXT_MESSAGE_END, messageId: id });
|
|
215
|
+
return events;
|
|
148
216
|
}
|
|
149
217
|
if (e.op === null) {
|
|
150
|
-
|
|
151
|
-
if (
|
|
218
|
+
// {§rejected-emission-entry} An emission the daemon rejected builds no part.
|
|
219
|
+
if (Translator.#modelArtifactKind(e.attrs) === null) {
|
|
152
220
|
throw new TypeError("An actionless model-origin row must carry attrs.kind=emissionAttempt.");
|
|
153
221
|
}
|
|
222
|
+
events.push(ambient);
|
|
154
223
|
return events;
|
|
155
224
|
}
|
|
156
|
-
|
|
157
|
-
|
|
225
|
+
// {§agui-parse-refusal} A refusal is the call the worker attempted; one that names no attempt builds no part.
|
|
226
|
+
const call = Translator.#callOf(e);
|
|
227
|
+
if (call === null) {
|
|
228
|
+
events.push(ambient);
|
|
229
|
+
return events;
|
|
230
|
+
}
|
|
231
|
+
const metadata = Translator.#rowMetadata(e);
|
|
232
|
+
events.push({ type: EventType.TOOL_CALL_START, toolCallId: id, toolCallName: call.name, metadata });
|
|
233
|
+
events.push({ type: EventType.TOOL_CALL_ARGS, toolCallId: id, delta: call.args });
|
|
158
234
|
events.push({ type: EventType.TOOL_CALL_END, toolCallId: id });
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
events.push({ type: EventType.TOOL_CALL_RESULT, toolCallId: id, messageId: `${id}/result`, content:
|
|
235
|
+
// {§agui-call-result} A call is answered by its receipt, unless something later answers it.
|
|
236
|
+
if (!Translator.#answeredLater(e)) {
|
|
237
|
+
events.push({ type: EventType.TOOL_CALL_RESULT, toolCallId: id, messageId: `${id}/result`, content: Translator.#asText(e.rx), metadata });
|
|
162
238
|
}
|
|
163
239
|
return events;
|
|
164
240
|
}
|
|
@@ -167,7 +243,7 @@ export default class Translator {
|
|
|
167
243
|
this.#modelWorkerId = event.workerId;
|
|
168
244
|
if (event.workerId !== this.#modelWorkerId)
|
|
169
245
|
return [];
|
|
170
|
-
const messageId =
|
|
246
|
+
const messageId = Translator.#requestReasoningId(event.modelCallId, event.requestSequence);
|
|
171
247
|
if (event.phase === "start") {
|
|
172
248
|
if (this.#activeReasoning.has(event.modelCallId)) {
|
|
173
249
|
throw new TypeError(`Reasoning model call ${event.modelCallId} already started a physical request.`);
|
|
@@ -184,7 +260,10 @@ export default class Translator {
|
|
|
184
260
|
return events;
|
|
185
261
|
}
|
|
186
262
|
const active = this.#activeReasoning.get(event.modelCallId);
|
|
187
|
-
|
|
263
|
+
// {§agui-readable-reasoning} A Run that joined after this request began presents none of it.
|
|
264
|
+
if (active === undefined)
|
|
265
|
+
return [];
|
|
266
|
+
if (active.requestSequence !== event.requestSequence) {
|
|
188
267
|
throw new TypeError(`Reasoning model call ${event.modelCallId} request ${event.requestSequence} emitted ${event.phase} without a start.`);
|
|
189
268
|
}
|
|
190
269
|
if (active.turnId !== event.turnId || active.loopId !== event.loopId) {
|
|
@@ -226,9 +305,10 @@ export default class Translator {
|
|
|
226
305
|
events.push({
|
|
227
306
|
type: EventType.STATE_DELTA,
|
|
228
307
|
delta: [
|
|
229
|
-
{
|
|
308
|
+
// {§loop-lifecycle-vocabulary} — a loop that concluded with any result below 400 completed.
|
|
309
|
+
{ op: "replace", path: "/plurnk/status/lifecycle", value: result.status < 400 ? "completed" : "failed" },
|
|
230
310
|
{ op: "replace", path: "/plurnk/status/loopId", value: n.loopId },
|
|
231
|
-
{ op: "replace", path: "/plurnk/status/
|
|
311
|
+
{ op: "replace", path: "/plurnk/status/terminatedAt", value: n.terminatedAt },
|
|
232
312
|
{ op: "replace", path: "/plurnk/status/waitUntil", value: null },
|
|
233
313
|
{ op: "replace", path: "/budget/curationWeight", value: n.usage.curationWeight },
|
|
234
314
|
{ op: "replace", path: "/budget/curationBudget", value: n.usage.curationBudget },
|
|
@@ -236,13 +316,6 @@ export default class Translator {
|
|
|
236
316
|
{ op: "replace", path: "/budget/contextCapacity", value: n.usage.contextCapacity },
|
|
237
317
|
],
|
|
238
318
|
});
|
|
239
|
-
// Family channel — the full terminal truth the core STATE_DELTA can't hold
|
|
240
|
-
// (loopId, turnIds, physical-request accounting, usage meta, attribution) PLUS the daemon workspaceId, so a
|
|
241
|
-
// plurnk client rebuilds its json record from the stream with ONE schema
|
|
242
|
-
// and no second round-trip. Numbers verbatim
|
|
243
|
-
// ({§agui-numbers-passthrough}). Generic frontends ignore it; the RUN_FINISHED/
|
|
244
|
-
// RUN_ERROR below is their terminal signal.
|
|
245
|
-
events.push({ type: EventType.CUSTOM, name: "plurnk.terminated", value: { ...n, workspaceId: this.#workspaceId } });
|
|
246
319
|
// The standard RAW channel (§475): the provider's NATIVE completion frame rides
|
|
247
320
|
// usage.meta (finish_reason, model, timings, id, …) — AG-UI's RAW is exactly this,
|
|
248
321
|
// a passthrough of an external system's own event with a source tag. Generic
|
|
@@ -250,77 +323,130 @@ export default class Translator {
|
|
|
250
323
|
if (n.usage.meta !== undefined && n.usage.meta !== null && Object.keys(n.usage.meta).length > 0) {
|
|
251
324
|
events.push({ type: EventType.RAW, event: n.usage.meta, source: "provider" });
|
|
252
325
|
}
|
|
253
|
-
|
|
254
|
-
|
|
326
|
+
// {§agui-run-outcome} A loop that completed finishes the Run with its result; one stopped without failing (499)
|
|
327
|
+
// finishes it cancelled; any other failure is the Run's error. Usage is each physical request's own counts, and
|
|
328
|
+
// the end's metadata is the loop's complete terminal record with the daemon's workspaceId, numbers verbatim, so
|
|
329
|
+
// a plurnk client rebuilds its record from the stream alone ({§agui-plurnk-metadata}, {§agui-numbers-passthrough}).
|
|
330
|
+
const usage = Translator.#tokenUsage(n.usage.accounting);
|
|
331
|
+
const metadata = { plurnk: { terminal: { ...n, workspaceId: this.#workspaceId } } };
|
|
332
|
+
if (result.status < 400) {
|
|
333
|
+
events.push({ type: EventType.RUN_FINISHED, threadId: this.#threadId, runId: this.#runId, outcome: { type: "success" }, result, ...usage, metadata });
|
|
334
|
+
}
|
|
335
|
+
else if (result.status === 499) {
|
|
336
|
+
events.push({ type: EventType.RUN_FINISHED, threadId: this.#threadId, runId: this.#runId, outcome: { type: "cancelled" }, ...usage, metadata });
|
|
255
337
|
}
|
|
256
338
|
else {
|
|
257
339
|
events.push({
|
|
258
340
|
type: EventType.RUN_ERROR,
|
|
259
341
|
message: result.problem.detail,
|
|
260
342
|
code: result.problem.type,
|
|
343
|
+
...usage,
|
|
344
|
+
metadata,
|
|
261
345
|
});
|
|
262
346
|
}
|
|
263
347
|
return events;
|
|
264
348
|
}
|
|
265
|
-
// {§agui-
|
|
266
|
-
//
|
|
267
|
-
|
|
349
|
+
// {§agui-run-outcome} AG-UI's token usage of the loop's physical requests, by the standard's own AI SDK mapping and
|
|
350
|
+
// per-model aggregation: the daemon's counts, never its gauge ({§agui-numbers-passthrough}). Unreported counts stay absent.
|
|
351
|
+
static #tokenUsage(accounting) {
|
|
352
|
+
const usage = aggregateTokenUsage(accounting.requests.flatMap(({ provider, model, usage: counts }) => tokenUsageFromAiSdkUsage(counts, { provider, model }) ?? []));
|
|
353
|
+
return usage.length === 0 ? {} : { usage };
|
|
354
|
+
}
|
|
355
|
+
// {§agui-replay} {§agui-snapshot} — the messages as the live projection built them: arrivals, each turn's
|
|
356
|
+
// reasoning, speech, and every other model operation as its tool call and result, under the
|
|
357
|
+
// live identities. Wire rows arrive as the log.read projection (tx parsed).
|
|
268
358
|
replay(entries, currentUser) {
|
|
359
|
+
return Translator.replayAll([{ translator: this, entries }], currentUser);
|
|
360
|
+
}
|
|
361
|
+
// {§agui-snapshot} {§agui-subagents} One conversation from several logs: every row in the order the workspace
|
|
362
|
+
// committed it, each projected by its own worker's translator into one message list.
|
|
363
|
+
static replayAll(parts, currentUser) {
|
|
269
364
|
const messages = [];
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// {§message-arrival} — an arrival replays as a user message; another actor's arrival
|
|
279
|
-
// carries its source as the message name ({§message-causal-source}).
|
|
280
|
-
if (e.origin === "_plurnk" && e.op === "SEND" && Translator.#attrKind(e.attrs) === "message") {
|
|
281
|
-
messages.push({ id: MessageAddress.messageId(e.source, this.#threadId) ?? id,
|
|
282
|
-
role: "user", content: Translator.#txBody(e.tx), ...(typeof e.source === "string" ? { name: e.source } : {}) });
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
const response = Translator.isResponse(e, this.#threadId);
|
|
286
|
-
if (e.origin !== "model" && !response)
|
|
287
|
-
continue;
|
|
288
|
-
const text = Translator.#txBody(e.tx);
|
|
289
|
-
if (response || e.op === "SEND" || e.op === "NOTE" || typeof e.op === "string" && TurnDisposition.isOp(e.op)) {
|
|
290
|
-
const reasoning = Translator.#claimReasoning(deliveredReasoning, e.turn_id, e.reasoning);
|
|
291
|
-
if (reasoning.length > 0)
|
|
292
|
-
messages.push({ id: `${id}/reasoning`, role: "reasoning", content: reasoning });
|
|
293
|
-
}
|
|
294
|
-
if (response) {
|
|
295
|
-
const message = { id, role: "assistant", content: text };
|
|
296
|
-
messages.push(message);
|
|
297
|
-
}
|
|
298
|
-
if (e.op === null) {
|
|
299
|
-
if (Translator.#modelArtifactKind(e.attrs) === null) {
|
|
300
|
-
throw new TypeError("An actionless model-origin replay row must carry attrs.kind=emissionAttempt.");
|
|
301
|
-
}
|
|
302
|
-
}
|
|
365
|
+
const order = (e) => typeof e.id === "number" ? e.id : Number.MAX_SAFE_INTEGER;
|
|
366
|
+
const rows = parts.flatMap(({ translator, entries }) => entries.map((e) => ({ translator, e })))
|
|
367
|
+
.toSorted((left, right) => order(left.e) - order(right.e));
|
|
368
|
+
const delivered = new Map();
|
|
369
|
+
for (const { translator, e } of rows) {
|
|
370
|
+
const reasoning = delivered.get(translator) ?? new Map();
|
|
371
|
+
delivered.set(translator, reasoning);
|
|
372
|
+
translator.#replayRow(e, messages, reasoning);
|
|
303
373
|
}
|
|
304
374
|
if (currentUser !== undefined && !messages.some(({ id }) => id === currentUser.id)) {
|
|
305
375
|
messages.push(currentUser);
|
|
306
376
|
}
|
|
307
377
|
return [{ type: EventType.MESSAGES_SNAPSHOT, messages }];
|
|
308
378
|
}
|
|
309
|
-
|
|
379
|
+
#replayRow(e, messages, deliveredReasoning) {
|
|
380
|
+
const attributed = (message) => this.#subagent === null ? message : { ...message, subagentRunId: this.#subagent.runId };
|
|
381
|
+
const id = this.#id(e);
|
|
382
|
+
const metadata = Translator.#rowMetadata(e);
|
|
383
|
+
const arrival = this.#arrival(e);
|
|
384
|
+
if (arrival !== null) {
|
|
385
|
+
messages.push(attributed({ id: arrival.id, role: "user", content: arrival.content, ...(arrival.name === undefined ? {} : { name: arrival.name }), metadata }));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const response = Translator.isResponse(e, this.#replyThreads);
|
|
389
|
+
if (e.origin !== "model" && !response) {
|
|
390
|
+
const answered = this.#answered(e);
|
|
391
|
+
if (answered !== null)
|
|
392
|
+
Translator.#placeResult(messages, attributed({ id, role: "tool", toolCallId: answered, content: Translator.#delivered(e), metadata }));
|
|
393
|
+
// {§agui-progress-reply}
|
|
394
|
+
else if (typeof e.answering === "string") {
|
|
395
|
+
messages.push(attributed({ id, role: "user", content: Translator.#txBody(e.tx), ...(typeof e.source === "string" ? { name: e.source } : {}), metadata }));
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (typeof e.op !== "string") {
|
|
400
|
+
if (Translator.#modelArtifactKind(e.attrs) === null) {
|
|
401
|
+
throw new TypeError("An actionless model-origin replay row must carry attrs.kind=emissionAttempt.");
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const reasoning = Translator.#claimReasoning(deliveredReasoning, e.turn_id, e.reasoning);
|
|
406
|
+
if (reasoning.length > 0)
|
|
407
|
+
messages.push(attributed({ id: Translator.#reasoningId(e, id), role: "reasoning", content: reasoning }));
|
|
408
|
+
const text = Translator.#txBody(e.tx);
|
|
409
|
+
if (response || Translator.#waitSpeech(e, text)) {
|
|
410
|
+
messages.push(attributed({ id, role: "assistant", content: text, metadata: Translator.#speechMetadata(e) }));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const call = Translator.#callOf(e);
|
|
414
|
+
if (call === null)
|
|
415
|
+
return;
|
|
416
|
+
messages.push(attributed({ id, role: "assistant", toolCalls: [{ id, type: "function", function: { name: call.name, arguments: call.args }, metadata }] }));
|
|
417
|
+
if (e.state === "proposed")
|
|
418
|
+
this.#held.add(id);
|
|
419
|
+
if (Translator.#answeredLater(e))
|
|
420
|
+
return;
|
|
421
|
+
Translator.#placeResult(messages, attributed({ id: `${id}/result`, role: "tool", toolCallId: id, content: Translator.#asText(e.rx), metadata }));
|
|
422
|
+
}
|
|
423
|
+
notice(notice, loopId = null) {
|
|
310
424
|
const diagnostic = { type: EventType.CUSTOM, name: "plurnk.notice", value: notice };
|
|
311
425
|
const value = notice;
|
|
312
|
-
// {§loop-status-notice} — lifecycle
|
|
426
|
+
// {§loop-status-notice} {§agui-status-timing} — the loop's lifecycle with its identity, packet count,
|
|
427
|
+
// stamps and deadline in one delta, never a diagnostic or chat message.
|
|
313
428
|
if (value.source === "engine:lifecycle" && value.kind === "loop_status") {
|
|
314
429
|
if (value.status !== 100 && value.status !== 102 && value.status !== 202)
|
|
315
430
|
throw new TypeError("A loop_status notice carries 100, 102 or 202.");
|
|
316
431
|
if (value.waitUntil !== null && (typeof value.waitUntil !== "number" || !Number.isFinite(value.waitUntil) || value.waitUntil < 0)) {
|
|
317
432
|
throw new TypeError("A loop_status notice carries a nullable Unix-millisecond waitUntil.");
|
|
318
433
|
}
|
|
434
|
+
if (!Number.isSafeInteger(value.packetCount) || value.packetCount < 0)
|
|
435
|
+
throw new TypeError("A loop_status notice carries its loop's packet count.");
|
|
436
|
+
if (value.claimedAt !== null && (!Number.isSafeInteger(value.claimedAt) || value.claimedAt <= 0)) {
|
|
437
|
+
throw new TypeError("A loop_status notice carries a nullable Unix-millisecond claimedAt.");
|
|
438
|
+
}
|
|
439
|
+
if (loopId === null || loopId <= 0)
|
|
440
|
+
throw new TypeError("A loop_status notice names its loop.");
|
|
319
441
|
return [{
|
|
320
442
|
type: EventType.STATE_DELTA,
|
|
321
443
|
delta: [
|
|
322
444
|
{ op: "replace", path: "/plurnk/status/lifecycle", value: lifecycleOfLoopStatus(value.status) },
|
|
445
|
+
{ op: "replace", path: "/plurnk/status/loopId", value: loopId },
|
|
446
|
+
{ op: "replace", path: "/plurnk/status/packetCount", value: value.packetCount },
|
|
323
447
|
{ op: "replace", path: "/plurnk/status/waitUntil", value: value.status === 202 ? value.waitUntil : null },
|
|
448
|
+
{ op: "replace", path: "/plurnk/status/claimedAt", value: value.claimedAt },
|
|
449
|
+
{ op: "replace", path: "/plurnk/status/terminatedAt", value: null },
|
|
324
450
|
],
|
|
325
451
|
}];
|
|
326
452
|
}
|
|
@@ -328,7 +454,7 @@ export default class Translator {
|
|
|
328
454
|
return [diagnostic];
|
|
329
455
|
const events = [{
|
|
330
456
|
type: EventType.STATE_DELTA,
|
|
331
|
-
delta: [{ op: "replace", path: "/plurnk/
|
|
457
|
+
delta: [{ op: "replace", path: "/plurnk/workspace/activity", value: derivationActivity(notice) }],
|
|
332
458
|
}];
|
|
333
459
|
if (value.level === "error" || value.level === "warn")
|
|
334
460
|
events.push(diagnostic);
|
|
@@ -384,10 +510,9 @@ export default class Translator {
|
|
|
384
510
|
}
|
|
385
511
|
// {§agui-readable-reasoning} Durable reasoning precedes the turn's first
|
|
386
512
|
// speech or note projection when live delivery has not already supplied it.
|
|
387
|
-
static #readableReasoningEvents(
|
|
513
|
+
static #readableReasoningEvents(messageId, value) {
|
|
388
514
|
if (typeof value !== "string" || value.length === 0)
|
|
389
515
|
return [];
|
|
390
|
-
const messageId = `${sendId}/reasoning`;
|
|
391
516
|
return [
|
|
392
517
|
{ type: EventType.REASONING_START, messageId },
|
|
393
518
|
{ type: EventType.REASONING_MESSAGE_START, messageId, role: "reasoning" },
|
|
@@ -411,7 +536,7 @@ export default class Translator {
|
|
|
411
536
|
return kind === "emissionAttempt" ? kind : null;
|
|
412
537
|
}
|
|
413
538
|
// {§loop-response-messages}: share admission with reattach orientation.
|
|
414
|
-
static isResponse(entry,
|
|
539
|
+
static isResponse(entry, replyThreads) {
|
|
415
540
|
if (entry.op !== "SEND" && entry.op !== "KILL")
|
|
416
541
|
return false;
|
|
417
542
|
const deliveredReply = Translator.#attrKind(entry.attrs) === "reply";
|
|
@@ -430,7 +555,7 @@ export default class Translator {
|
|
|
430
555
|
const answers = rx.answers;
|
|
431
556
|
if (!Array.isArray(answers))
|
|
432
557
|
return false;
|
|
433
|
-
return answers.some((address) => MessageAddress.messageId(address, threadId) !== null);
|
|
558
|
+
return answers.some((address) => replyThreads.some((threadId) => MessageAddress.messageId(address, threadId) !== null));
|
|
434
559
|
}
|
|
435
560
|
// The model-facing textual statement body out of the tx. The real
|
|
436
561
|
// wire ships tx PARSED (an object); a string is tolerated and parsed for robustness.
|
|
@@ -459,6 +584,9 @@ export default class Translator {
|
|
|
459
584
|
return body.raw;
|
|
460
585
|
return "";
|
|
461
586
|
}
|
|
587
|
+
static #waitSpeech(e, text) {
|
|
588
|
+
return e.op === "WAIT" && e.origin === "model" && text.trim().length > 0;
|
|
589
|
+
}
|
|
462
590
|
// A wire value (object or string) as display text.
|
|
463
591
|
static #asText(v) {
|
|
464
592
|
if (typeof v === "string")
|
|
@@ -467,14 +595,159 @@ export default class Translator {
|
|
|
467
595
|
return "";
|
|
468
596
|
return JSON.stringify(v);
|
|
469
597
|
}
|
|
470
|
-
//
|
|
471
|
-
|
|
598
|
+
// {§agui-snapshot} A row's message identity is its log coordinate L/T/S ({§methods-log-coordinate}).
|
|
599
|
+
static idOf(e) {
|
|
600
|
+
const { loop_seq: loop, turn_seq: turn, sequence } = e;
|
|
601
|
+
if (!Number.isSafeInteger(loop) || !Number.isSafeInteger(turn) || !Number.isSafeInteger(sequence)) {
|
|
602
|
+
throw new TypeError("A log row carries its loop, turn and sequence ordinals.");
|
|
603
|
+
}
|
|
604
|
+
return `${loop}/${turn}/${sequence}`;
|
|
605
|
+
}
|
|
606
|
+
// {§agui-readable-reasoning} Reasoning is the request that streamed it, live and on replay alike; reasoning a
|
|
607
|
+
// FORK inherited was streamed by no request of this worker and is its row's.
|
|
608
|
+
static #requestReasoningId(modelCallId, requestSequence) {
|
|
609
|
+
return `model-call-${modelCallId}/request-${requestSequence}/reasoning`;
|
|
610
|
+
}
|
|
611
|
+
static #reasoningId(e, id) {
|
|
612
|
+
const { reasoning_model_call_id: call, reasoning_request_sequence: request } = e;
|
|
613
|
+
return typeof call === "number" && typeof request === "number" ? Translator.#requestReasoningId(call, request) : `${id}/reasoning`;
|
|
614
|
+
}
|
|
615
|
+
// {§message-arrival} An arrival is a user message, live and on replay alike. One a client on this thread sent is that
|
|
616
|
+
// client's own, under its message id and as it sent it; any other is identified by its coordinate and named by its
|
|
617
|
+
// source ({§message-causal-source}).
|
|
618
|
+
#arrival(e) {
|
|
619
|
+
if (e.origin !== "_plurnk" || e.op !== "SEND" || Translator.#attrKind(e.attrs) !== "message")
|
|
620
|
+
return null;
|
|
621
|
+
const messageId = MessageAddress.messageId(e.source, this.#threadId);
|
|
622
|
+
if (messageId !== null)
|
|
623
|
+
return { id: messageId, content: Translator.#txBody(e.tx) };
|
|
624
|
+
return {
|
|
625
|
+
id: this.#id(e),
|
|
626
|
+
content: Translator.#txBody(e.tx),
|
|
627
|
+
...(typeof e.source === "string" ? { name: e.source } : {}),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
// {§agui-call-result} A call its receipt does not answer: a gated one awaits its own settlement ({§proposal-202-pauses}),
|
|
631
|
+
// and one that opened an obligation the delivery that discharges it.
|
|
632
|
+
static #answeredLater(e) {
|
|
633
|
+
return e.state === "proposed" || Translator.#opensObligation(e);
|
|
634
|
+
}
|
|
635
|
+
// {§agui-call-result} A call that opened an obligation is answered by the delivery that discharges it, never by its
|
|
636
|
+
// receipt, which rides the row: an execution its stream, a message the reply or conclusion ({§worker-obligations}).
|
|
637
|
+
static #opensObligation(e) {
|
|
638
|
+
const attrs = typeof e.attrs === "string" ? JSON.parse(e.attrs) : e.attrs;
|
|
639
|
+
const opened = attrs !== null && typeof attrs === "object" ? attrs : {};
|
|
640
|
+
return (typeof opened.stream === "string" || typeof opened.message === "string") && typeof e.status_rx === "number" && e.status_rx < 400;
|
|
641
|
+
}
|
|
642
|
+
// {§agui-call-result} What the worker received for its call: a reply's words, as an arrival's ({§agui-snapshot}),
|
|
643
|
+
// or an observation's result.
|
|
644
|
+
static #delivered(e) {
|
|
645
|
+
return e.op === "SEND" ? Translator.#txBody(e.tx) : Translator.#asText(e.rx);
|
|
646
|
+
}
|
|
647
|
+
// {§agui-call-result} The call a discharging row answers: the operation it names ({§methods-invocation}), in the
|
|
648
|
+
// namespace of the worker whose log holds both.
|
|
649
|
+
#answered(e) {
|
|
650
|
+
if (e.invocation === undefined)
|
|
651
|
+
return null;
|
|
652
|
+
const coordinate = Translator.callOf(e.invocation);
|
|
653
|
+
return this.#subagent === null ? coordinate : `${Translator.subagentIds(this.#subagent.worker)}#${coordinate}`;
|
|
654
|
+
}
|
|
655
|
+
// {§agui-plurnk-metadata} What the row that builds a message, tool call or result holds beyond the standard fields: its
|
|
656
|
+
// durable id, log coordinate, worker, origin, operation, source, address, signal, status, attrs and state, the same live
|
|
657
|
+
// and on snapshot.
|
|
658
|
+
static #rowMetadata(e) {
|
|
659
|
+
return { plurnk: { id: e.id, coordinate: Translator.idOf(e), worker: e.worker_id, origin: e.origin, op: e.op, source: e.source,
|
|
660
|
+
address: Translator.addressOf(e), signal: e.signal, status: e.status_rx, attrs: e.attrs, state: e.state } };
|
|
661
|
+
}
|
|
662
|
+
// {§agui-plurnk-metadata} The address a row recorded: `scheme://host[:port]path[?query][#fragment]`, or the bare path a
|
|
663
|
+
// file target records, with its channel; null when the row named none.
|
|
664
|
+
static addressOf(e) {
|
|
665
|
+
const text = (value) => typeof value === "string" ? value : null;
|
|
666
|
+
const pathname = text(e.pathname);
|
|
667
|
+
if (pathname === null)
|
|
668
|
+
return null;
|
|
669
|
+
const fragment = text(e.fragment) === null ? "" : `#${text(e.fragment)}`;
|
|
670
|
+
const scheme = text(e.scheme);
|
|
671
|
+
if (scheme === null)
|
|
672
|
+
return `${pathname}${fragment}`;
|
|
673
|
+
const port = typeof e.port === "number" ? `:${e.port}` : "";
|
|
674
|
+
const query = text(e.query) === null ? "" : `?${text(e.query)}`;
|
|
675
|
+
return `${scheme}://${text(e.hostname) ?? ""}${port}${pathname}${query}${fragment}`;
|
|
676
|
+
}
|
|
677
|
+
// {§agui-plurnk-metadata} Speech holds the body; its heading's aside and its receipt have no field in a text message.
|
|
678
|
+
static #speechMetadata(e) {
|
|
679
|
+
const statement = Translator.#parsed(e.tx);
|
|
680
|
+
const aside = statement !== null && typeof statement === "object" ? statement.aside ?? null : null;
|
|
681
|
+
return { plurnk: { ...Translator.#rowMetadata(e).plurnk, aside, receipt: Translator.#parsed(e.rx) } };
|
|
682
|
+
}
|
|
683
|
+
// A wire field as its value: the live wire ships it parsed, a replayed one may carry its JSON text.
|
|
684
|
+
static #parsed(value) {
|
|
685
|
+
if (value === undefined || value === "")
|
|
686
|
+
return null;
|
|
687
|
+
if (typeof value !== "string")
|
|
688
|
+
return value;
|
|
689
|
+
try {
|
|
690
|
+
return JSON.parse(value);
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return value;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// {§methods-invocation} The coordinate of the operation a log address names in its own worker's log.
|
|
697
|
+
static callOf(invocation) {
|
|
698
|
+
const match = typeof invocation === "string" ? /^log:\/\/\/(\d+)\/(\d+)\/(\d+)\/[^/]+$/u.exec(invocation) : null;
|
|
699
|
+
if (match === null)
|
|
700
|
+
throw new TypeError(`An invocation is an operation's log address: ${String(invocation)}`);
|
|
701
|
+
return `${match[1]}/${match[2]}/${match[3]}`;
|
|
702
|
+
}
|
|
703
|
+
// {§agui-subagents} A delegated worker's rows are identified within its own namespace, so two logs never collide.
|
|
704
|
+
static subagentIds(worker) {
|
|
705
|
+
return `worker://${worker}`;
|
|
706
|
+
}
|
|
707
|
+
#id(e) {
|
|
708
|
+
const coordinate = Translator.idOf(e);
|
|
709
|
+
return this.#subagent === null ? coordinate : `${Translator.subagentIds(this.#subagent.worker)}#${coordinate}`;
|
|
710
|
+
}
|
|
711
|
+
// {§agui-subagents} Everything a delegation's conversation produces is attributed to its subagent run, its metadata
|
|
712
|
+
// included; only the events that describe a Run itself belong to no subagent.
|
|
713
|
+
#attributed(events) {
|
|
714
|
+
const subagent = this.#subagent;
|
|
715
|
+
if (subagent === null)
|
|
716
|
+
return events;
|
|
717
|
+
return events.map((event) => Translator.#runScoped.has(event.type) ? event : { ...event, subagentRunId: subagent.runId });
|
|
718
|
+
}
|
|
719
|
+
static #runScoped = new Set([EventType.RUN_STARTED, EventType.RUN_FINISHED, EventType.RUN_ERROR, EventType.MESSAGES_SNAPSHOT]);
|
|
720
|
+
// {§agui-snapshot} A result sits where the official reducer puts one that arrives live: after its call and the results
|
|
721
|
+
// already beside it, or last when the call is not in view.
|
|
722
|
+
static #placeResult(messages, result) {
|
|
723
|
+
const call = messages.findIndex((message) => message.role === "assistant" && message.toolCalls?.some(({ id }) => id === result.toolCallId) === true);
|
|
724
|
+
if (call === -1) {
|
|
725
|
+
messages.push(result);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
let at = call + 1;
|
|
729
|
+
while (at < messages.length && messages[at].role === "tool")
|
|
730
|
+
at++;
|
|
731
|
+
messages.splice(at, 0, result);
|
|
732
|
+
}
|
|
733
|
+
// {§agui-parse-refusal} The call a model operation row is: its operation and its statement, or for a parse refusal the
|
|
734
|
+
// statement the worker attempted, its verb and its text as written; null for a refusal that names no attempt.
|
|
735
|
+
static #callOf(e) {
|
|
736
|
+
if (e.op !== "error")
|
|
737
|
+
return { name: e.op, args: Translator.#argsFor(e) };
|
|
738
|
+
const attempt = Translator.#parsed(e.attrs)?.attempt;
|
|
739
|
+
return typeof attempt?.verb === "string" && typeof attempt.text === "string"
|
|
740
|
+
? { name: attempt.verb, args: JSON.stringify({ text: attempt.text }) } : null;
|
|
741
|
+
}
|
|
742
|
+
// {§agui-projection} A tool call's arguments are the operation as authored: its durable statement, every operand
|
|
743
|
+
// exactly as parsed ({§log-sensitive-request-evidence}), without the verb that is the tool's name or the position it
|
|
744
|
+
// was written at. AG-UI streams arguments as deltas; a dispatched operation is atomic, so they arrive as one.
|
|
472
745
|
static #argsFor(e) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
746
|
+
const statement = typeof e.tx === "string" && e.tx.length > 0 ? JSON.parse(e.tx) : e.tx;
|
|
747
|
+
if (statement === null || typeof statement !== "object" || Array.isArray(statement))
|
|
748
|
+
return JSON.stringify({});
|
|
749
|
+
const { op: _op, position: _position, ...operands } = statement;
|
|
750
|
+
return JSON.stringify(operands);
|
|
478
751
|
}
|
|
479
752
|
}
|
|
480
753
|
//# sourceMappingURL=Translator.js.map
|