@xenosystem/blocks 0.6.0 → 0.7.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/dist/agent/host-source.d.ts +118 -0
- package/dist/agent/host-source.js +228 -0
- package/dist/agent/index.d.ts +568 -0
- package/dist/agent/index.js +1439 -0
- package/dist/auth/index.d.ts +399 -3
- package/dist/auth/index.js +620 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types-C5T3uVwd.d.ts +425 -0
- package/package.json +9 -3
|
@@ -0,0 +1,1439 @@
|
|
|
1
|
+
import "../chunk-2KG3PWR4.js";
|
|
2
|
+
|
|
3
|
+
// src/agent/agent/panel.ts
|
|
4
|
+
import { createElement } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
import {
|
|
7
|
+
bindConfig,
|
|
8
|
+
configBool,
|
|
9
|
+
configString,
|
|
10
|
+
isRecord
|
|
11
|
+
} from "@xenosystem/panel-sdk";
|
|
12
|
+
|
|
13
|
+
// src/agent/agent/unified-diff.ts
|
|
14
|
+
var HUNK_HEADER = /^@@+ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@+(.*)$/;
|
|
15
|
+
var FILE_HEADER = /^(diff --git |index |--- |\+\+\+ |new file mode |deleted file mode |similarity index |rename (from|to) |old mode |new mode |Binary files )/;
|
|
16
|
+
function hash(text) {
|
|
17
|
+
let h = 2166136261;
|
|
18
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
19
|
+
h ^= text.charCodeAt(i);
|
|
20
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return h.toString(16).padStart(8, "0");
|
|
23
|
+
}
|
|
24
|
+
function parseUnifiedDiff(path, diff) {
|
|
25
|
+
const hunks = [];
|
|
26
|
+
const ids = /* @__PURE__ */ new Set();
|
|
27
|
+
let current = null;
|
|
28
|
+
let oldLine = 0;
|
|
29
|
+
let newLine = 0;
|
|
30
|
+
const close = () => {
|
|
31
|
+
if (!current) return;
|
|
32
|
+
const body = current.lines.map((l) => `${l.kind[0]}${l.text}`).join("\n");
|
|
33
|
+
let id = `${path}#${hash(`${current.header ?? ""}
|
|
34
|
+
${body}`)}`;
|
|
35
|
+
if (ids.has(id)) {
|
|
36
|
+
id = `${id}-${ids.size}`;
|
|
37
|
+
}
|
|
38
|
+
ids.add(id);
|
|
39
|
+
current.id = id;
|
|
40
|
+
hunks.push(current);
|
|
41
|
+
current = null;
|
|
42
|
+
};
|
|
43
|
+
for (const raw of diff.split("\n")) {
|
|
44
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
45
|
+
const header = HUNK_HEADER.exec(line);
|
|
46
|
+
if (header) {
|
|
47
|
+
close();
|
|
48
|
+
oldLine = Number(header[1]);
|
|
49
|
+
newLine = Number(header[3]);
|
|
50
|
+
current = {
|
|
51
|
+
id: "",
|
|
52
|
+
oldStart: oldLine,
|
|
53
|
+
// ⚠️ An absent count means 1, per the format. Defaulting it to 0 would render a
|
|
54
|
+
// single-line hunk as an empty one — present in the list, empty on screen.
|
|
55
|
+
oldLines: header[2] === void 0 ? 1 : Number(header[2]),
|
|
56
|
+
newStart: newLine,
|
|
57
|
+
newLines: header[4] === void 0 ? 1 : Number(header[4]),
|
|
58
|
+
lines: [],
|
|
59
|
+
header: line
|
|
60
|
+
};
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!current) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (line.startsWith("\\")) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line.startsWith("+")) {
|
|
70
|
+
current.lines.push({ kind: "added", newLineNumber: newLine, text: line.slice(1) });
|
|
71
|
+
newLine += 1;
|
|
72
|
+
} else if (line.startsWith("-")) {
|
|
73
|
+
current.lines.push({ kind: "removed", oldLineNumber: oldLine, text: line.slice(1) });
|
|
74
|
+
oldLine += 1;
|
|
75
|
+
} else if (line.startsWith(" ")) {
|
|
76
|
+
current.lines.push({
|
|
77
|
+
kind: "context",
|
|
78
|
+
oldLineNumber: oldLine,
|
|
79
|
+
newLineNumber: newLine,
|
|
80
|
+
text: line.slice(1)
|
|
81
|
+
});
|
|
82
|
+
oldLine += 1;
|
|
83
|
+
newLine += 1;
|
|
84
|
+
} else if (line === "") {
|
|
85
|
+
if (current.lines.filter((l) => l.kind !== "added").length >= current.oldLines) continue;
|
|
86
|
+
current.lines.push({ kind: "context", oldLineNumber: oldLine, newLineNumber: newLine, text: "" });
|
|
87
|
+
oldLine += 1;
|
|
88
|
+
newLine += 1;
|
|
89
|
+
} else if (FILE_HEADER.test(line)) {
|
|
90
|
+
close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
close();
|
|
94
|
+
return hunks;
|
|
95
|
+
}
|
|
96
|
+
function countChanges(hunks) {
|
|
97
|
+
let additions = 0;
|
|
98
|
+
let deletions = 0;
|
|
99
|
+
for (const hunk of hunks) {
|
|
100
|
+
for (const line of hunk.lines) {
|
|
101
|
+
if (line.kind === "added") additions += 1;
|
|
102
|
+
else if (line.kind === "removed") deletions += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { additions, deletions };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/agent/agent/projections.ts
|
|
109
|
+
var AGENT_FANOUT_PORTS = [
|
|
110
|
+
"runs",
|
|
111
|
+
"records",
|
|
112
|
+
"elicitations",
|
|
113
|
+
"withdraw",
|
|
114
|
+
"diffModel",
|
|
115
|
+
"terminalData",
|
|
116
|
+
"terminalSession"
|
|
117
|
+
];
|
|
118
|
+
var RUN_STATUS = {
|
|
119
|
+
idle: "requested",
|
|
120
|
+
queued: "queued",
|
|
121
|
+
running: "running",
|
|
122
|
+
// The two that had no run-panel equivalent until the panel gained them, and the reason it did.
|
|
123
|
+
"waiting-for-user": "awaiting-decision",
|
|
124
|
+
"waiting-for-permission": "awaiting-permission",
|
|
125
|
+
succeeded: "succeeded",
|
|
126
|
+
failed: "failed",
|
|
127
|
+
cancelled: "cancelled"
|
|
128
|
+
};
|
|
129
|
+
function turnStatusToRunStatus(state) {
|
|
130
|
+
return RUN_STATUS[state];
|
|
131
|
+
}
|
|
132
|
+
function levelFor(role) {
|
|
133
|
+
return role === "error" ? "error" : "info";
|
|
134
|
+
}
|
|
135
|
+
var AgentProjector = class {
|
|
136
|
+
now;
|
|
137
|
+
/** turnId → sessionId. The address a run action comes back on. */
|
|
138
|
+
turnSession = /* @__PURE__ */ new Map();
|
|
139
|
+
/** callId → the step path inside its run, so a nested call can be patched in place. */
|
|
140
|
+
callPath = /* @__PURE__ */ new Map();
|
|
141
|
+
/** messageId → seen, so the second event for a message patches rather than re-appends. */
|
|
142
|
+
messages = /* @__PURE__ */ new Set();
|
|
143
|
+
/** askId → sessionId. */
|
|
144
|
+
askSession = /* @__PURE__ */ new Map();
|
|
145
|
+
/** terminalId → sessionId. */
|
|
146
|
+
terminalSession = /* @__PURE__ */ new Map();
|
|
147
|
+
/**
|
|
148
|
+
* File path → the change set it belongs to.
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ **Replaced wholesale on every `patch` event, because the diff panel's model is too.** The
|
|
151
|
+
* map and the thing it describes move together, so a decision can never address a change set the
|
|
152
|
+
* user is no longer looking at.
|
|
153
|
+
*/
|
|
154
|
+
patchPaths = /* @__PURE__ */ new Map();
|
|
155
|
+
/**
|
|
156
|
+
* Answers that addressed nothing known, since the last clear.
|
|
157
|
+
*
|
|
158
|
+
* Surfaced rather than swallowed, for the reason `xeno.core.console` surfaces its dropped
|
|
159
|
+
* patches: a mis-wired return path and a dead one look identical from the outside, and this is
|
|
160
|
+
* the one number that tells them apart.
|
|
161
|
+
*/
|
|
162
|
+
dropped = 0;
|
|
163
|
+
constructor(options = {}) {
|
|
164
|
+
this.now = options.now ?? (() => Date.now());
|
|
165
|
+
}
|
|
166
|
+
/** How many answers were dropped for want of a correlation. */
|
|
167
|
+
get droppedAnswers() {
|
|
168
|
+
return this.dropped;
|
|
169
|
+
}
|
|
170
|
+
/** Forget everything. Used when the host replaces or clears the session index. */
|
|
171
|
+
reset() {
|
|
172
|
+
this.turnSession.clear();
|
|
173
|
+
this.callPath.clear();
|
|
174
|
+
this.messages.clear();
|
|
175
|
+
this.askSession.clear();
|
|
176
|
+
this.terminalSession.clear();
|
|
177
|
+
this.patchPaths = /* @__PURE__ */ new Map();
|
|
178
|
+
this.dropped = 0;
|
|
179
|
+
}
|
|
180
|
+
/** How many asks are still outstanding. */
|
|
181
|
+
get pendingAsks() {
|
|
182
|
+
return this.askSession.size;
|
|
183
|
+
}
|
|
184
|
+
/** Which session an ask belongs to, or `undefined`. */
|
|
185
|
+
sessionForAsk(askId) {
|
|
186
|
+
return this.askSession.get(askId);
|
|
187
|
+
}
|
|
188
|
+
/** Which session a turn belongs to, or `undefined`. */
|
|
189
|
+
sessionForTurn(turnId) {
|
|
190
|
+
return this.turnSession.get(turnId);
|
|
191
|
+
}
|
|
192
|
+
/** Which session a terminal belongs to, or `undefined`. */
|
|
193
|
+
sessionForTerminal(terminalId) {
|
|
194
|
+
return this.terminalSession.get(terminalId);
|
|
195
|
+
}
|
|
196
|
+
/** Which change set a reviewed file belongs to, or `undefined`. */
|
|
197
|
+
patchForPath(path) {
|
|
198
|
+
return this.patchPaths.get(path);
|
|
199
|
+
}
|
|
200
|
+
/** Record that an ask has been settled, so a second answer for it is dropped rather than resent. */
|
|
201
|
+
settleAsk(askId) {
|
|
202
|
+
this.askSession.delete(askId);
|
|
203
|
+
}
|
|
204
|
+
/** Count an answer that could not be correlated. */
|
|
205
|
+
dropAnswer() {
|
|
206
|
+
this.dropped += 1;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Project one event.
|
|
210
|
+
*
|
|
211
|
+
* @param event - The event.
|
|
212
|
+
* @returns Zero or more emissions, in the order they must be delivered. **Order matters**: a
|
|
213
|
+
* console record must be appended before it is patched, and a step must be announced before it
|
|
214
|
+
* is addressed — both panels drop an update for something they do not hold.
|
|
215
|
+
*/
|
|
216
|
+
project(event) {
|
|
217
|
+
switch (event.type) {
|
|
218
|
+
case "session":
|
|
219
|
+
return [];
|
|
220
|
+
case "session-closed": {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
case "turn": {
|
|
224
|
+
const fresh = !this.turnSession.has(event.turnId);
|
|
225
|
+
this.turnSession.set(event.turnId, event.sessionId);
|
|
226
|
+
const status = turnStatusToRunStatus(event.state);
|
|
227
|
+
if (fresh) {
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
portId: "runs",
|
|
231
|
+
value: {
|
|
232
|
+
append: [
|
|
233
|
+
{
|
|
234
|
+
id: event.turnId,
|
|
235
|
+
label: event.label ?? event.turnId,
|
|
236
|
+
status,
|
|
237
|
+
// The run panel sorts on this and requires it. A turn with no reported start
|
|
238
|
+
// is stamped on arrival rather than defaulted to the epoch, which would file
|
|
239
|
+
// it at the bottom of the list — exactly where nobody looks for the thing they
|
|
240
|
+
// just asked for.
|
|
241
|
+
startedAt: event.startedAt ?? this.now(),
|
|
242
|
+
endedAt: event.endedAt,
|
|
243
|
+
error: event.error,
|
|
244
|
+
source: event.sessionId
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
portId: "runs",
|
|
254
|
+
value: {
|
|
255
|
+
patch: {
|
|
256
|
+
[event.turnId]: {
|
|
257
|
+
status,
|
|
258
|
+
...event.label === void 0 ? {} : { label: event.label },
|
|
259
|
+
...event.endedAt === void 0 ? {} : { endedAt: event.endedAt },
|
|
260
|
+
...event.error === void 0 ? {} : { error: event.error }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
case "tool": {
|
|
268
|
+
this.turnSession.set(event.turnId, event.sessionId);
|
|
269
|
+
const known = this.callPath.get(event.callId);
|
|
270
|
+
const status = turnStatusToRunStatus(event.state);
|
|
271
|
+
if (!known) {
|
|
272
|
+
const parentPath = event.parentCallId ? this.callPath.get(event.parentCallId) : void 0;
|
|
273
|
+
const path = [...parentPath ?? [], event.callId];
|
|
274
|
+
this.callPath.set(event.callId, path);
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
portId: "runs",
|
|
278
|
+
value: {
|
|
279
|
+
appendSteps: [
|
|
280
|
+
{
|
|
281
|
+
runId: event.turnId,
|
|
282
|
+
path: parentPath,
|
|
283
|
+
steps: [
|
|
284
|
+
{
|
|
285
|
+
id: event.callId,
|
|
286
|
+
label: event.label ?? event.callId,
|
|
287
|
+
status,
|
|
288
|
+
startedAt: event.startedAt,
|
|
289
|
+
endedAt: event.endedAt,
|
|
290
|
+
progress: event.progress,
|
|
291
|
+
detail: event.detail,
|
|
292
|
+
error: event.error
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
return [
|
|
302
|
+
{
|
|
303
|
+
portId: "runs",
|
|
304
|
+
value: {
|
|
305
|
+
patchSteps: [
|
|
306
|
+
{
|
|
307
|
+
runId: event.turnId,
|
|
308
|
+
path: known,
|
|
309
|
+
patch: {
|
|
310
|
+
status,
|
|
311
|
+
...event.label === void 0 ? {} : { label: event.label },
|
|
312
|
+
...event.progress === void 0 ? {} : { progress: event.progress },
|
|
313
|
+
...event.detail === void 0 ? {} : { detail: event.detail },
|
|
314
|
+
...event.endedAt === void 0 ? {} : { endedAt: event.endedAt },
|
|
315
|
+
...event.error === void 0 ? {} : { error: event.error }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
case "message": {
|
|
324
|
+
const fresh = !this.messages.has(event.messageId);
|
|
325
|
+
const delta = {};
|
|
326
|
+
if (fresh) {
|
|
327
|
+
this.messages.add(event.messageId);
|
|
328
|
+
delta.append = [
|
|
329
|
+
{
|
|
330
|
+
id: event.messageId,
|
|
331
|
+
ts: event.ts ?? this.now(),
|
|
332
|
+
level: levelFor(event.role),
|
|
333
|
+
message: event.text ?? "",
|
|
334
|
+
source: event.sessionId,
|
|
335
|
+
scope: event.role
|
|
336
|
+
}
|
|
337
|
+
];
|
|
338
|
+
if (event.append !== void 0) {
|
|
339
|
+
delta.patch = { [event.messageId]: { appendMessage: event.append } };
|
|
340
|
+
}
|
|
341
|
+
return [{ portId: "records", value: delta }];
|
|
342
|
+
}
|
|
343
|
+
const patch = {};
|
|
344
|
+
if (event.text !== void 0) patch.message = event.text;
|
|
345
|
+
if (event.append !== void 0) patch.appendMessage = event.append;
|
|
346
|
+
if (Object.keys(patch).length === 0) return [];
|
|
347
|
+
return [{ portId: "records", value: { patch: { [event.messageId]: patch } } }];
|
|
348
|
+
}
|
|
349
|
+
case "ask": {
|
|
350
|
+
this.askSession.set(event.ask.id, event.sessionId);
|
|
351
|
+
return [{ portId: "elicitations", value: event.ask }];
|
|
352
|
+
}
|
|
353
|
+
case "ask-withdraw": {
|
|
354
|
+
this.askSession.delete(event.askId);
|
|
355
|
+
return [{ portId: "withdraw", value: event.askId }];
|
|
356
|
+
}
|
|
357
|
+
case "patch": {
|
|
358
|
+
const paths = /* @__PURE__ */ new Map();
|
|
359
|
+
const files = event.files.map((file) => {
|
|
360
|
+
paths.set(file.path, { sessionId: event.sessionId, patchId: event.patchId });
|
|
361
|
+
return this.projectFile(file);
|
|
362
|
+
});
|
|
363
|
+
this.patchPaths = paths;
|
|
364
|
+
return [
|
|
365
|
+
{
|
|
366
|
+
portId: "diffModel",
|
|
367
|
+
value: {
|
|
368
|
+
files,
|
|
369
|
+
status: "ready",
|
|
370
|
+
oldLabel: event.oldLabel,
|
|
371
|
+
newLabel: event.newLabel
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
case "terminal": {
|
|
377
|
+
this.terminalSession.set(event.terminalId, event.sessionId);
|
|
378
|
+
const out = [];
|
|
379
|
+
if (event.session !== void 0) {
|
|
380
|
+
out.push({ portId: "terminalSession", value: event.session });
|
|
381
|
+
}
|
|
382
|
+
if (event.data !== void 0) {
|
|
383
|
+
out.push({ portId: "terminalData", value: { id: event.terminalId, data: event.data } });
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
default: {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** One file of a change set → the diff panel's file shape. */
|
|
393
|
+
projectFile(file) {
|
|
394
|
+
if (file.omitted !== void 0 || file.diff === void 0) {
|
|
395
|
+
return {
|
|
396
|
+
path: file.path,
|
|
397
|
+
oldPath: file.oldPath,
|
|
398
|
+
status: file.status,
|
|
399
|
+
language: file.language,
|
|
400
|
+
hunks: [],
|
|
401
|
+
omitted: file.omitted ?? "unreadable"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const hunks = parseUnifiedDiff(file.path, file.diff);
|
|
405
|
+
if (hunks.length === 0) {
|
|
406
|
+
return {
|
|
407
|
+
path: file.path,
|
|
408
|
+
oldPath: file.oldPath,
|
|
409
|
+
status: file.status,
|
|
410
|
+
language: file.language,
|
|
411
|
+
hunks: [],
|
|
412
|
+
omitted: "unreadable"
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const { additions, deletions } = countChanges(hunks);
|
|
416
|
+
return {
|
|
417
|
+
path: file.path,
|
|
418
|
+
oldPath: file.oldPath,
|
|
419
|
+
status: file.status,
|
|
420
|
+
language: file.language,
|
|
421
|
+
hunks,
|
|
422
|
+
additions,
|
|
423
|
+
deletions
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// src/agent/agent/types.ts
|
|
429
|
+
function isBusy(turn) {
|
|
430
|
+
return turn === "queued" || turn === "running";
|
|
431
|
+
}
|
|
432
|
+
function isWaitingOnUser(turn) {
|
|
433
|
+
return turn === "waiting-for-user" || turn === "waiting-for-permission";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/agent/agent/controller.ts
|
|
437
|
+
var AgentController = class {
|
|
438
|
+
host;
|
|
439
|
+
projector;
|
|
440
|
+
sessions = [];
|
|
441
|
+
activeSessionId = null;
|
|
442
|
+
connection = "idle";
|
|
443
|
+
connectionMessage = null;
|
|
444
|
+
catalog = { providers: [] };
|
|
445
|
+
draft = "";
|
|
446
|
+
/** Last delta revision applied. A delta whose rev has not advanced is dropped. */
|
|
447
|
+
rev = -1;
|
|
448
|
+
listeners = /* @__PURE__ */ new Set();
|
|
449
|
+
/**
|
|
450
|
+
* Memoized view state.
|
|
451
|
+
*
|
|
452
|
+
* `useSyncExternalStore` compares by identity and loops forever if `getState` returns a fresh
|
|
453
|
+
* object each call. Every mutation clears this; nothing else may.
|
|
454
|
+
*/
|
|
455
|
+
snapshot = null;
|
|
456
|
+
constructor(options) {
|
|
457
|
+
this.host = options.host;
|
|
458
|
+
this.projector = new AgentProjector({ now: options.now });
|
|
459
|
+
}
|
|
460
|
+
/* ── Subscription ──────────────────────────────────────────────────────── */
|
|
461
|
+
subscribe = (listener) => {
|
|
462
|
+
this.listeners.add(listener);
|
|
463
|
+
return () => this.listeners.delete(listener);
|
|
464
|
+
};
|
|
465
|
+
getState = () => {
|
|
466
|
+
if (!this.snapshot) {
|
|
467
|
+
const active = this.sessions.find((s) => s.id === this.activeSessionId) ?? null;
|
|
468
|
+
const turn = active?.turn ?? "idle";
|
|
469
|
+
const busy = isBusy(turn);
|
|
470
|
+
const activeProvider = this.catalog.providers.find((p) => p.id === (active?.providerId ?? this.catalog.activeProviderId)) ?? null;
|
|
471
|
+
this.snapshot = {
|
|
472
|
+
sessions: this.sessions,
|
|
473
|
+
active,
|
|
474
|
+
turn,
|
|
475
|
+
connection: this.connection,
|
|
476
|
+
// A turn's own failure sentence wins over the connection's: it is the more specific fact,
|
|
477
|
+
// and it is the one the user is looking at.
|
|
478
|
+
message: (turn === "failed" ? active?.message : null) ?? this.connectionMessage,
|
|
479
|
+
providers: this.catalog.providers,
|
|
480
|
+
activeProvider,
|
|
481
|
+
activeModelId: active?.modelId ?? this.catalog.activeModelId ?? null,
|
|
482
|
+
draft: this.draft,
|
|
483
|
+
canSubmit: this.canSubmit(),
|
|
484
|
+
busy,
|
|
485
|
+
pendingAsks: this.projector.pendingAsks
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return this.snapshot;
|
|
489
|
+
};
|
|
490
|
+
notify() {
|
|
491
|
+
this.snapshot = null;
|
|
492
|
+
for (const listener of this.listeners) listener();
|
|
493
|
+
}
|
|
494
|
+
/* ── Fan-out ───────────────────────────────────────────────────────────── */
|
|
495
|
+
fanOut(emissions) {
|
|
496
|
+
for (const emission of emissions) this.host.emit(emission.portId, emission.value);
|
|
497
|
+
}
|
|
498
|
+
/** Emit the one thing the host acts on. */
|
|
499
|
+
intent(intent) {
|
|
500
|
+
this.host.emit("intent", intent);
|
|
501
|
+
}
|
|
502
|
+
/* ── Host-pushed state ─────────────────────────────────────────────────── */
|
|
503
|
+
/**
|
|
504
|
+
* Apply a session delta.
|
|
505
|
+
*
|
|
506
|
+
* The host is authoritative: sessions are upserted, never merged cleverly, and the panel keeps no
|
|
507
|
+
* "better" older value. A panel that second-guesses its host is a second source of truth.
|
|
508
|
+
*
|
|
509
|
+
* @param delta - The delta.
|
|
510
|
+
*/
|
|
511
|
+
ingest(delta) {
|
|
512
|
+
if (typeof delta.rev === "number") {
|
|
513
|
+
if (delta.rev <= this.rev) return;
|
|
514
|
+
this.rev = delta.rev;
|
|
515
|
+
}
|
|
516
|
+
if (delta.clear) {
|
|
517
|
+
this.sessions = [];
|
|
518
|
+
this.activeSessionId = null;
|
|
519
|
+
this.projector.reset();
|
|
520
|
+
this.notify();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (delta.replace) {
|
|
524
|
+
this.sessions = [...delta.replace].sort(orderSessions);
|
|
525
|
+
this.projector.reset();
|
|
526
|
+
if (this.activeSessionId && !this.sessions.some((s) => s.id === this.activeSessionId)) {
|
|
527
|
+
this.activeSessionId = null;
|
|
528
|
+
}
|
|
529
|
+
this.notify();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
let touched = false;
|
|
533
|
+
for (const event of delta.events ?? []) {
|
|
534
|
+
if (event.type === "session") {
|
|
535
|
+
this.upsertSession(event.session);
|
|
536
|
+
touched = true;
|
|
537
|
+
} else if (event.type === "session-closed") {
|
|
538
|
+
this.sessions = this.sessions.filter((s) => s.id !== event.sessionId);
|
|
539
|
+
if (this.activeSessionId === event.sessionId) this.activeSessionId = null;
|
|
540
|
+
touched = true;
|
|
541
|
+
} else if (event.type === "turn") {
|
|
542
|
+
touched = this.setTurnState(event.sessionId, event.state) || touched;
|
|
543
|
+
} else if (event.type === "ask" || event.type === "ask-withdraw") {
|
|
544
|
+
touched = true;
|
|
545
|
+
}
|
|
546
|
+
this.fanOut(this.projector.project(event));
|
|
547
|
+
}
|
|
548
|
+
if (!this.activeSessionId && this.sessions.length > 0) {
|
|
549
|
+
this.activeSessionId = this.sessions[0].id;
|
|
550
|
+
touched = true;
|
|
551
|
+
}
|
|
552
|
+
if (touched) this.notify();
|
|
553
|
+
}
|
|
554
|
+
upsertSession(next) {
|
|
555
|
+
const at = this.sessions.findIndex((s) => s.id === next.id);
|
|
556
|
+
if (at < 0) {
|
|
557
|
+
this.sessions = [...this.sessions, next].sort(orderSessions);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const defined = Object.fromEntries(Object.entries(next).filter(([, v]) => v !== void 0));
|
|
561
|
+
const merged = { ...this.sessions[at], ...defined };
|
|
562
|
+
const sessions = [...this.sessions];
|
|
563
|
+
sessions[at] = merged;
|
|
564
|
+
this.sessions = sessions.sort(orderSessions);
|
|
565
|
+
}
|
|
566
|
+
setTurnState(sessionId, turn) {
|
|
567
|
+
const at = this.sessions.findIndex((s) => s.id === sessionId);
|
|
568
|
+
if (at < 0) return false;
|
|
569
|
+
if (this.sessions[at].turn === turn) return false;
|
|
570
|
+
const sessions = [...this.sessions];
|
|
571
|
+
sessions[at] = { ...sessions[at], turn };
|
|
572
|
+
this.sessions = sessions;
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
/** Install the provider catalogue. The host decides what is selectable; the panel renders it. */
|
|
576
|
+
setCatalog(catalog) {
|
|
577
|
+
this.catalog = { ...catalog, providers: catalog.providers ?? [] };
|
|
578
|
+
this.notify();
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Report the connection out of band.
|
|
582
|
+
*
|
|
583
|
+
* Separate from the session delta so a host never has to express "not connected" as an empty
|
|
584
|
+
* session list — the distinction `xeno.core.runs` and `xeno.core.diff` both draw, and for the
|
|
585
|
+
* same reason: zero sessions means "there are none" only when someone looked.
|
|
586
|
+
*/
|
|
587
|
+
setStatus(status, message) {
|
|
588
|
+
this.connection = status;
|
|
589
|
+
this.connectionMessage = message ?? null;
|
|
590
|
+
this.notify();
|
|
591
|
+
}
|
|
592
|
+
/* ── The composer ──────────────────────────────────────────────────────── */
|
|
593
|
+
setDraft(text) {
|
|
594
|
+
this.draft = text;
|
|
595
|
+
this.notify();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Would {@link submit} do anything?
|
|
599
|
+
*
|
|
600
|
+
* 🔴 The view's `disabled` derives from THIS, so an enabled button cannot be a button that
|
|
601
|
+
* refuses. The affordance and the guard are one predicate.
|
|
602
|
+
*
|
|
603
|
+
* ⚠️ `connection === 'idle'` does NOT block. Idle means *the host has never reported a
|
|
604
|
+
* connection state*, and inventing a blocker out of silence would make the panel unusable in
|
|
605
|
+
* every host that does not wire the optional `status` port. Silence is not a refusal; the host
|
|
606
|
+
* refuses if it must.
|
|
607
|
+
*/
|
|
608
|
+
canSubmit() {
|
|
609
|
+
if (!this.activeSessionId) return false;
|
|
610
|
+
if (this.draft.trim() === "") return false;
|
|
611
|
+
if (this.connection === "connecting" || this.connection === "failed") return false;
|
|
612
|
+
const active = this.sessions.find((s) => s.id === this.activeSessionId);
|
|
613
|
+
return !isBusy(active?.turn ?? "idle");
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Ask the host to run the draft.
|
|
617
|
+
*
|
|
618
|
+
* 🔴 **The draft is NOT cleared here.** The panel does not know the turn started — the host may
|
|
619
|
+
* refuse, the provider may be unavailable, a permission check may reject it. Clearing on emit
|
|
620
|
+
* would show success before anything settled, which is a claim the panel cannot back; the draft
|
|
621
|
+
* clears when the host reports a `running` turn. (`panel-template`'s rule, applied to the one
|
|
622
|
+
* place in this catalog where a user would notice losing their text.)
|
|
623
|
+
*
|
|
624
|
+
* @returns Whether an intent was emitted.
|
|
625
|
+
*/
|
|
626
|
+
submit() {
|
|
627
|
+
if (!this.canSubmit()) return false;
|
|
628
|
+
const active = this.sessions.find((s) => s.id === this.activeSessionId);
|
|
629
|
+
this.intent({
|
|
630
|
+
type: "prompt",
|
|
631
|
+
sessionId: active.id,
|
|
632
|
+
text: this.draft,
|
|
633
|
+
providerId: active.providerId ?? this.catalog.activeProviderId,
|
|
634
|
+
modelId: active.modelId ?? this.catalog.activeModelId
|
|
635
|
+
});
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Ask the host to steer the running turn.
|
|
640
|
+
*
|
|
641
|
+
* ⚠️ **Steering lands at the agent's next decision point, not instantly.** That was measured
|
|
642
|
+
* against real Claude Code and recorded in the ADE spec, and it is why this is a separate verb
|
|
643
|
+
* from {@link submit} rather than "submit while busy": the two have different latencies and
|
|
644
|
+
* different failure modes, and a control that hid the difference would be lying about which one
|
|
645
|
+
* the user got.
|
|
646
|
+
*
|
|
647
|
+
* @returns Whether an intent was emitted.
|
|
648
|
+
*/
|
|
649
|
+
steer() {
|
|
650
|
+
const active = this.sessions.find((s) => s.id === this.activeSessionId);
|
|
651
|
+
if (!active || this.draft.trim() === "") return false;
|
|
652
|
+
if (!isBusy(active.turn ?? "idle")) return false;
|
|
653
|
+
this.intent({ type: "steer", sessionId: active.id, text: this.draft });
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
/** Ask the host to stop the running turn. */
|
|
657
|
+
cancel() {
|
|
658
|
+
const active = this.sessions.find((s) => s.id === this.activeSessionId);
|
|
659
|
+
if (!active) return false;
|
|
660
|
+
this.intent({ type: "cancel", sessionId: active.id });
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
/** Show another session. Panel-local AND an intent — the host may need to attach to it. */
|
|
664
|
+
selectSession(sessionId) {
|
|
665
|
+
if (!this.sessions.some((s) => s.id === sessionId)) return false;
|
|
666
|
+
this.activeSessionId = sessionId;
|
|
667
|
+
this.notify();
|
|
668
|
+
this.intent({ type: "select-session", sessionId });
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
/** Ask the host to start a session. The panel does not create one — it has no way to. */
|
|
672
|
+
newSession(providerId, modelId) {
|
|
673
|
+
this.intent({
|
|
674
|
+
type: "new-session",
|
|
675
|
+
providerId: providerId ?? this.catalog.activeProviderId,
|
|
676
|
+
modelId: modelId ?? this.catalog.activeModelId
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Ask the host to switch the active session's agent.
|
|
681
|
+
*
|
|
682
|
+
* Refused for a provider the host declared unavailable: the panel must not offer what the host
|
|
683
|
+
* will not honour, which is the rule `xeno.core.consent` applies to durations.
|
|
684
|
+
*
|
|
685
|
+
* @returns Whether an intent was emitted.
|
|
686
|
+
*/
|
|
687
|
+
selectProvider(providerId, modelId) {
|
|
688
|
+
const provider = this.catalog.providers.find((p) => p.id === providerId);
|
|
689
|
+
if (!provider || provider.unavailable !== void 0) return false;
|
|
690
|
+
if (!this.activeSessionId) return false;
|
|
691
|
+
this.intent({ type: "select-provider", sessionId: this.activeSessionId, providerId, modelId });
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
/* ── Answers coming back from the canonical panels ─────────────────────── */
|
|
695
|
+
/**
|
|
696
|
+
* A consent decision or an elicitation result, from `xeno.core.consent`.
|
|
697
|
+
*
|
|
698
|
+
* 🔴 **Dropped when the id correlates to nothing.** A decision routed to a guessed session
|
|
699
|
+
* applies a human's approval to work they never saw. Both of consent's output ports arrive here
|
|
700
|
+
* because both are answers to an ask this panel minted, and the panel forwards each verbatim —
|
|
701
|
+
* it does not read `decision`, and must not: interpreting an allow/deny would make this a second
|
|
702
|
+
* place where a grant is decided.
|
|
703
|
+
*
|
|
704
|
+
* @param answer - The decision or result, verbatim.
|
|
705
|
+
* @returns Whether an intent was emitted.
|
|
706
|
+
*/
|
|
707
|
+
answer(answer) {
|
|
708
|
+
if (!answer || typeof answer !== "object") return false;
|
|
709
|
+
const id = answer.id;
|
|
710
|
+
if (typeof id !== "string") return false;
|
|
711
|
+
const sessionId = this.projector.sessionForAsk(id);
|
|
712
|
+
if (!sessionId) {
|
|
713
|
+
this.projector.dropAnswer();
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
this.projector.settleAsk(id);
|
|
717
|
+
this.intent({ type: "answer", sessionId, askId: id, answer });
|
|
718
|
+
this.notify();
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* A hunk decision, from `xeno.core.diff`.
|
|
723
|
+
*
|
|
724
|
+
* @param decision - `{path, hunkId, action, comment?}`, verbatim.
|
|
725
|
+
* @returns Whether an intent was emitted.
|
|
726
|
+
*/
|
|
727
|
+
patchDecision(decision) {
|
|
728
|
+
if (!decision || typeof decision !== "object") return false;
|
|
729
|
+
const path = decision.path;
|
|
730
|
+
if (typeof path !== "string") return false;
|
|
731
|
+
const owner = this.projector.patchForPath(path);
|
|
732
|
+
if (!owner) {
|
|
733
|
+
this.projector.dropAnswer();
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
this.intent({
|
|
737
|
+
type: "patch-decision",
|
|
738
|
+
sessionId: owner.sessionId,
|
|
739
|
+
patchId: owner.patchId,
|
|
740
|
+
decision
|
|
741
|
+
});
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* A run action, from `xeno.core.runs`.
|
|
746
|
+
*
|
|
747
|
+
* ⚠️ The targeted step is read from `stepPath`'s LAST element, falling back to `stepId`. Both are
|
|
748
|
+
* emitted together by that panel and mean the same thing at depth 1; only the path survives
|
|
749
|
+
* nesting, which is the case an agent's subagent calls produce.
|
|
750
|
+
*
|
|
751
|
+
* @param action - `{runId, action, stepId?, stepPath?}`, verbatim.
|
|
752
|
+
* @returns Whether an intent was emitted.
|
|
753
|
+
*/
|
|
754
|
+
runAction(action) {
|
|
755
|
+
if (!action || typeof action !== "object") return false;
|
|
756
|
+
const record = action;
|
|
757
|
+
if (typeof record.runId !== "string") return false;
|
|
758
|
+
if (record.action !== "cancel" && record.action !== "retry") return false;
|
|
759
|
+
const sessionId = this.projector.sessionForTurn(record.runId);
|
|
760
|
+
if (!sessionId) {
|
|
761
|
+
this.projector.dropAnswer();
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
const path = Array.isArray(record.stepPath) ? record.stepPath : void 0;
|
|
765
|
+
const last = path && path.length > 0 ? path[path.length - 1] : void 0;
|
|
766
|
+
const callId = typeof last === "string" ? last : typeof record.stepId === "string" ? record.stepId : void 0;
|
|
767
|
+
this.intent({ type: "run-action", sessionId, turnId: record.runId, action: record.action, callId });
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* A terminal intent, from `xeno.core.terminal`.
|
|
772
|
+
*
|
|
773
|
+
* @param intent - The terminal panel's intent, verbatim.
|
|
774
|
+
* @returns Whether an intent was emitted.
|
|
775
|
+
*/
|
|
776
|
+
terminalIntent(intent) {
|
|
777
|
+
if (!intent || typeof intent !== "object") return false;
|
|
778
|
+
const id = intent.id;
|
|
779
|
+
if (typeof id !== "string") return false;
|
|
780
|
+
const sessionId = this.projector.sessionForTerminal(id);
|
|
781
|
+
if (!sessionId) {
|
|
782
|
+
this.projector.dropAnswer();
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
this.intent({ type: "terminal", sessionId, terminal: intent });
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
/** Answers that could not be correlated, since the last reset. Surfaced, never swallowed. */
|
|
789
|
+
get droppedAnswers() {
|
|
790
|
+
return this.projector.droppedAnswers;
|
|
791
|
+
}
|
|
792
|
+
/* ── Lifecycle ─────────────────────────────────────────────────────────── */
|
|
793
|
+
/**
|
|
794
|
+
* Serialize.
|
|
795
|
+
*
|
|
796
|
+
* 🔴 **Preferences plus the unsent draft, and nothing else.** No transcript, no run list, no
|
|
797
|
+
* session content — that is host state which has moved on, and `.xapp` is a plain JSON file that
|
|
798
|
+
* may contain nothing sensitive.
|
|
799
|
+
*/
|
|
800
|
+
serialize() {
|
|
801
|
+
return { activeSessionId: this.activeSessionId, draft: this.draft };
|
|
802
|
+
}
|
|
803
|
+
/** Restore preferences. Sessions are NOT restored; the host re-pushes them. */
|
|
804
|
+
deserialize(state) {
|
|
805
|
+
if (!state || typeof state !== "object") return;
|
|
806
|
+
const s = state;
|
|
807
|
+
if (typeof s.activeSessionId === "string" || s.activeSessionId === null) {
|
|
808
|
+
this.activeSessionId = s.activeSessionId ?? null;
|
|
809
|
+
}
|
|
810
|
+
if (typeof s.draft === "string") this.draft = s.draft;
|
|
811
|
+
this.notify();
|
|
812
|
+
}
|
|
813
|
+
/** Release everything. */
|
|
814
|
+
dispose() {
|
|
815
|
+
this.listeners.clear();
|
|
816
|
+
this.sessions = [];
|
|
817
|
+
this.projector.reset();
|
|
818
|
+
this.snapshot = null;
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
function orderSessions(a, b) {
|
|
822
|
+
const at = a.createdAt;
|
|
823
|
+
const bt = b.createdAt;
|
|
824
|
+
if (at === void 0 && bt === void 0) return 0;
|
|
825
|
+
if (at === void 0) return 1;
|
|
826
|
+
if (bt === void 0) return -1;
|
|
827
|
+
return bt - at;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/agent/agent/manifest.ts
|
|
831
|
+
import { WELL_KNOWN_PORT_SCHEMAS } from "@xenosystem/panel-sdk";
|
|
832
|
+
var AGENT_PANEL_ID = "xeno.core.agent";
|
|
833
|
+
var AGENT_SESSION_SCHEMA = "xeno.agentsession@1";
|
|
834
|
+
var AGENT_CATALOG_SCHEMA = "xeno.agentcatalog@1";
|
|
835
|
+
var AGENT_INTENT_SCHEMA = "xeno.agentintent@1";
|
|
836
|
+
var CONSENT_ELICITATION_SCHEMA = "xeno.elicitation@1";
|
|
837
|
+
var CONSENT_ELICITATION_RESULT_SCHEMA = "xeno.elicitationresult@1";
|
|
838
|
+
var agentManifest = {
|
|
839
|
+
id: AGENT_PANEL_ID,
|
|
840
|
+
version: "0.1.0",
|
|
841
|
+
title: "Agent",
|
|
842
|
+
icon: "bot",
|
|
843
|
+
description: "One agent surface for every XENO app: the composer, the lane and provider picker, and the session switcher \u2014 plus the translation from one session-event stream into the canonical panels that render the tool timeline, the log, the permission queue, the diff and the terminal. Holds no agent logic and runs nothing.",
|
|
844
|
+
defaultSlot: "right",
|
|
845
|
+
inputs: [
|
|
846
|
+
{
|
|
847
|
+
id: "session",
|
|
848
|
+
name: "Session Events",
|
|
849
|
+
type: "object",
|
|
850
|
+
schema: AGENT_SESSION_SCHEMA,
|
|
851
|
+
description: "A delta: {rev?, events?, replace?, clear?}. Events are session/turn/message/tool/ask/patch/terminal \u2014 see the package README for the shape and INTEGRATION.md for the host obligation. Fan-in: one wire per agent host.",
|
|
852
|
+
multiple: true
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: "catalog",
|
|
856
|
+
name: "Provider Catalog",
|
|
857
|
+
type: "object",
|
|
858
|
+
schema: AGENT_CATALOG_SCHEMA,
|
|
859
|
+
description: '{providers[], activeProviderId?, activeModelId?}. A provider carries a `lane` (cloud|local|acp) and an `unavailable` SENTENCE \u2014 never a boolean, because "why can I not pick this?" is the whole question a greyed-out row raises.',
|
|
860
|
+
multiple: false
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
id: "status",
|
|
864
|
+
name: "Connection",
|
|
865
|
+
type: "object",
|
|
866
|
+
description: '{status, message?} \u2014 idle | connecting | ready | failed, out of band, so a host never has to express "not connected" as an empty session list.',
|
|
867
|
+
multiple: false
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
id: "decision",
|
|
871
|
+
name: "Consent Decision",
|
|
872
|
+
type: "object",
|
|
873
|
+
schema: WELL_KNOWN_PORT_SCHEMAS.CONSENT_DECISION,
|
|
874
|
+
description: "The consent panel's `decision` output, routed back so it can be correlated to the session that asked. Forwarded VERBATIM \u2014 this panel never reads allow/deny, because interpreting it would make this a second place where a grant is decided.",
|
|
875
|
+
multiple: true
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
id: "result",
|
|
879
|
+
name: "Elicitation Result",
|
|
880
|
+
type: "object",
|
|
881
|
+
schema: CONSENT_ELICITATION_RESULT_SCHEMA,
|
|
882
|
+
description: "The consent panel's `result` output \u2014 a text/path/choice answer, or a cancellation carrying no value at all. Correlated and forwarded verbatim.",
|
|
883
|
+
multiple: true
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
id: "diffDecision",
|
|
887
|
+
name: "Hunk Decision",
|
|
888
|
+
type: "object",
|
|
889
|
+
schema: WELL_KNOWN_PORT_SCHEMAS.DIFF_DECISION_INTENT,
|
|
890
|
+
description: "The diff panel's `decision` output. \u{1F534} Dropped when its path belongs to no change set currently on screen \u2014 forwarding it would apply a review of one proposal to another.",
|
|
891
|
+
multiple: true
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
id: "runAction",
|
|
895
|
+
name: "Run Action",
|
|
896
|
+
type: "object",
|
|
897
|
+
description: "The runs panel's `action` output: {runId, action, stepId?, stepPath?}. The run id IS the turn id; the targeted tool call is `stepPath`'s last element.",
|
|
898
|
+
multiple: true
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
id: "terminalIntent",
|
|
902
|
+
name: "Terminal Intent",
|
|
903
|
+
type: "object",
|
|
904
|
+
schema: WELL_KNOWN_PORT_SCHEMAS.TERMINAL_INTENT,
|
|
905
|
+
description: "The terminal panel's `intent` output, correlated to the session that owns the instance and forwarded verbatim.",
|
|
906
|
+
multiple: true
|
|
907
|
+
}
|
|
908
|
+
],
|
|
909
|
+
outputs: [
|
|
910
|
+
{
|
|
911
|
+
id: "intent",
|
|
912
|
+
name: "Intent",
|
|
913
|
+
type: "object",
|
|
914
|
+
schema: AGENT_INTENT_SCHEMA,
|
|
915
|
+
description: "The ONE thing the host acts on: prompt | cancel | steer | select-session | new-session | select-provider | answer | patch-decision | run-action | terminal. Every answer arriving from a canonical panel leaves here carrying the session id that panel could not know."
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
id: "runs",
|
|
919
|
+
name: "Runs Delta",
|
|
920
|
+
type: "object",
|
|
921
|
+
description: "For `xeno.core.runs` \u2192 `runs`. A turn is a RUN; a tool call is a STEP, nested by `parentCallId`, addressed by path. Progress travels as `patchSteps`, never as a re-push of the run."
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
id: "records",
|
|
925
|
+
name: "Log Delta",
|
|
926
|
+
type: "object",
|
|
927
|
+
description: "For `xeno.core.console` \u2192 `records`. A message opens with `append` and streams with `patch.appendMessage` \u2014 the update-by-id path a token-streamed reply has no other representation in."
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
id: "elicitations",
|
|
931
|
+
name: "Elicitations",
|
|
932
|
+
type: "object",
|
|
933
|
+
schema: CONSENT_ELICITATION_SCHEMA,
|
|
934
|
+
description: "For `xeno.core.consent` \u2192 `elicitations`. Forwarded verbatim; whether an ask can be rendered is that panel\u2019s decision, not this one\u2019s."
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
id: "withdraw",
|
|
938
|
+
name: "Withdraw",
|
|
939
|
+
type: "string",
|
|
940
|
+
description: "For `xeno.core.consent` \u2192 `withdraw`. An ask the agent no longer needs answered, so the queue is not left holding a question nobody is waiting on."
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
id: "diffModel",
|
|
944
|
+
name: "Diff Model",
|
|
945
|
+
type: "object",
|
|
946
|
+
schema: WELL_KNOWN_PORT_SCHEMAS.DIFF_MODEL,
|
|
947
|
+
description: 'For `xeno.core.diff` \u2192 `model`. Unified-diff text is parsed HERE (the panel ships no engine by charter). A file with no readable diff is reported `omitted: "unreadable"`, never as identical.'
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
id: "terminalData",
|
|
951
|
+
name: "Terminal Data",
|
|
952
|
+
type: "object",
|
|
953
|
+
schema: WELL_KNOWN_PORT_SCHEMAS.TERMINAL_DATA,
|
|
954
|
+
description: "For `xeno.core.terminal` \u2192 `data`. Raw bytes, escape sequences intact \u2014 never pre-parsed, because the emulator is the parser."
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
id: "terminalSession",
|
|
958
|
+
name: "Terminal Session",
|
|
959
|
+
type: "object",
|
|
960
|
+
description: "For `xeno.core.terminal` \u2192 `session`. Emitted BEFORE any data for that instance: a write into a terminal the panel has not been told about has nowhere to land."
|
|
961
|
+
}
|
|
962
|
+
],
|
|
963
|
+
commands: [
|
|
964
|
+
{
|
|
965
|
+
id: "get_sessions",
|
|
966
|
+
title: "Get Sessions",
|
|
967
|
+
description: 'Return every session with its turn state, newest first, plus the connection status. The STATUS travels with the count so a caller cannot read 0 as "none" when the truth is "not connected".',
|
|
968
|
+
parameters: {}
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
id: "get_status",
|
|
972
|
+
title: "Get Status",
|
|
973
|
+
description: "Connection, active session, turn state, pending asks, and how many panel answers were dropped for want of a correlation.",
|
|
974
|
+
parameters: {}
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
id: "select_session",
|
|
978
|
+
title: "Select Session",
|
|
979
|
+
description: "Show another session. Returns the active session id, or null.",
|
|
980
|
+
parameters: { sessionId: { type: "string", description: "Session id", required: true } }
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
id: "cancel_turn",
|
|
984
|
+
title: "Cancel Turn",
|
|
985
|
+
description: "Ask the host to stop the active session\u2019s turn. Stopping only ever reduces what happens, which is why it is the one mutating verb here.",
|
|
986
|
+
parameters: {}
|
|
987
|
+
}
|
|
988
|
+
// 🔴 WHAT IS DELIBERATELY ABSENT, and why each one is a refusal rather than an omission:
|
|
989
|
+
//
|
|
990
|
+
// - **No `submit_prompt`.** An agent that can start turns can start them forever — an unbounded
|
|
991
|
+
// self-driving loop with a billing meter attached and no human decision point anywhere in it.
|
|
992
|
+
// Every other agent-facing verb in this catalog costs at most one wrong row; this one costs
|
|
993
|
+
// credits until somebody notices.
|
|
994
|
+
// - **No `set_draft`.** Writing into the human's composer is putting words in their mouth: the
|
|
995
|
+
// text sits where they expect their own to be, and the next keystroke sends it.
|
|
996
|
+
// - **No `answer_ask`.** An agent answering its own permission prompt is precisely what
|
|
997
|
+
// `xeno.core.consent` exists to prevent, and routing around that panel would defeat it just
|
|
998
|
+
// as thoroughly as editing it.
|
|
999
|
+
// - **No patch accept/reject.** Same reasoning as `xeno.core.diff`: the review step exists to
|
|
1000
|
+
// check the agent, so the agent must not be able to operate the thing that checks it.
|
|
1001
|
+
],
|
|
1002
|
+
config: [
|
|
1003
|
+
{
|
|
1004
|
+
key: "submitOnEnter",
|
|
1005
|
+
label: "Enter sends",
|
|
1006
|
+
type: "boolean",
|
|
1007
|
+
defaultValue: true,
|
|
1008
|
+
description: "Enter submits and Shift+Enter inserts a newline. Off swaps the two."
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
key: "showLane",
|
|
1012
|
+
label: "Show lane",
|
|
1013
|
+
type: "boolean",
|
|
1014
|
+
defaultValue: true,
|
|
1015
|
+
description: "Show whether the turn runs in the cloud, locally, or through ACP. Where the code and the credits go is not a detail."
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
key: "showSessions",
|
|
1019
|
+
label: "Show session switcher",
|
|
1020
|
+
type: "boolean",
|
|
1021
|
+
defaultValue: true,
|
|
1022
|
+
description: "Hide it in a host that owns session selection in its own chrome."
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
key: "emptyHint",
|
|
1026
|
+
label: "Empty hint",
|
|
1027
|
+
type: "text",
|
|
1028
|
+
description: "Say what would produce a session, not that there are none.",
|
|
1029
|
+
placeholder: "Start a session to talk to an agent."
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1032
|
+
capabilities: ["storage.local"],
|
|
1033
|
+
sdk: "^1.1.0"
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// src/agent/agent/react/AgentPanelView.tsx
|
|
1037
|
+
import { useSyncExternalStore } from "react";
|
|
1038
|
+
import {
|
|
1039
|
+
Badge,
|
|
1040
|
+
EmptyState,
|
|
1041
|
+
Row,
|
|
1042
|
+
RowList,
|
|
1043
|
+
ScrollArea,
|
|
1044
|
+
StatusBadge,
|
|
1045
|
+
StatusBar,
|
|
1046
|
+
TextButton,
|
|
1047
|
+
Toolbar,
|
|
1048
|
+
ToolbarGroup
|
|
1049
|
+
} from "@xenosystem/workbench/primitives/react";
|
|
1050
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
1051
|
+
var LANE_LABEL = {
|
|
1052
|
+
cloud: "Cloud",
|
|
1053
|
+
local: "Local",
|
|
1054
|
+
acp: "ACP"
|
|
1055
|
+
};
|
|
1056
|
+
var TURN = {
|
|
1057
|
+
idle: { label: "Idle", tone: "neutral" },
|
|
1058
|
+
queued: { label: "Queued", tone: "neutral" },
|
|
1059
|
+
running: { label: "Working", tone: "info" },
|
|
1060
|
+
"waiting-for-user": { label: "Needs an answer", tone: "warning" },
|
|
1061
|
+
"waiting-for-permission": { label: "Needs permission", tone: "warning" },
|
|
1062
|
+
succeeded: { label: "Done", tone: "success" },
|
|
1063
|
+
failed: { label: "Failed", tone: "error" },
|
|
1064
|
+
cancelled: { label: "Cancelled", tone: "neutral" }
|
|
1065
|
+
};
|
|
1066
|
+
var COLUMN = { display: "flex", flexDirection: "column", height: "100%", minHeight: 0 };
|
|
1067
|
+
var COMPOSER = {
|
|
1068
|
+
width: "100%",
|
|
1069
|
+
minHeight: 64,
|
|
1070
|
+
resize: "vertical",
|
|
1071
|
+
background: "transparent",
|
|
1072
|
+
color: "inherit",
|
|
1073
|
+
border: "none",
|
|
1074
|
+
outline: "none",
|
|
1075
|
+
font: "inherit",
|
|
1076
|
+
padding: "8px 10px"
|
|
1077
|
+
};
|
|
1078
|
+
function AgentPanelView({
|
|
1079
|
+
controller,
|
|
1080
|
+
submitOnEnter = true,
|
|
1081
|
+
showLane = true,
|
|
1082
|
+
showSessions = true,
|
|
1083
|
+
emptyHint
|
|
1084
|
+
}) {
|
|
1085
|
+
const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
|
|
1086
|
+
if (state.connection === "failed") {
|
|
1087
|
+
return /* @__PURE__ */ jsx(ErrorPane, { message: state.message });
|
|
1088
|
+
}
|
|
1089
|
+
if (state.connection === "connecting") {
|
|
1090
|
+
return /* @__PURE__ */ jsx(EmptyState, { title: "Connecting\u2026", hint: "Waiting for the agent host." });
|
|
1091
|
+
}
|
|
1092
|
+
if (state.sessions.length === 0) {
|
|
1093
|
+
return /* @__PURE__ */ jsxs("div", { style: COLUMN, children: [
|
|
1094
|
+
/* @__PURE__ */ jsx(EmptyState, { title: "No sessions", hint: emptyHint ?? "Start a session to talk to an agent." }),
|
|
1095
|
+
/* @__PURE__ */ jsx(
|
|
1096
|
+
Toolbar,
|
|
1097
|
+
{
|
|
1098
|
+
right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.newSession(), children: "New session" }) })
|
|
1099
|
+
}
|
|
1100
|
+
)
|
|
1101
|
+
] });
|
|
1102
|
+
}
|
|
1103
|
+
const turn = TURN[state.turn];
|
|
1104
|
+
const lane = state.activeProvider?.lane;
|
|
1105
|
+
return /* @__PURE__ */ jsxs("div", { style: COLUMN, children: [
|
|
1106
|
+
/* @__PURE__ */ jsx(
|
|
1107
|
+
Toolbar,
|
|
1108
|
+
{
|
|
1109
|
+
left: /* @__PURE__ */ jsxs(ToolbarGroup, { children: [
|
|
1110
|
+
/* @__PURE__ */ jsx(StatusBadge, { tone: turn.tone, children: turn.label }),
|
|
1111
|
+
showLane && lane ? /* @__PURE__ */ jsx(Badge, { children: LANE_LABEL[lane] ?? lane }) : null,
|
|
1112
|
+
state.pendingAsks > 0 ? /* @__PURE__ */ jsx(Badge, { children: `${state.pendingAsks} waiting` }) : null
|
|
1113
|
+
] }),
|
|
1114
|
+
right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: /* @__PURE__ */ jsx(TextButton, { onClick: () => controller.newSession(), children: "New session" }) })
|
|
1115
|
+
}
|
|
1116
|
+
),
|
|
1117
|
+
showSessions ? /* @__PURE__ */ jsx(ScrollArea, { children: /* @__PURE__ */ jsx(RowList, { children: state.sessions.map((session) => /* @__PURE__ */ jsx(
|
|
1118
|
+
Row,
|
|
1119
|
+
{
|
|
1120
|
+
noIcon: true,
|
|
1121
|
+
label: session.title ?? session.id,
|
|
1122
|
+
selected: session.id === state.active?.id,
|
|
1123
|
+
onClick: () => controller.selectSession(session.id),
|
|
1124
|
+
trailing: /* @__PURE__ */ jsx(Badge, { children: TURN[session.turn ?? "idle"].label })
|
|
1125
|
+
},
|
|
1126
|
+
session.id
|
|
1127
|
+
)) }) }) : null,
|
|
1128
|
+
state.providers.length > 1 ? /* @__PURE__ */ jsx(RowList, { children: state.providers.map((provider) => /* @__PURE__ */ jsx(
|
|
1129
|
+
Row,
|
|
1130
|
+
{
|
|
1131
|
+
noIcon: true,
|
|
1132
|
+
label: provider.label,
|
|
1133
|
+
meta: provider.unavailable ?? (showLane ? LANE_LABEL[provider.lane] ?? provider.lane : void 0),
|
|
1134
|
+
disabled: provider.unavailable !== void 0,
|
|
1135
|
+
selected: provider.id === state.activeProvider?.id,
|
|
1136
|
+
onClick: () => controller.selectProvider(provider.id)
|
|
1137
|
+
},
|
|
1138
|
+
provider.id
|
|
1139
|
+
)) }) : null,
|
|
1140
|
+
/* @__PURE__ */ jsx(
|
|
1141
|
+
"textarea",
|
|
1142
|
+
{
|
|
1143
|
+
style: COMPOSER,
|
|
1144
|
+
value: state.draft,
|
|
1145
|
+
placeholder: state.busy ? "Steer the running turn\u2026" : "Ask the agent\u2026",
|
|
1146
|
+
onChange: (event) => controller.setDraft(event.target.value),
|
|
1147
|
+
onKeyDown: (event) => {
|
|
1148
|
+
if (event.key !== "Enter") return;
|
|
1149
|
+
const wantsSubmit = submitOnEnter ? !event.shiftKey : event.shiftKey;
|
|
1150
|
+
if (!wantsSubmit) return;
|
|
1151
|
+
event.preventDefault();
|
|
1152
|
+
if (state.busy) controller.steer();
|
|
1153
|
+
else controller.submit();
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
),
|
|
1157
|
+
/* @__PURE__ */ jsx(
|
|
1158
|
+
StatusBar,
|
|
1159
|
+
{
|
|
1160
|
+
left: state.message ?? state.active?.cwd ?? void 0,
|
|
1161
|
+
right: /* @__PURE__ */ jsx(ToolbarGroup, { end: true, children: state.busy ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1162
|
+
/* @__PURE__ */ jsx(TextButton, { onClick: () => controller.steer(), children: "Steer" }),
|
|
1163
|
+
/* @__PURE__ */ jsx(TextButton, { onClick: () => controller.cancel(), children: "Stop" })
|
|
1164
|
+
] }) : (
|
|
1165
|
+
// `disabled` derives from the SAME predicate `submit()` enforces, so an enabled
|
|
1166
|
+
// button can never be a button that refuses.
|
|
1167
|
+
/* @__PURE__ */ jsx(TextButton, { disabled: !state.canSubmit, onClick: () => controller.submit(), children: "Send" })
|
|
1168
|
+
) })
|
|
1169
|
+
}
|
|
1170
|
+
)
|
|
1171
|
+
] });
|
|
1172
|
+
}
|
|
1173
|
+
function ErrorPane({ message }) {
|
|
1174
|
+
return /* @__PURE__ */ jsx(EmptyState, { title: "Not connected", hint: message ?? "The agent host reported a failure." });
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/agent/agent/panel.ts
|
|
1178
|
+
var CONNECTIONS = ["idle", "connecting", "ready", "failed"];
|
|
1179
|
+
function createAgentPanel(options = {}) {
|
|
1180
|
+
return {
|
|
1181
|
+
manifest: agentManifest,
|
|
1182
|
+
activate(host) {
|
|
1183
|
+
const initial = host.config ?? {};
|
|
1184
|
+
const controller = new AgentController({
|
|
1185
|
+
host: { emit: (portId, value) => host.emit(portId, value) },
|
|
1186
|
+
now: options.now
|
|
1187
|
+
});
|
|
1188
|
+
const resolve = (config) => ({
|
|
1189
|
+
submitOnEnter: configBool(config, "submitOnEnter", true),
|
|
1190
|
+
showLane: configBool(config, "showLane", true),
|
|
1191
|
+
showSessions: configBool(config, "showSessions", true),
|
|
1192
|
+
emptyHint: configString(config, "emptyHint", "Start a session to talk to an agent.")
|
|
1193
|
+
});
|
|
1194
|
+
let renderConfig = resolve(initial);
|
|
1195
|
+
let unrender = null;
|
|
1196
|
+
let root = null;
|
|
1197
|
+
let currentEl = null;
|
|
1198
|
+
const draw = (el) => {
|
|
1199
|
+
unrender?.();
|
|
1200
|
+
if (options.render) {
|
|
1201
|
+
unrender = options.render(el, { controller, config: renderConfig });
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
root = createRoot(el);
|
|
1205
|
+
root.render(createElement(AgentPanelView, { controller, ...renderConfig }));
|
|
1206
|
+
unrender = () => {
|
|
1207
|
+
root?.unmount();
|
|
1208
|
+
root = null;
|
|
1209
|
+
};
|
|
1210
|
+
};
|
|
1211
|
+
const unbindConfig = bindConfig(host, (config) => {
|
|
1212
|
+
renderConfig = resolve(config);
|
|
1213
|
+
if (currentEl) draw(currentEl);
|
|
1214
|
+
});
|
|
1215
|
+
return {
|
|
1216
|
+
render(el) {
|
|
1217
|
+
currentEl = el;
|
|
1218
|
+
draw(el);
|
|
1219
|
+
},
|
|
1220
|
+
onInput(portId, value) {
|
|
1221
|
+
switch (portId) {
|
|
1222
|
+
case "session": {
|
|
1223
|
+
if (!isRecord(value)) return;
|
|
1224
|
+
if (value.events !== void 0 && !Array.isArray(value.events)) return;
|
|
1225
|
+
if (value.replace !== void 0 && !Array.isArray(value.replace)) return;
|
|
1226
|
+
controller.ingest(value);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
case "catalog": {
|
|
1230
|
+
if (!isRecord(value) || !Array.isArray(value.providers)) return;
|
|
1231
|
+
controller.setCatalog(value);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
case "status": {
|
|
1235
|
+
if (!isRecord(value)) return;
|
|
1236
|
+
const status = value.status;
|
|
1237
|
+
if (typeof status !== "string" || !CONNECTIONS.includes(status)) return;
|
|
1238
|
+
controller.setStatus(
|
|
1239
|
+
status,
|
|
1240
|
+
typeof value.message === "string" ? value.message : void 0
|
|
1241
|
+
);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
// The four return paths. Each guards inside the controller and DROPS an answer it
|
|
1245
|
+
// cannot correlate — a decision routed to a guessed session applies a human's approval
|
|
1246
|
+
// to work they never saw.
|
|
1247
|
+
case "decision":
|
|
1248
|
+
case "result":
|
|
1249
|
+
controller.answer(value);
|
|
1250
|
+
return;
|
|
1251
|
+
case "diffDecision":
|
|
1252
|
+
controller.patchDecision(value);
|
|
1253
|
+
return;
|
|
1254
|
+
case "runAction":
|
|
1255
|
+
controller.runAction(value);
|
|
1256
|
+
return;
|
|
1257
|
+
case "terminalIntent":
|
|
1258
|
+
controller.terminalIntent(value);
|
|
1259
|
+
return;
|
|
1260
|
+
default:
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
async onCommand(commandId, params) {
|
|
1265
|
+
const state = controller.getState();
|
|
1266
|
+
switch (commandId) {
|
|
1267
|
+
case "get_sessions": {
|
|
1268
|
+
return {
|
|
1269
|
+
connection: state.connection,
|
|
1270
|
+
count: state.sessions.length,
|
|
1271
|
+
sessions: state.sessions.map((session) => ({
|
|
1272
|
+
id: session.id,
|
|
1273
|
+
title: session.title ?? null,
|
|
1274
|
+
providerId: session.providerId ?? null,
|
|
1275
|
+
lane: session.lane ?? null,
|
|
1276
|
+
turn: session.turn ?? "idle",
|
|
1277
|
+
active: session.id === state.active?.id
|
|
1278
|
+
}))
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
case "get_status": {
|
|
1282
|
+
return {
|
|
1283
|
+
connection: state.connection,
|
|
1284
|
+
message: state.message,
|
|
1285
|
+
activeSessionId: state.active?.id ?? null,
|
|
1286
|
+
turn: state.turn,
|
|
1287
|
+
busy: state.busy,
|
|
1288
|
+
pendingAsks: state.pendingAsks,
|
|
1289
|
+
// Surfaced rather than swallowed: a mis-wired return path and a dead one look
|
|
1290
|
+
// identical from the outside, and this is the one number that tells them apart.
|
|
1291
|
+
droppedAnswers: controller.droppedAnswers
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
case "select_session": {
|
|
1295
|
+
if (typeof params.sessionId !== "string") return null;
|
|
1296
|
+
controller.selectSession(params.sessionId);
|
|
1297
|
+
return controller.getState().active?.id ?? null;
|
|
1298
|
+
}
|
|
1299
|
+
case "cancel_turn": {
|
|
1300
|
+
return controller.cancel();
|
|
1301
|
+
}
|
|
1302
|
+
default:
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
serialize() {
|
|
1307
|
+
return controller.serialize();
|
|
1308
|
+
},
|
|
1309
|
+
deserialize(state) {
|
|
1310
|
+
controller.deserialize(state);
|
|
1311
|
+
},
|
|
1312
|
+
dispose() {
|
|
1313
|
+
unbindConfig();
|
|
1314
|
+
currentEl = null;
|
|
1315
|
+
unrender?.();
|
|
1316
|
+
unrender = null;
|
|
1317
|
+
controller.dispose();
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
var agentPanel = createAgentPanel();
|
|
1324
|
+
|
|
1325
|
+
// src/agent/agent/wiring.ts
|
|
1326
|
+
var AGENT_WIRING_TARGETS = {
|
|
1327
|
+
RUNS: "xeno.core.runs",
|
|
1328
|
+
CONSOLE: "xeno.core.console",
|
|
1329
|
+
CONSENT: "xeno.core.consent",
|
|
1330
|
+
DIFF: "xeno.core.diff",
|
|
1331
|
+
TERMINAL: "xeno.core.terminal"
|
|
1332
|
+
};
|
|
1333
|
+
var AGENT = AGENT_PANEL_ID;
|
|
1334
|
+
var { RUNS, CONSOLE, CONSENT, DIFF, TERMINAL } = AGENT_WIRING_TARGETS;
|
|
1335
|
+
var AGENT_PANEL_WIRING = Object.freeze([
|
|
1336
|
+
// ── The tool timeline ──────────────────────────────────────────────────
|
|
1337
|
+
{
|
|
1338
|
+
from: { panel: AGENT, port: "runs" },
|
|
1339
|
+
to: { panel: RUNS, port: "runs" },
|
|
1340
|
+
why: "Turns become runs and tool calls become nested steps. Without it there is no timeline at all \u2014 the panel that can render a subagent three levels down never receives one.",
|
|
1341
|
+
optional: true
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
from: { panel: RUNS, port: "action" },
|
|
1345
|
+
to: { panel: AGENT, port: "runAction" },
|
|
1346
|
+
why: "Cancel and Retry on a run or one of its steps. Without it those buttons emit into nothing and read as a broken agent rather than a missing wire.",
|
|
1347
|
+
optional: true
|
|
1348
|
+
},
|
|
1349
|
+
// ── The transcript ─────────────────────────────────────────────────────
|
|
1350
|
+
{
|
|
1351
|
+
from: { panel: AGENT, port: "records" },
|
|
1352
|
+
to: { panel: CONSOLE, port: "records" },
|
|
1353
|
+
why: "The conversation itself, streamed by update-by-id. Without it the agent replies into nothing visible.",
|
|
1354
|
+
optional: true
|
|
1355
|
+
},
|
|
1356
|
+
// ── Permissions and questions ──────────────────────────────────────────
|
|
1357
|
+
{
|
|
1358
|
+
from: { panel: AGENT, port: "elicitations" },
|
|
1359
|
+
to: { panel: CONSENT, port: "elicitations" },
|
|
1360
|
+
why: "Every permission the agent needs, and every question it asks. Without it a turn that stops on a grant looks like a hung agent.",
|
|
1361
|
+
optional: true
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
from: { panel: AGENT, port: "withdraw" },
|
|
1365
|
+
to: { panel: CONSENT, port: "withdraw" },
|
|
1366
|
+
why: "An ask the agent no longer needs. Without it the queue keeps a question nobody is waiting on, in front of the ones that matter.",
|
|
1367
|
+
optional: true
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
from: { panel: CONSENT, port: "decision" },
|
|
1371
|
+
to: { panel: AGENT, port: "decision" },
|
|
1372
|
+
why: "Permission answers. \u{1F534} Without it the agent never learns it was allowed, and the user watches an Allow they already pressed change nothing.",
|
|
1373
|
+
optional: true
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
from: { panel: CONSENT, port: "result" },
|
|
1377
|
+
to: { panel: AGENT, port: "result" },
|
|
1378
|
+
why: "Text, path and choice answers. A SECOND port because a permission and an answer partition \u2014 wiring only `decision` silently drops every question the agent asked.",
|
|
1379
|
+
optional: true
|
|
1380
|
+
},
|
|
1381
|
+
// ── Review ─────────────────────────────────────────────────────────────
|
|
1382
|
+
{
|
|
1383
|
+
from: { panel: AGENT, port: "diffModel" },
|
|
1384
|
+
to: { panel: DIFF, port: "model" },
|
|
1385
|
+
why: "Proposed edits, parsed from unified diff here because the diff panel ships no engine by charter.",
|
|
1386
|
+
optional: true
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
from: { panel: DIFF, port: "decision" },
|
|
1390
|
+
to: { panel: AGENT, port: "diffDecision" },
|
|
1391
|
+
why: "Accept and reject, correlated back to the change set that produced them. \u26A0\uFE0F Only meaningful with the diff panel\u2019s `review` config on; it is off by default.",
|
|
1392
|
+
optional: true
|
|
1393
|
+
},
|
|
1394
|
+
// ── The terminal ───────────────────────────────────────────────────────
|
|
1395
|
+
{
|
|
1396
|
+
from: { panel: AGENT, port: "terminalSession" },
|
|
1397
|
+
to: { panel: TERMINAL, port: "session" },
|
|
1398
|
+
why: "Instance lifecycle. Must be wired for `terminalData` to land anywhere \u2014 the panel drops a write for an instance it has not been told about.",
|
|
1399
|
+
optional: true
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
from: { panel: AGENT, port: "terminalData" },
|
|
1403
|
+
to: { panel: TERMINAL, port: "data" },
|
|
1404
|
+
why: "The agent\u2019s PTY output, escape sequences intact.",
|
|
1405
|
+
optional: true
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
from: { panel: TERMINAL, port: "intent" },
|
|
1409
|
+
to: { panel: AGENT, port: "terminalIntent" },
|
|
1410
|
+
why: "What the HUMAN types, plus resize and stop. Without it the terminal renders and swallows every keystroke.",
|
|
1411
|
+
optional: true
|
|
1412
|
+
}
|
|
1413
|
+
]);
|
|
1414
|
+
function wiringFor(panelId) {
|
|
1415
|
+
return AGENT_PANEL_WIRING.filter((w) => w.from.panel === panelId || w.to.panel === panelId);
|
|
1416
|
+
}
|
|
1417
|
+
export {
|
|
1418
|
+
AGENT_CATALOG_SCHEMA,
|
|
1419
|
+
AGENT_FANOUT_PORTS,
|
|
1420
|
+
AGENT_INTENT_SCHEMA,
|
|
1421
|
+
AGENT_PANEL_ID,
|
|
1422
|
+
AGENT_PANEL_WIRING,
|
|
1423
|
+
AGENT_SESSION_SCHEMA,
|
|
1424
|
+
AGENT_WIRING_TARGETS,
|
|
1425
|
+
AgentController,
|
|
1426
|
+
AgentPanelView,
|
|
1427
|
+
AgentProjector,
|
|
1428
|
+
CONSENT_ELICITATION_RESULT_SCHEMA,
|
|
1429
|
+
CONSENT_ELICITATION_SCHEMA,
|
|
1430
|
+
agentManifest,
|
|
1431
|
+
agentPanel,
|
|
1432
|
+
countChanges,
|
|
1433
|
+
createAgentPanel,
|
|
1434
|
+
isBusy,
|
|
1435
|
+
isWaitingOnUser,
|
|
1436
|
+
parseUnifiedDiff,
|
|
1437
|
+
turnStatusToRunStatus,
|
|
1438
|
+
wiringFor
|
|
1439
|
+
};
|