@xenosystem/blocks 0.6.0 → 0.8.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.
@@ -0,0 +1,118 @@
1
+ import { X as XenoAgentSessionDelta, a as XenoAgentEvent } from '../types-C5T3uVwd.js';
2
+
3
+ /**
4
+ * The reference adapter: XENO Agent host runtime events → this panel's `session` input.
5
+ *
6
+ * ## Why this lives in the PANEL package
7
+ *
8
+ * A panel owns its input contract and a host adapts to it — that is how all 24 canonical panels
9
+ * work. But "the host adapts" left every host to rediscover the same three traps, and this package
10
+ * had already paid for all three while writing its own harness. So the reference adapter ships
11
+ * here, beside the contract it targets, as a separate subpath (`@xenosystem/blocks/agent/host-source`)
12
+ * that a renderer never imports.
13
+ *
14
+ * 🔴 **It is written STRUCTURALLY and imports no host type.** `@xeno-corporation/xeno-agent-interface-contract`
15
+ * is not a dependency and must not become one: this package has zero runtime dependencies by a
16
+ * publish gate, and a panel that compiled against a host's types would invert the direction the
17
+ * whole catalogue depends on. The shapes below are what the host emits, restated as the minimum
18
+ * this adapter reads — and `translate` refuses anything it does not recognise rather than guessing.
19
+ *
20
+ * ## The three traps, all measured rather than anticipated
21
+ *
22
+ * 1. 🔴 **`rev` must ADVANCE per delta.** The controller drops `rev <= this.rev` **silently**, and
23
+ * `Date.now()` is not monotonic enough — three synchronous sends land in one millisecond, so two
24
+ * of every three deltas vanish and the failure surfaces somewhere unrelated. `createHostSource`
25
+ * owns a counter for exactly this reason; a caller never supplies `rev`.
26
+ * 2. 🔴 **`session` NESTS its payload under `session`; `turn`, `message` and `tool` are flat.**
27
+ * `INTEGRATION.md` documents the payload without showing the nesting, and the author of that
28
+ * document got it wrong reading it back.
29
+ * 3. 🔴 **A streamed reply is ONE record grown by `append`, never one per chunk.** One event per
30
+ * token defeats the console's adjacent-duplicate collapsing and is the reason the console gained
31
+ * an update-by-id channel at all. `assistantDelta` opens with `text` once and `append`s after.
32
+ *
33
+ * ⚠️ **Unrecognised events are COUNTED, not dropped silently.** A host whose vocabulary drifts must
34
+ * be able to see that it drifted — a source that quietly ignores half its input looks identical to
35
+ * a quiet agent.
36
+ */
37
+
38
+ /** The host envelope, restated as the fields this adapter reads. Structural on purpose. */
39
+ interface HostRuntimeEvent {
40
+ readonly conversationId: string;
41
+ readonly requestId?: string;
42
+ readonly type: string;
43
+ readonly providerId?: string;
44
+ readonly parentRunId?: string;
45
+ readonly subagentRunId?: string;
46
+ readonly timestamp?: string;
47
+ readonly event?: Record<string, unknown>;
48
+ }
49
+ /**
50
+ * A conversation as the host DESCRIBES it, not as it streams.
51
+ *
52
+ * 🔴 **The runtime event stream carries no title.** Measured against
53
+ * `@xeno-corporation/xeno-agent-interface-contract`: a conversation's `title` lives on the
54
+ * conversation RECORD returned by `conversation.open` / `conversation.list`, and appears on no
55
+ * runtime envelope. A host that forwards only the event stream therefore shows every conversation
56
+ * unnamed — which is what happened the first time this chain ran end to end, and which the first
57
+ * fix made worse by promoting a tool's `label` into the title (see `translate`).
58
+ *
59
+ * So the record is its own entry point. Fields the host does not know are simply absent.
60
+ */
61
+ interface HostConversationRecord {
62
+ readonly id: string;
63
+ readonly title?: string;
64
+ readonly providerId?: string;
65
+ /** `cloud` | `sdk-native` | `acp` — the panel renders it, it does not interpret it. */
66
+ readonly lane?: string;
67
+ }
68
+ interface HostSourceOptions {
69
+ /** Called with each delta, in order. Wire this to `deliverInput('session', delta)`. */
70
+ readonly emit: (delta: XenoAgentSessionDelta) => void;
71
+ /** Injectable for tests. Never used for `rev` — see trap 1. */
72
+ readonly now?: () => number;
73
+ }
74
+ interface HostSourceStats {
75
+ /** Events this adapter understood and translated. */
76
+ readonly translated: number;
77
+ /** Events it did not recognise, by `type`. Reported, never silently dropped. */
78
+ readonly unrecognised: Readonly<Record<string, number>>;
79
+ }
80
+ /**
81
+ * The host frame, before any of this can read it.
82
+ *
83
+ * 🔴 **A host event is THREE layers deep, and the first version of this adapter read the top one.**
84
+ * Measured against a running host on 2026-09-06 — real `AgentHostCoordinator`, real ACP turn
85
+ * adapter, real socket — a turn produced 13 events and this producer translated **zero**:
86
+ *
87
+ * ```
88
+ * outer { type: "runtime.event.appended", source: "host", payload: { requestId, event: … } }
89
+ * ↓
90
+ * runtime { conversationId, requestId, providerId, providerKind, type: "acp.…", event: … }
91
+ * ↓
92
+ * payload { messageId, text, … } ← the fields this adapter actually reads
93
+ * ```
94
+ *
95
+ * The MIDDLE layer is `HostRuntimeEvent`, so the structural assumption was right and the entry
96
+ * point was wrong. Unwrapping is done here rather than by every consumer, because a consumer that
97
+ * forgets it sees a silent agent — not an error.
98
+ *
99
+ * ⚠️ Not every outer event wraps a runtime event: `turn.execution.completed` carries its
100
+ * conversation and request ids directly on `payload` and has no inner event at all. That one is
101
+ * the turn's terminal signal, so dropping it would leave a finished turn spinning forever.
102
+ */
103
+ declare function unwrapHostFrame(frame: Record<string, unknown>): HostRuntimeEvent | null;
104
+ /**
105
+ * Translate ONE host event into zero or more panel events.
106
+ *
107
+ * Pure and exported so a host can test its own vocabulary against this without a panel, a socket,
108
+ * or a running agent.
109
+ */
110
+ declare function translate(envelope: HostRuntimeEvent): XenoAgentEvent[];
111
+ declare function createHostSource(options: HostSourceOptions): {
112
+ push: (frame: HostRuntimeEvent | Record<string, unknown>) => void;
113
+ session: (record: HostConversationRecord) => void;
114
+ stats: () => HostSourceStats;
115
+ reset: () => void;
116
+ };
117
+
118
+ export { type HostConversationRecord, type HostRuntimeEvent, type HostSourceOptions, type HostSourceStats, createHostSource, translate, unwrapHostFrame };
@@ -0,0 +1,228 @@
1
+ import "../chunk-2KG3PWR4.js";
2
+
3
+ // src/agent/agent/hostSource.ts
4
+ var str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
5
+ function turnStateOf(type, event) {
6
+ if (type === "running" || type === "turn.started") return "running";
7
+ if (type === "turn.completed" || type === "completed") return "succeeded";
8
+ if (type === "turn.failed" || type === "failed") return "failed";
9
+ if (type === "turn.cancelled" || type === "cancelled") return "cancelled";
10
+ if (type === "turn.execution.completed") return "succeeded";
11
+ if (type === "turn.execution.failed") return "failed";
12
+ const state = str(event.state);
13
+ if (type === "turn" && state) return state;
14
+ return void 0;
15
+ }
16
+ function elicitationOf(type, event) {
17
+ if (!type.startsWith("elicitation.")) return null;
18
+ if (type.endsWith(".requested")) {
19
+ const ask = event.elicitation ?? event.request ?? event;
20
+ return { ask };
21
+ }
22
+ if (type.endsWith(".answer")) {
23
+ const id = str(event.permissionId) ?? str(event.askUserId) ?? str(event.chooseDirectoryId) ?? str(event.workspacePlacementId) ?? str(event.id);
24
+ return id ? { withdrawId: id } : null;
25
+ }
26
+ return null;
27
+ }
28
+ function unwrapHostFrame(frame) {
29
+ const type = str(frame.type);
30
+ if (!type) return null;
31
+ const payload = frame.payload ?? {};
32
+ if (type === "runtime.event.appended") {
33
+ const inner = payload.event;
34
+ return inner && typeof inner === "object" ? inner : null;
35
+ }
36
+ if (str(frame.conversationId)) return frame;
37
+ const conversationId = str(payload.conversationId) ?? str(frame.conversationId);
38
+ if (!conversationId) return null;
39
+ return {
40
+ conversationId,
41
+ ...str(payload.requestId) ?? str(frame.requestId) ? { requestId: str(payload.requestId) ?? str(frame.requestId) } : {},
42
+ type,
43
+ event: payload
44
+ };
45
+ }
46
+ function fromAcp(type, event) {
47
+ if (type === "acp.session.message.delta" || type === "acp.session.message") {
48
+ const messageId = str(event.messageId);
49
+ if (!messageId) return null;
50
+ const text = str(event.text) ?? "";
51
+ return { message: { messageId, append: text, role: "assistant" } };
52
+ }
53
+ if (type === "acp.session.plan") {
54
+ const content = str(event.content);
55
+ if (!content) return { ignored: true };
56
+ return { message: { messageId: `plan-${str(event.id) ?? "current"}`, text: content, role: "system" } };
57
+ }
58
+ if (type.startsWith("acp.tool.")) {
59
+ const callId = str(event.toolCallId);
60
+ if (!callId) return null;
61
+ const status = str(event.status) ?? "";
62
+ const state = type === "acp.tool.completed" || status === "completed" ? status === "failed" ? "failed" : "succeeded" : status === "failed" ? "failed" : "running";
63
+ return {
64
+ tool: {
65
+ callId,
66
+ // `title` is what a person reads; `toolName` is the tool's id. Later updates carry only the
67
+ // latter, and the panel keeps the label it was opened with, so passing both is correct.
68
+ ...str(event.title) ?? str(event.toolName) ? { label: str(event.title) ?? str(event.toolName) } : {},
69
+ state,
70
+ ...str(event.kind) ? { detail: { kind: str(event.kind) } } : {}
71
+ }
72
+ };
73
+ }
74
+ if (type === "acp.session.completed") {
75
+ return { turn: str(event.status) === "failed" ? "failed" : "succeeded" };
76
+ }
77
+ if (type === "acp.session.failed" || type === "acp.agent.failed") return { turn: "failed" };
78
+ if (type === "acp.agent.started" || type === "acp.session.started") return { turn: "running" };
79
+ if (type === "acp.session.usage" || type === "acp.session.thought") return { ignored: true };
80
+ return null;
81
+ }
82
+ function translate(envelope) {
83
+ const sessionId = envelope.conversationId;
84
+ if (!sessionId) return [];
85
+ const event = envelope.event ?? {};
86
+ const type = envelope.type;
87
+ const out = [];
88
+ out.push({
89
+ type: "session",
90
+ session: {
91
+ id: sessionId,
92
+ title: str(event.title),
93
+ providerId: envelope.providerId
94
+ }
95
+ });
96
+ const turnId = envelope.requestId;
97
+ const state = turnStateOf(type, event);
98
+ if (turnId && state) {
99
+ out.push({ type: "turn", sessionId, turnId, state, label: str(event.label) });
100
+ }
101
+ const elicitation = elicitationOf(type, event);
102
+ if (elicitation) {
103
+ if ("ask" in elicitation) out.push({ type: "ask", sessionId, ask: elicitation.ask });
104
+ else out.push({ type: "ask-withdraw", sessionId, askId: elicitation.withdrawId });
105
+ return out;
106
+ }
107
+ const acp = type.startsWith("acp.") ? fromAcp(type, event) : null;
108
+ if (acp) {
109
+ if (acp.turn && turnId) {
110
+ out.push({ type: "turn", sessionId, turnId, state: acp.turn, label: str(event.title) });
111
+ }
112
+ if (acp.message) {
113
+ out.push({
114
+ type: "message",
115
+ sessionId,
116
+ turnId,
117
+ messageId: acp.message.messageId,
118
+ role: acp.message.role,
119
+ ...acp.message.append !== void 0 ? { append: acp.message.append } : { text: acp.message.text ?? "" }
120
+ });
121
+ }
122
+ if (acp.tool && turnId) {
123
+ out.push({
124
+ type: "tool",
125
+ sessionId,
126
+ turnId,
127
+ callId: acp.tool.callId,
128
+ parentCallId: envelope.parentRunId,
129
+ ...acp.tool.label ? { label: acp.tool.label } : {},
130
+ state: acp.tool.state,
131
+ ...acp.tool.detail ? { detail: acp.tool.detail } : {}
132
+ });
133
+ }
134
+ return out;
135
+ }
136
+ if (type === "assistant" || type === "message") {
137
+ const messageId = str(event.messageId) ?? str(event.id) ?? turnId;
138
+ if (messageId) {
139
+ const append = str(event.delta) ?? str(event.append);
140
+ out.push({
141
+ type: "message",
142
+ sessionId,
143
+ turnId,
144
+ messageId,
145
+ role: str(event.role) ?? "assistant",
146
+ // Trap 3: `append` is the streaming channel; `text` opens or replaces.
147
+ ...append !== void 0 ? { append } : { text: str(event.text) ?? "" }
148
+ });
149
+ }
150
+ return out;
151
+ }
152
+ if (type === "tool" || type.startsWith("tools.")) {
153
+ const callId = str(event.callId) ?? str(event.toolCallId) ?? str(event.id);
154
+ if (callId && turnId) {
155
+ out.push({
156
+ type: "tool",
157
+ sessionId,
158
+ turnId,
159
+ callId,
160
+ // A subagent's call nests under the call that spawned it — the recursion `runs` gained.
161
+ parentCallId: envelope.parentRunId,
162
+ label: str(event.label) ?? str(event.name) ?? type.replace(/^tools\./, ""),
163
+ state: str(event.state) ?? "running",
164
+ detail: str(event.detail),
165
+ error: str(event.error)
166
+ });
167
+ }
168
+ return out;
169
+ }
170
+ return out;
171
+ }
172
+ var IGNORED_TYPES = /* @__PURE__ */ new Set(["acp.session.usage", "acp.session.thought"]);
173
+ function createHostSource(options) {
174
+ let rev = 0;
175
+ let translated = 0;
176
+ const unrecognised = {};
177
+ return {
178
+ push(frame) {
179
+ const envelope = unwrapHostFrame(frame) ?? frame;
180
+ const events = translate(envelope);
181
+ const type = envelope.type ?? frame.type;
182
+ if (events.length <= 1 && type && !IGNORED_TYPES.has(type)) {
183
+ unrecognised[type] = (unrecognised[type] ?? 0) + 1;
184
+ } else {
185
+ translated += 1;
186
+ }
187
+ if (events.length === 0) return;
188
+ rev += 1;
189
+ options.emit({ rev, events });
190
+ },
191
+ /*
192
+ * Upsert a conversation from the host's own DESCRIPTION of it.
193
+ *
194
+ * ⚠️ Deliberately NOT counted as `translated` — that statistic answers "did this adapter
195
+ * understand the host's event VOCABULARY", and a record the host handed over directly proves
196
+ * nothing about the stream. Conflating them would let a source that understood no events at
197
+ * all report a healthy translation count.
198
+ */
199
+ session(record) {
200
+ if (!record.id) return;
201
+ rev += 1;
202
+ options.emit({
203
+ rev,
204
+ events: [
205
+ {
206
+ type: "session",
207
+ session: {
208
+ id: record.id,
209
+ title: record.title,
210
+ providerId: record.providerId,
211
+ lane: record.lane
212
+ }
213
+ }
214
+ ]
215
+ });
216
+ },
217
+ stats: () => ({ translated, unrecognised: { ...unrecognised } }),
218
+ reset() {
219
+ translated = 0;
220
+ for (const key of Object.keys(unrecognised)) delete unrecognised[key];
221
+ }
222
+ };
223
+ }
224
+ export {
225
+ createHostSource,
226
+ translate,
227
+ unwrapHostFrame
228
+ };