@datalayer/agent-runtimes 1.5.45 → 1.5.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist-embed/datalayer-app-main.js +3 -3
  2. package/lib/portable/hosts/__tests__/fixtures/lock-with-jupyter.json +40 -0
  3. package/lib/portable/hosts/__tests__/fixtures/npm-ls-with-jupyter.json +21 -0
  4. package/lib/portable/hosts/agentClient.d.ts +167 -0
  5. package/lib/portable/hosts/agentClient.js +346 -0
  6. package/lib/portable/hosts/approvalRenderers.d.ts +43 -0
  7. package/lib/portable/hosts/approvalRenderers.js +180 -0
  8. package/lib/portable/hosts/client.d.ts +95 -0
  9. package/lib/portable/hosts/client.js +174 -0
  10. package/lib/portable/hosts/contracts.d.ts +199 -0
  11. package/lib/portable/hosts/contracts.js +235 -0
  12. package/lib/portable/hosts/contracts.json +381 -0
  13. package/lib/portable/hosts/deadLetters.d.ts +41 -0
  14. package/lib/portable/hosts/deadLetters.js +124 -0
  15. package/lib/portable/hosts/deliveries.d.ts +76 -0
  16. package/lib/portable/hosts/deliveries.js +147 -0
  17. package/lib/portable/hosts/forbiddenGraph.d.ts +77 -0
  18. package/lib/portable/hosts/forbiddenGraph.js +174 -0
  19. package/lib/portable/hosts/health.d.ts +35 -0
  20. package/lib/portable/hosts/health.js +92 -0
  21. package/lib/portable/hosts/index.d.ts +40 -0
  22. package/lib/portable/hosts/index.js +45 -0
  23. package/lib/portable/hosts/observability.d.ts +67 -0
  24. package/lib/portable/hosts/observability.js +171 -0
  25. package/lib/portable/hosts/postgresQueue.d.ts +55 -0
  26. package/lib/portable/hosts/postgresQueue.js +217 -0
  27. package/lib/portable/hosts/queue.d.ts +47 -0
  28. package/lib/portable/hosts/queue.js +63 -0
  29. package/lib/portable/hosts/quotas.d.ts +81 -0
  30. package/lib/portable/hosts/quotas.js +147 -0
  31. package/lib/portable/hosts/rateLimit.d.ts +13 -0
  32. package/lib/portable/hosts/rateLimit.js +8 -0
  33. package/lib/portable/hosts/resolution.d.ts +54 -0
  34. package/lib/portable/hosts/resolution.js +46 -0
  35. package/lib/portable/hosts/schema.d.ts +19 -0
  36. package/lib/portable/hosts/schema.js +81 -0
  37. package/lib/portable/hosts/secretCipher.d.ts +23 -0
  38. package/lib/portable/hosts/secretCipher.js +45 -0
  39. package/lib/portable/hosts/sessionUid.d.ts +7 -0
  40. package/lib/portable/hosts/sessionUid.js +28 -0
  41. package/lib/portable/hosts/turnState.d.ts +45 -0
  42. package/lib/portable/hosts/turnState.js +113 -0
  43. package/lib/portable/index.d.ts +2 -0
  44. package/lib/portable/index.js +3 -0
  45. package/lib/portable/semantic-events.json +480 -0
  46. package/lib/portable/semantic.d.ts +78 -0
  47. package/lib/portable/semantic.js +100 -0
  48. package/lib/portable/semanticOfAgUi.d.ts +41 -0
  49. package/lib/portable/semanticOfAgUi.js +108 -0
  50. package/lib/protocols/A2AAdapter.js +19 -0
  51. package/lib/test-setup.js +17 -14
  52. package/package.json +1 -1
