@datalayer/agent-runtimes 1.5.50 → 1.5.52

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.
@@ -36,6 +36,15 @@ export interface AgentClient<P> {
36
36
  startTurn(request: HostTurnRequest<P>): Promise<TurnStream>;
37
37
  stopTurn(sessionId: string, request: HostTurnRequest<P>): Promise<void>;
38
38
  }
39
+ /**
40
+ * Maps a session to its host place again, once the place changed after the
41
+ * turn started (plans/DISCORD.md §3.2: Discord's answer thread exists only
42
+ * once the answer is shown, so the target is PUT again with it).
43
+ */
44
+ export interface SessionBinder<P> {
45
+ /** Whether the session was mapped: `false` where nothing binds (development's credentials). */
46
+ bindSession(sessionId: string, request: HostTurnRequest<P>): Promise<boolean>;
47
+ }
39
48
  /** Where a host asks from (`exchange.optional_request.place`): a channel, or a direct conversation. */
40
49
  export type Place = Readonly<{
41
50
  kind: 'channel' | 'direct';
@@ -154,13 +163,7 @@ export declare class HostStarters<K, P> implements StarterSource<K> {
154
163
  }
155
164
  /** Each AG-UI event of a run's `text/event-stream` body, as it arrives (`AgUiFrames`). */
156
165
  export declare function agUiEventsOf(body: ReadableStream<Uint8Array>): AsyncIterable<AgUiEvent>;
157
- /**
158
- * The host's Datalayer client: a turn is started or sent on the deployment's
159
- * session API (idempotent by the host's event id, SLACK §4.3, 2), its AG-UI
160
- * stream reduced to semantic events, and its session mapped to the host's
161
- * thread so that approvals and completions are delivered (§4.3, 3).
162
- */
163
- export declare class HttpAgentClient<P> implements AgentClient<P> {
166
+ export declare class HttpAgentClient<P> implements AgentClient<P>, SessionBinder<P> {
164
167
  #private;
165
168
  readonly apiUrl: string;
166
169
  readonly credentials: HostCredentials<P>;
@@ -169,6 +172,11 @@ export declare class HttpAgentClient<P> implements AgentClient<P> {
169
172
  readonly hostKeyFor?: HostKeyLookup<P> | undefined;
170
173
  constructor(apiUrl: string, credentials: HostCredentials<P>, shape: HostShape<P>, fetchImplementation?: typeof fetch, hostKeyFor?: HostKeyLookup<P> | undefined);
171
174
  startTurn(request: HostTurnRequest<P>): Promise<TurnStream>;
175
+ /**
176
+ * The session mapped again with the shape's target for `request`, as the
177
+ * speaker (a fresh exchange): the binding is replaced, never doubled.
178
+ */
179
+ bindSession(sessionId: string, request: HostTurnRequest<P>): Promise<boolean>;
172
180
  stopTurn(sessionId: string, request: HostTurnRequest<P>): Promise<void>;
173
181
  }
174
182
  /**
@@ -118,12 +118,16 @@ export class DevelopmentCredentials {
118
118
  async forTurn(request) {
119
119
  const response = await this.fetchImplementation(`${this.apiUrl}/api/ai-agents/v1/apps/deployments/${encodeURIComponent(request.applicationId)}`, { headers: { authorization: `Bearer ${this.token}` } });
120
120
  if (!response.ok) {
121
- throw new Error(`Datalayer did not answer the deployment (${response.status}).`);
121
+ throw new HostRefusedError(response.status, 'Datalayer did not answer the deployment.', 'The deployment');
122
122
  }
123
123
  const body = (await response.json());
124
124
  const kept = body.deployment?.kept;
125
125
  if (!kept?.url || !kept.agent_id) {
126
- throw new Error('The deployment is not kept always on: turn it on under Ship.');
126
+ // A refusal the host says to the person (409), with the deployment's
127
+ // own reason when it gives one (a daily credit limit spent, paused).
128
+ throw new HostRefusedError(409, kept?.why
129
+ ? `The deployment is not answering now: ${kept.why}`
130
+ : 'The deployment is not kept always on: turn it on under Ship.', 'The deployment');
127
131
  }
128
132
  return {
129
133
  accessToken: this.token,
@@ -203,6 +207,19 @@ export async function* agUiEventsOf(body) {
203
207
  * stream reduced to semantic events, and its session mapped to the host's
204
208
  * thread so that approvals and completions are delivered (§4.3, 3).
205
209
  */
210
+ /** A refusal's `detail` when the answer is FastAPI's JSON, its status alone otherwise. */
211
+ async function refusalDetailOf(response) {
212
+ const text = await response.text().catch(() => '');
213
+ try {
214
+ const detail = JSON.parse(text).detail;
215
+ if (typeof detail === 'string' && detail)
216
+ return detail;
217
+ }
218
+ catch {
219
+ // Not JSON: the status says it.
220
+ }
221
+ return `Datalayer refused the turn (${response.status}).`;
222
+ }
206
223
  export class HttpAgentClient {
207
224
  apiUrl;
208
225
  credentials;
@@ -251,7 +268,10 @@ export class HttpAgentClient {
251
268
  method: 'POST',
252
269
  });
253
270
  if (!response.ok || !response.body) {
254
- throw new Error(`Datalayer refused the turn (${response.status}).`);
271
+ // The runtime's own reason, said to the person by the host: an
272
+ // application that signs its users and got no user token, a spent
273
+ // limit, a session of someone else.
274
+ throw new HostRefusedError(response.status, await refusalDetailOf(response), 'The turn');
255
275
  }
256
276
  const hostKey = host.bindable && this.hostKeyFor
257
277
  ? await this.hostKeyFor(request)
@@ -266,6 +286,20 @@ export class HttpAgentClient {
266
286
  updates: this.#updates(response.body, host, sessionId, run),
267
287
  };
268
288
  }
289
+ /**
290
+ * The session mapped again with the shape's target for `request`, as the
291
+ * speaker (a fresh exchange): the binding is replaced, never doubled.
292
+ */
293
+ async bindSession(sessionId, request) {
294
+ const host = await this.credentials.forTurn(request);
295
+ const hostKey = host.bindable && this.hostKeyFor
296
+ ? await this.hostKeyFor(request)
297
+ : undefined;
298
+ if (!hostKey)
299
+ return false;
300
+ await this.#bind(request, host, sessionId, hostKey);
301
+ return true;
302
+ }
269
303
  async stopTurn(sessionId, request) {
270
304
  const host = await this.credentials.forTurn(request);
271
305
  assertTrustedStreamUrl(this.apiUrl, host.sessionApiUrl);
@@ -22,7 +22,7 @@ export * from '../semantic';
22
22
  export { askQuestionOf, semanticOf, turnOf, type TurnInfo, } from '../semanticOfAgUi';
23
23
  export * from './contracts';
24
24
  export { DatalayerHostClient, HostRefusedError, type BoundHost, type HostSelf, } from './client';
25
- export { agUiEventsOf, assertTrustedStreamUrl, DevelopmentCredentials, HostKeyCredentials, hostSessionOf, HostStarters, HttpAgentClient, InstallationCredentials, lastAnswer, type AgentClient, type HostCredentials, type HostKeyLookup, type HostSession, type HostShape, type HostTurnRequest, type Place, type Starter, type StarterSource, type TurnProvenance, type TurnStream, } from './agentClient';
25
+ export { agUiEventsOf, assertTrustedStreamUrl, DevelopmentCredentials, HostKeyCredentials, hostSessionOf, HostStarters, HttpAgentClient, InstallationCredentials, lastAnswer, type AgentClient, type HostCredentials, type HostKeyLookup, type HostSession, type HostShape, type HostTurnRequest, type Place, type SessionBinder, type Starter, type StarterSource, type TurnProvenance, type TurnStream, } from './agentClient';
26
26
  export { hostSessionUid } from './sessionUid';
27
27
  export { DATALAYER_ONLY, MAIL_WORDS, RENDERERS, renderApproval, type RenderedAction, type RenderedField, } from './approvalRenderers';
28
28
  export { DELIVERY_PATH, DELIVERY_ROUTE, DeliveryPoller, deliveryRoute, receiveDelivery, type BoundHostKey, type DeliveryClaimStore, type DeliveryJob, type DeliverySink, } from './deliveries';
@@ -40,6 +40,13 @@ export interface WorkQueue<T> {
40
40
  /** In-process development queue. A durable adapter is required in production. */
41
41
  export declare class MemoryWorkQueue<T> implements WorkQueue<T> {
42
42
  #private;
43
+ readonly onFailure: (error: unknown, item: T) => void;
44
+ /**
45
+ * A job that throws is reported here and dropped, and the next one is
46
+ * worked: one failure never stops the queue, nor the process with an
47
+ * unhandled rejection. The durable queue retries and dead-letters instead.
48
+ */
49
+ constructor(onFailure?: (error: unknown, item: T) => void);
43
50
  enqueue(item: T, idempotencyKey?: string): Promise<boolean>;
44
51
  ready(): Promise<boolean>;
45
52
  start(handler: (item: T) => Promise<void>): void;
@@ -11,10 +11,21 @@ export function tablePrefixOf(prefix) {
11
11
  }
12
12
  /** In-process development queue. A durable adapter is required in production. */
13
13
  export class MemoryWorkQueue {
14
+ onFailure;
14
15
  #items = [];
15
16
  #keys = new Set();
16
17
  #handler;
17
18
  #running = false;
19
+ /**
20
+ * A job that throws is reported here and dropped, and the next one is
21
+ * worked: one failure never stops the queue, nor the process with an
22
+ * unhandled rejection. The durable queue retries and dead-letters instead.
23
+ */
24
+ constructor(onFailure = error => {
25
+ console.error(`A job failed and was dropped: ${error instanceof Error ? error.message : String(error)}`);
26
+ }) {
27
+ this.onFailure = onFailure;
28
+ }
18
29
  async enqueue(item, idempotencyKey) {
19
30
  if (idempotencyKey) {
20
31
  if (this.#keys.has(idempotencyKey)) {
@@ -52,7 +63,12 @@ export class MemoryWorkQueue {
52
63
  }
53
64
  const item = this.#items.shift();
54
65
  if (item !== undefined) {
55
- await handler(item);
66
+ try {
67
+ await handler(item);
68
+ }
69
+ catch (error) {
70
+ this.onFailure(error, item);
71
+ }
56
72
  }
57
73
  }
58
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.5.50",
3
+ "version": "1.5.52",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",