@@ -0,0 +1,167 @@
1
+ import { type AgUiEvent } from '../agui';
2
+ import type { SemanticEvent } from '../semantic';
3
+ import { type HostExchange } from './contracts';
4
+ /**
5
+ * A turn a host asks for. `provenance` is the host's own thread key (Slack's
6
+ * `ThreadKey`, Discord's): opaque here, read only through the `HostShape`.
7
+ */
8
+ export type HostTurnRequest<P> = Readonly<{
9
+ /** The deployment the turn runs: one the installation allows. */
10
+ applicationId: string;
11
+ externalEventId: string;
12
+ /** The host user the host speaks for: what the credential exchange names. */
13
+ externalUserId: string;
14
+ principalId: string;
15
+ prompt: string;
16
+ provenance: P;
17
+ sessionId?: string;
18
+ /**
19
+ * How many sessions the thread had before (an explicit start over or
20
+ * another application): a new session's uid is derived with it.
21
+ */
22
+ sessionGeneration?: number;
23
+ }>;
24
+ export type TurnStream = Readonly<{
25
+ /**
26
+ * The run the stream is of, as its `loop.turn` event names it: known once
27
+ * the stream has said it (what a Stop button names).
28
+ */
29
+ run: {
30
+ id?: string;
31
+ };
32
+ sessionId: string;
33
+ updates: AsyncIterable<SemanticEvent>;
34
+ }>;
35
+ export interface AgentClient<P> {
36
+ startTurn(request: HostTurnRequest<P>): Promise<TurnStream>;
37
+ stopTurn(sessionId: string, request: HostTurnRequest<P>): Promise<void>;
38
+ }
39
+ /** Where a host asks from (`exchange.optional_request.place`): a channel, or a direct conversation. */
40
+ export type Place = Readonly<{
41
+ kind: 'channel' | 'direct';
42
+ id: string;
43
+ }>;
44
+ /**
45
+ * What is the host's own in a turn: the session uid its thread derives
46
+ * (`hostSessionUid`), the place Datalayer checks the channel policy against,
47
+ * the delivery target handed back with every delivery, and what is said when
48
+ * its installation holds no host key.
49
+ */
50
+ export type HostShape<P> = Readonly<{
51
+ sessionUidOf(request: HostTurnRequest<P>): string;
52
+ placeOf(request: HostTurnRequest<P>): Place;
53
+ targetOf(request: HostTurnRequest<P>): Record<string, unknown>;
54
+ notBound: string;
55
+ }>;
56
+ /**
57
+ * What a turn runs with: a short-lived credential for the linked person and
58
+ * where the deployment's session API is. A bot token is never one.
59
+ */
60
+ export type HostSession = Readonly<{
61
+ accessToken: string;
62
+ agent: string;
63
+ deploymentUid: string;
64
+ /** Whether the session can be mapped to its host thread for deliveries. */
65
+ bindable: boolean;
66
+ sessionApiUrl: string;
67
+ userToken?: string;
68
+ }>;
69
+ export interface HostCredentials<P> {
70
+ forTurn(request: HostTurnRequest<P>): Promise<HostSession>;
71
+ }
72
+ /** What the installation's key is, for a turn: `undefined` when it was never bound. */
73
+ export type HostKeyLookup<P> = (request: HostTurnRequest<P>) => Promise<string | undefined>;
74
+ /**
75
+ * The host credential exchange (SLACK §4.3, 1): the installation's key and
76
+ * the host user it speaks for, exchanged at ai-agents for a fifteen-minute
77
+ * host token of the linked person and one deployment.
78
+ */
79
+ export declare class HostKeyCredentials<P> implements HostCredentials<P> {
80
+ readonly apiUrl: string;
81
+ readonly hostKeyFor: HostKeyLookup<P>;
82
+ readonly shape: HostShape<P>;
83
+ readonly fetchImplementation: typeof fetch;
84
+ constructor(apiUrl: string, hostKeyFor: HostKeyLookup<P>, shape: HostShape<P>, fetchImplementation?: typeof fetch);
85
+ exchange(request: HostTurnRequest<P>): Promise<HostExchange>;
86
+ /** The exchange for one host user and deployment, with a key in hand (a delivery, an action). */
87
+ exchangeFor(input: {
88
+ deploymentUid: string;
89
+ externalUserId: string;
90
+ hostKey: string;
91
+ /** Where the turn is asked from: Datalayer refuses a denied channel at once. */
92
+ place?: Place;
93
+ }): Promise<HostExchange>;
94
+ forTurn(request: HostTurnRequest<P>): Promise<HostSession>;
95
+ }
96
+ export declare function hostSessionOf(exchanged: HostExchange): HostSession;
97
+ /**
98
+ * A turn's credentials: the exchange when its installation holds a host
99
+ * key; otherwise, in development only, the developer's own (`fallback`);
100
+ * otherwise a refusal saying the installation is not connected.
101
+ */
102
+ export declare class InstallationCredentials<P> implements HostCredentials<P> {
103
+ readonly keyed: HostKeyCredentials<P>;
104
+ readonly fallback?: HostCredentials<P> | undefined;
105
+ constructor(keyed: HostKeyCredentials<P>, fallback?: HostCredentials<P> | undefined);
106
+ forTurn(request: HostTurnRequest<P>): Promise<HostSession>;
107
+ }
108
+ /**
109
+ * Development only: the developer's own Datalayer key, and their own kept
110
+ * deployment, read from ai-agents as its owner. Never in production.
111
+ */
112
+ export declare class DevelopmentCredentials<P> implements HostCredentials<P> {
113
+ readonly apiUrl: string;
114
+ readonly token: string;
115
+ readonly fetchImplementation: typeof fetch;
116
+ constructor(apiUrl: string, token: string, fetchImplementation?: typeof fetch);
117
+ forTurn(request: HostTurnRequest<P>): Promise<HostSession>;
118
+ }
119
+ /** One of an application's starters (SLACK S-104): the words shown, and the message it suggests asking. */
120
+ export type Starter = Readonly<{
121
+ label: string;
122
+ message: string;
123
+ }>;
124
+ /** Where an application's starters are read, as one linked person of one installation. */
125
+ export interface StarterSource<K> {
126
+ startersOf(key: K, externalUserId: string, applicationId: string): Promise<readonly Starter[]>;
127
+ }
128
+ /**
129
+ * An application's starters (SLACK S-104), read from its runtime as the
130
+ * linked person: the host credential exchange (no place: what is suggested
131
+ * is private to them), then `GET /api/v1/apps/current` beside the session
132
+ * API it answers. A runtime that does not answer starters is refused.
133
+ */
134
+ export declare class HostStarters<K, P> implements StarterSource<K> {
135
+ readonly apiUrl: string;
136
+ readonly keyed: HostKeyCredentials<P>;
137
+ readonly hostKeyOf: (key: K) => Promise<string | undefined>;
138
+ readonly fetchImplementation: typeof fetch;
139
+ constructor(apiUrl: string, keyed: HostKeyCredentials<P>, hostKeyOf: (key: K) => Promise<string | undefined>, fetchImplementation?: typeof fetch);
140
+ startersOf(key: K, externalUserId: string, applicationId: string): Promise<readonly Starter[]>;
141
+ }
142
+ /** Each AG-UI event of a run's `text/event-stream` body, as it arrives (`AgUiFrames`). */
143
+ export declare function agUiEventsOf(body: ReadableStream<Uint8Array>): AsyncIterable<AgUiEvent>;
144
+ /**
145
+ * The host's Datalayer client: a turn is started or sent on the deployment's
146
+ * session API (idempotent by the host's event id, SLACK §4.3, 2), its AG-UI
147
+ * stream reduced to semantic events, and its session mapped to the host's
148
+ * thread so that approvals and completions are delivered (§4.3, 3).
149
+ */
150
+ export declare class HttpAgentClient<P> implements AgentClient<P> {
151
+ #private;
152
+ readonly apiUrl: string;
153
+ readonly credentials: HostCredentials<P>;
154
+ readonly shape: HostShape<P>;
155
+ readonly fetchImplementation: typeof fetch;
156
+ readonly hostKeyFor?: HostKeyLookup<P> | undefined;
157
+ constructor(apiUrl: string, credentials: HostCredentials<P>, shape: HostShape<P>, fetchImplementation?: typeof fetch, hostKeyFor?: HostKeyLookup<P> | undefined);
158
+ startTurn(request: HostTurnRequest<P>): Promise<TurnStream>;
159
+ stopTurn(sessionId: string, request: HostTurnRequest<P>): Promise<void>;
160
+ }
161
+ /**
162
+ * The session's last answer, read with the host token (what a replayed turn
163
+ * and a `turn.completed` delivery show); `null` when the session is not read.
164
+ */
165
+ export declare function lastAnswer(fetchImplementation: typeof fetch, host: HostSession, sessionId: string): Promise<string | undefined | null>;
166
+ /** A session API or stream URL Datalayer answered: HTTPS, on Datalayer's own origin. */
167
+ export declare function assertTrustedStreamUrl(baseUrl: string, streamUrl: string): void;
@@ -0,0 +1,346 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A chat host's turns on Datalayer: the host credential exchange, a turn
7
+ * started or sent on the deployment's session API (idempotent by the host's
8
+ * event id), its AG-UI stream reduced to semantic events, and its session
9
+ * mapped to the host's thread so approvals and completions are delivered
10
+ * (SLACK §4.3, DISCORD §4.3). What is the host's own (its thread key, the
11
+ * session uid, the place, the delivery target, its words) is injected as a
12
+ * `HostShape`. Moved from the Slack service (plans/DISCORD.md DC-004).
13
+ *
14
+ * @module portable/hosts/agentClient
15
+ */
16
+ import { z } from 'zod';
17
+ import { AgUiFrames } from '../agui';
18
+ import { semanticOf, turnOf } from '../semanticOfAgUi';
19
+ import { HostRefusedError } from './client';
20
+ import { exchangeSchema, HOST_KEY_HEADER, IDEMPOTENCY_HEADER, } from './contracts';
21
+ /**
22
+ * The host credential exchange (SLACK §4.3, 1): the installation's key and
23
+ * the host user it speaks for, exchanged at ai-agents for a fifteen-minute
24
+ * host token of the linked person and one deployment.
25
+ */
26
+ export class HostKeyCredentials {
27
+ apiUrl;
28
+ hostKeyFor;
29
+ shape;
30
+ fetchImplementation;
31
+ constructor(apiUrl, hostKeyFor, shape, fetchImplementation = fetch) {
32
+ this.apiUrl = apiUrl;
33
+ this.hostKeyFor = hostKeyFor;
34
+ this.shape = shape;
35
+ this.fetchImplementation = fetchImplementation;
36
+ }
37
+ async exchange(request) {
38
+ const hostKey = await this.hostKeyFor(request);
39
+ if (!hostKey) {
40
+ throw new Error(this.shape.notBound);
41
+ }
42
+ return this.exchangeFor({
43
+ deploymentUid: request.applicationId,
44
+ externalUserId: request.externalUserId,
45
+ hostKey,
46
+ place: this.shape.placeOf(request),
47
+ });
48
+ }
49
+ /** The exchange for one host user and deployment, with a key in hand (a delivery, an action). */
50
+ async exchangeFor(input) {
51
+ const response = await this.fetchImplementation(`${this.apiUrl}/api/ai-agents/v1/apps/hosts/self/token`, {
52
+ body: JSON.stringify({
53
+ deployment_uid: input.deploymentUid,
54
+ external_user_id: input.externalUserId,
55
+ ...(input.place ? { place: input.place } : {}),
56
+ }),
57
+ headers: {
58
+ 'content-type': 'application/json',
59
+ [HOST_KEY_HEADER]: input.hostKey,
60
+ },
61
+ method: 'POST',
62
+ });
63
+ if (!response.ok) {
64
+ const body = (await response.json().catch(() => ({})));
65
+ throw new HostRefusedError(response.status, typeof body.detail === 'string' ? body.detail : 'refused', 'Datalayer refused the host credential exchange');
66
+ }
67
+ return exchangeSchema.parse(await response.json());
68
+ }
69
+ async forTurn(request) {
70
+ return hostSessionOf(await this.exchange(request));
71
+ }
72
+ }
73
+ export function hostSessionOf(exchanged) {
74
+ return {
75
+ accessToken: exchanged.access_token,
76
+ agent: exchanged.session_api.agent,
77
+ bindable: true,
78
+ deploymentUid: exchanged.deployment.uid,
79
+ sessionApiUrl: exchanged.session_api.url,
80
+ userToken: exchanged.user_token,
81
+ };
82
+ }
83
+ /**
84
+ * A turn's credentials: the exchange when its installation holds a host
85
+ * key; otherwise, in development only, the developer's own (`fallback`);
86
+ * otherwise a refusal saying the installation is not connected.
87
+ */
88
+ export class InstallationCredentials {
89
+ keyed;
90
+ fallback;
91
+ constructor(keyed, fallback) {
92
+ this.keyed = keyed;
93
+ this.fallback = fallback;
94
+ }
95
+ async forTurn(request) {
96
+ if (await this.keyed.hostKeyFor(request)) {
97
+ return this.keyed.forTurn(request);
98
+ }
99
+ if (this.fallback) {
100
+ return this.fallback.forTurn(request);
101
+ }
102
+ throw new Error(this.keyed.shape.notBound);
103
+ }
104
+ }
105
+ /**
106
+ * Development only: the developer's own Datalayer key, and their own kept
107
+ * deployment, read from ai-agents as its owner. Never in production.
108
+ */
109
+ export class DevelopmentCredentials {
110
+ apiUrl;
111
+ token;
112
+ fetchImplementation;
113
+ constructor(apiUrl, token, fetchImplementation = fetch) {
114
+ this.apiUrl = apiUrl;
115
+ this.token = token;
116
+ this.fetchImplementation = fetchImplementation;
117
+ }
118
+ async forTurn(request) {
119
+ const response = await this.fetchImplementation(`${this.apiUrl}/api/ai-agents/v1/apps/deployments/${encodeURIComponent(request.applicationId)}`, { headers: { authorization: `Bearer ${this.token}` } });
120
+ if (!response.ok) {
121
+ throw new Error(`Datalayer did not answer the deployment (${response.status}).`);
122
+ }
123
+ const body = (await response.json());
124
+ const kept = body.deployment?.kept;
125
+ if (!kept?.url || !kept.agent_id) {
126
+ throw new Error('The deployment is not kept always on: turn it on under Ship.');
127
+ }
128
+ return {
129
+ accessToken: this.token,
130
+ agent: kept.agent_id,
131
+ bindable: false,
132
+ deploymentUid: request.applicationId,
133
+ sessionApiUrl: `${kept.url.replace(/\/+$/, '')}/api/v1/apps/sessions`,
134
+ };
135
+ }
136
+ }
137
+ /** What `GET /api/v1/apps/current` answers that a host reads: the application's starters. */
138
+ const currentAppSchema = z.object({
139
+ starters: z.array(z.object({ label: z.string(), message: z.string() })),
140
+ });
141
+ /**
142
+ * An application's starters (SLACK S-104), read from its runtime as the
143
+ * linked person: the host credential exchange (no place: what is suggested
144
+ * is private to them), then `GET /api/v1/apps/current` beside the session
145
+ * API it answers. A runtime that does not answer starters is refused.
146
+ */
147
+ export class HostStarters {
148
+ apiUrl;
149
+ keyed;
150
+ hostKeyOf;
151
+ fetchImplementation;
152
+ constructor(apiUrl, keyed, hostKeyOf, fetchImplementation = fetch) {
153
+ this.apiUrl = apiUrl;
154
+ this.keyed = keyed;
155
+ this.hostKeyOf = hostKeyOf;
156
+ this.fetchImplementation = fetchImplementation;
157
+ }
158
+ async startersOf(key, externalUserId, applicationId) {
159
+ const hostKey = await this.hostKeyOf(key);
160
+ if (!hostKey) {
161
+ throw new Error(this.keyed.shape.notBound);
162
+ }
163
+ const exchanged = await this.keyed.exchangeFor({
164
+ deploymentUid: applicationId,
165
+ externalUserId,
166
+ hostKey,
167
+ });
168
+ const sessions = exchanged.session_api.url.replace(/\/+$/, '');
169
+ assertTrustedStreamUrl(this.apiUrl, sessions);
170
+ if (!sessions.endsWith('/api/v1/apps/sessions')) {
171
+ throw new Error("Datalayer answered a session API that is not an application runtime's.");
172
+ }
173
+ const response = await this.fetchImplementation(`${sessions.slice(0, -'/sessions'.length)}/current`, { headers: { authorization: `Bearer ${exchanged.access_token}` } });
174
+ if (!response.ok) {
175
+ throw new HostRefusedError(response.status, 'refused', 'The application did not say what it suggests asking');
176
+ }
177
+ return currentAppSchema.parse(await response.json()).starters;
178
+ }
179
+ }
180
+ /** Each AG-UI event of a run's `text/event-stream` body, as it arrives (`AgUiFrames`). */
181
+ export async function* agUiEventsOf(body) {
182
+ const decoder = new TextDecoder();
183
+ const frames = new AgUiFrames();
184
+ const reader = body.getReader();
185
+ try {
186
+ for (;;) {
187
+ const { done, value } = await reader.read();
188
+ if (done) {
189
+ break;
190
+ }
191
+ yield* frames.push(decoder.decode(value, { stream: true }));
192
+ }
193
+ yield* frames.push(decoder.decode());
194
+ yield* frames.end();
195
+ }
196
+ finally {
197
+ reader.releaseLock();
198
+ }
199
+ }
200
+ /**
201
+ * The host's Datalayer client: a turn is started or sent on the deployment's
202
+ * session API (idempotent by the host's event id, SLACK §4.3, 2), its AG-UI
203
+ * stream reduced to semantic events, and its session mapped to the host's
204
+ * thread so that approvals and completions are delivered (§4.3, 3).
205
+ */
206
+ export class HttpAgentClient {
207
+ apiUrl;
208
+ credentials;
209
+ shape;
210
+ fetchImplementation;
211
+ hostKeyFor;
212
+ constructor(apiUrl, credentials, shape, fetchImplementation = fetch, hostKeyFor) {
213
+ this.apiUrl = apiUrl;
214
+ this.credentials = credentials;
215
+ this.shape = shape;
216
+ this.fetchImplementation = fetchImplementation;
217
+ this.hostKeyFor = hostKeyFor;
218
+ }
219
+ async startTurn(request) {
220
+ const host = await this.credentials.forTurn(request);
221
+ assertTrustedStreamUrl(this.apiUrl, host.sessionApiUrl);
222
+ const sessionId = request.sessionId ?? this.shape.sessionUidOf(request);
223
+ const headers = {
224
+ accept: 'text/event-stream',
225
+ authorization: `Bearer ${host.accessToken}`,
226
+ 'content-type': 'application/json',
227
+ [IDEMPOTENCY_HEADER]: request.externalEventId,
228
+ };
229
+ const response = request.sessionId
230
+ ? await this.fetchImplementation(`${host.sessionApiUrl}/${encodeURIComponent(sessionId)}/messages`, {
231
+ body: JSON.stringify({
232
+ external_event_id: request.externalEventId,
233
+ text: request.prompt,
234
+ }),
235
+ headers,
236
+ method: 'POST',
237
+ })
238
+ : await this.fetchImplementation(host.sessionApiUrl, {
239
+ body: JSON.stringify({
240
+ agent: host.agent,
241
+ deployment_uid: host.deploymentUid,
242
+ external_event_id: request.externalEventId,
243
+ opener: request.prompt,
244
+ session: sessionId,
245
+ ...(host.userToken ? { user_token: host.userToken } : {}),
246
+ }),
247
+ headers,
248
+ method: 'POST',
249
+ });
250
+ if (!response.ok || !response.body) {
251
+ throw new Error(`Datalayer refused the turn (${response.status}).`);
252
+ }
253
+ const hostKey = host.bindable && this.hostKeyFor
254
+ ? await this.hostKeyFor(request)
255
+ : undefined;
256
+ if (hostKey) {
257
+ await this.#bind(request, host, sessionId, hostKey);
258
+ }
259
+ const run = {};
260
+ return {
261
+ run,
262
+ sessionId,
263
+ updates: this.#updates(response.body, host, sessionId, run),
264
+ };
265
+ }
266
+ async stopTurn(sessionId, request) {
267
+ const host = await this.credentials.forTurn(request);
268
+ assertTrustedStreamUrl(this.apiUrl, host.sessionApiUrl);
269
+ const response = await this.fetchImplementation(`${host.sessionApiUrl}/${encodeURIComponent(sessionId)}/stop`, {
270
+ headers: { authorization: `Bearer ${host.accessToken}` },
271
+ method: 'POST',
272
+ });
273
+ if (!response.ok && response.status !== 409) {
274
+ throw new Error(`Datalayer could not stop the turn (${response.status}).`);
275
+ }
276
+ }
277
+ async #bind(request, host, sessionId, hostKey) {
278
+ const response = await this.fetchImplementation(`${this.apiUrl}/api/ai-agents/v1/apps/hosts/sessions/${encodeURIComponent(sessionId)}`, {
279
+ // Handed back with every delivery: the thread, and the host user the
280
+ // host token speaks for, whom an approval waits on.
281
+ body: JSON.stringify({ target: this.shape.targetOf(request) }),
282
+ headers: {
283
+ authorization: `Bearer ${host.accessToken}`,
284
+ 'content-type': 'application/json',
285
+ [HOST_KEY_HEADER]: hostKey,
286
+ },
287
+ method: 'PUT',
288
+ });
289
+ if (!response.ok) {
290
+ throw new Error(`Datalayer did not map the thread to its session (${response.status}).`);
291
+ }
292
+ }
293
+ async *#updates(body, host, sessionId, run) {
294
+ const tools = new Map();
295
+ for await (const event of agUiEventsOf(body)) {
296
+ const turn = turnOf(event);
297
+ if (turn?.turn) {
298
+ run.id = turn.turn;
299
+ }
300
+ if (turn?.replayed) {
301
+ // A retry: nothing ran again; what the turn said is the session's.
302
+ yield* this.#replayed(host, sessionId, turn.state);
303
+ return;
304
+ }
305
+ yield* semanticOf(event, tools);
306
+ }
307
+ }
308
+ async *#replayed(host, sessionId, state) {
309
+ if (state === 'running') {
310
+ yield { kind: 'progress', label: 'Still working on this message…' };
311
+ return;
312
+ }
313
+ const text = await lastAnswer(this.fetchImplementation, host, sessionId);
314
+ if (text === null) {
315
+ yield {
316
+ kind: 'error',
317
+ message: 'This message was already answered in Datalayer.',
318
+ };
319
+ return;
320
+ }
321
+ yield { kind: 'completion', text };
322
+ }
323
+ }
324
+ /**
325
+ * The session's last answer, read with the host token (what a replayed turn
326
+ * and a `turn.completed` delivery show); `null` when the session is not read.
327
+ */
328
+ export async function lastAnswer(fetchImplementation, host, sessionId) {
329
+ const response = await fetchImplementation(`${host.sessionApiUrl}/${encodeURIComponent(sessionId)}/messages`, { headers: { authorization: `Bearer ${host.accessToken}` } });
330
+ if (!response.ok) {
331
+ return null;
332
+ }
333
+ const { messages } = (await response.json());
334
+ const last = [...(messages ?? [])]
335
+ .reverse()
336
+ .find(message => message.role === 'assistant');
337
+ return typeof last?.content === 'string' ? last.content : undefined;
338
+ }
339
+ /** A session API or stream URL Datalayer answered: HTTPS, on Datalayer's own origin. */
340
+ export function assertTrustedStreamUrl(baseUrl, streamUrl) {
341
+ const base = new URL(baseUrl);
342
+ const stream = new URL(streamUrl);
343
+ if (stream.protocol !== 'https:' || stream.origin !== base.origin) {
344
+ throw new Error('Datalayer returned an untrusted stream URL.');
345
+ }
346
+ }
@@ -0,0 +1,43 @@
1
+ import type { ApprovalPresentation } from './contracts';
2
+ /**
3
+ * Typed approval renderers (plans/SLACK.md §2 rule 6, S-302; plans/DISCORD.md
4
+ * §3.6). A host offers Approve and Reject only when a renderer that knows the
5
+ * tool proves that every material argument is shown: each argument the server
6
+ * presented is named by the renderer, or the card fails closed. An unknown
7
+ * tool, an argument the renderer does not know, an attachment, and every
8
+ * destructive, financial or publishing action are reviewed in Datalayer
9
+ * (SLACK §15, 5). They render ai-agents' presentation v1, a different input
10
+ * from `../approvals.ts` and `../approvalCard.ts`. Moved from the Slack
11
+ * service (plans/DISCORD.md DC-004).
12
+ *
13
+ * @module portable/hosts/approvalRenderers
14
+ */
15
+ export type RenderedField = Readonly<{
16
+ label: string;
17
+ value: string;
18
+ }>;
19
+ export type RenderedAction = Readonly<{
20
+ /** The renderer that knew the tool. */
21
+ renderer: string;
22
+ /** The fields shown, in order, each whole. */
23
+ fields: readonly RenderedField[];
24
+ /** Whether every argument is shown and nothing needs Datalayer. */
25
+ complete: boolean;
26
+ /** Why not, in words, when it is not. */
27
+ reasons: readonly string[];
28
+ }>;
29
+ type Renderer = Readonly<{
30
+ name: string;
31
+ matches(presentation: ApprovalPresentation): boolean;
32
+ render(presentation: ApprovalPresentation): RenderedAction;
33
+ }>;
34
+ /** Action classes that are decided in Datalayer only (§15, 5). */
35
+ export declare const DATALAYER_ONLY: Set<"unknown" | "read" | "write" | "send" | "delete" | "publish" | "execute" | "financial">;
36
+ /** The renderers, the first that matches wins: Datalayer-only before any other. */
37
+ export declare const RENDERERS: readonly Renderer[];
38
+ /**
39
+ * How a host shows an approval, by the renderer that knows its tool; with
40
+ * none, nothing is decided in the host.
41
+ */
42
+ export declare function renderApproval(presentation: ApprovalPresentation): RenderedAction;
43
+ export {};