@datalayer/agent-runtimes 1.5.45 → 1.5.47

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 (54) hide show
  1. package/dist-embed/datalayer-app-main.js +4 -4
  2. package/lib/portable/conversation.d.ts +6 -0
  3. package/lib/portable/conversation.js +29 -1
  4. package/lib/portable/hosts/__tests__/fixtures/lock-with-jupyter.json +40 -0
  5. package/lib/portable/hosts/__tests__/fixtures/npm-ls-with-jupyter.json +21 -0
  6. package/lib/portable/hosts/agentClient.d.ts +167 -0
  7. package/lib/portable/hosts/agentClient.js +346 -0
  8. package/lib/portable/hosts/approvalRenderers.d.ts +43 -0
  9. package/lib/portable/hosts/approvalRenderers.js +180 -0
  10. package/lib/portable/hosts/client.d.ts +95 -0
  11. package/lib/portable/hosts/client.js +174 -0
  12. package/lib/portable/hosts/contracts.d.ts +199 -0
  13. package/lib/portable/hosts/contracts.js +235 -0
  14. package/lib/portable/hosts/contracts.json +381 -0
  15. package/lib/portable/hosts/deadLetters.d.ts +41 -0
  16. package/lib/portable/hosts/deadLetters.js +124 -0
  17. package/lib/portable/hosts/deliveries.d.ts +76 -0
  18. package/lib/portable/hosts/deliveries.js +147 -0
  19. package/lib/portable/hosts/forbiddenGraph.d.ts +77 -0
  20. package/lib/portable/hosts/forbiddenGraph.js +174 -0
  21. package/lib/portable/hosts/health.d.ts +35 -0
  22. package/lib/portable/hosts/health.js +92 -0
  23. package/lib/portable/hosts/index.d.ts +40 -0
  24. package/lib/portable/hosts/index.js +45 -0
  25. package/lib/portable/hosts/observability.d.ts +67 -0
  26. package/lib/portable/hosts/observability.js +171 -0
  27. package/lib/portable/hosts/postgresQueue.d.ts +55 -0
  28. package/lib/portable/hosts/postgresQueue.js +217 -0
  29. package/lib/portable/hosts/queue.d.ts +47 -0
  30. package/lib/portable/hosts/queue.js +63 -0
  31. package/lib/portable/hosts/quotas.d.ts +81 -0
  32. package/lib/portable/hosts/quotas.js +147 -0
  33. package/lib/portable/hosts/rateLimit.d.ts +13 -0
  34. package/lib/portable/hosts/rateLimit.js +8 -0
  35. package/lib/portable/hosts/resolution.d.ts +54 -0
  36. package/lib/portable/hosts/resolution.js +46 -0
  37. package/lib/portable/hosts/schema.d.ts +19 -0
  38. package/lib/portable/hosts/schema.js +81 -0
  39. package/lib/portable/hosts/secretCipher.d.ts +23 -0
  40. package/lib/portable/hosts/secretCipher.js +45 -0
  41. package/lib/portable/hosts/sessionUid.d.ts +7 -0
  42. package/lib/portable/hosts/sessionUid.js +28 -0
  43. package/lib/portable/hosts/turnState.d.ts +45 -0
  44. package/lib/portable/hosts/turnState.js +113 -0
  45. package/lib/portable/index.d.ts +2 -0
  46. package/lib/portable/index.js +3 -0
  47. package/lib/portable/semantic-events.json +480 -0
  48. package/lib/portable/semantic.d.ts +78 -0
  49. package/lib/portable/semantic.js +100 -0
  50. package/lib/portable/semanticOfAgUi.d.ts +41 -0
  51. package/lib/portable/semanticOfAgUi.js +108 -0
  52. package/lib/protocols/A2AAdapter.js +19 -0
  53. package/lib/test-setup.js +17 -14
  54. package/package.json +1 -1
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The tables every host service keeps the same way, under its own prefix:
3
+ * ingress claims and the work queue (`<prefix>_event_claims`,
4
+ * `<prefix>_jobs`), quota counts (`<prefix>_usage`), privileged payload reads
5
+ * (`<prefix>_payload_access`) and dead-job replays (`<prefix>_job_replays`).
6
+ * Their final shapes, as idempotent statements: a service applies them
7
+ * before its own migrations, every time. TypeScript strings, since the npm
8
+ * tarball ships no `.sql`. Moved from the Slack service's migrations 001,
9
+ * 004 and 007 (plans/DISCORD.md DC-004).
10
+ *
11
+ * @module portable/hosts/schema
12
+ */
13
+ import { type Queryable } from './queue';
14
+ /** The shared tables of one host, as one idempotent script. */
15
+ export declare function hostSchemaSql(tablePrefix: string): string;
16
+ /** The tables `hostSchemaSql` makes, by name: what a test and an operator check. */
17
+ export declare function hostTables(tablePrefix: string): string[];
18
+ /** Apply the shared tables of one host: idempotent, before the service's own migrations. */
19
+ export declare function applyHostSchema(database: Queryable, tablePrefix: string): Promise<void>;
@@ -0,0 +1,81 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The tables every host service keeps the same way, under its own prefix:
7
+ * ingress claims and the work queue (`<prefix>_event_claims`,
8
+ * `<prefix>_jobs`), quota counts (`<prefix>_usage`), privileged payload reads
9
+ * (`<prefix>_payload_access`) and dead-job replays (`<prefix>_job_replays`).
10
+ * Their final shapes, as idempotent statements: a service applies them
11
+ * before its own migrations, every time. TypeScript strings, since the npm
12
+ * tarball ships no `.sql`. Moved from the Slack service's migrations 001,
13
+ * 004 and 007 (plans/DISCORD.md DC-004).
14
+ *
15
+ * @module portable/hosts/schema
16
+ */
17
+ import { tablePrefixOf } from './queue';
18
+ /** The shared tables of one host, as one idempotent script. */
19
+ export function hostSchemaSql(tablePrefix) {
20
+ const p = tablePrefixOf(tablePrefix);
21
+ return `BEGIN;
22
+
23
+ CREATE TABLE IF NOT EXISTS ${p}_event_claims (
24
+ event_id text PRIMARY KEY,
25
+ claimed_at timestamptz NOT NULL DEFAULT now()
26
+ );
27
+ CREATE INDEX IF NOT EXISTS ${p}_event_claims_claimed_at ON ${p}_event_claims (claimed_at);
28
+
29
+ CREATE TABLE IF NOT EXISTS ${p}_jobs (
30
+ id bigserial PRIMARY KEY,
31
+ payload jsonb NOT NULL,
32
+ state text NOT NULL DEFAULT 'ready' CHECK (state IN ('ready', 'processing', 'dead')),
33
+ attempts integer NOT NULL DEFAULT 0,
34
+ available_at timestamptz NOT NULL DEFAULT now(),
35
+ locked_at timestamptz,
36
+ last_error text,
37
+ created_at timestamptz NOT NULL DEFAULT now()
38
+ );
39
+ CREATE INDEX IF NOT EXISTS ${p}_jobs_ready ON ${p}_jobs (state, available_at, id);
40
+
41
+ CREATE TABLE IF NOT EXISTS ${p}_usage (
42
+ usage_key text NOT NULL,
43
+ usage_window text NOT NULL,
44
+ used integer NOT NULL DEFAULT 0,
45
+ PRIMARY KEY (usage_key, usage_window)
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS ${p}_payload_access (
49
+ id bigserial PRIMARY KEY,
50
+ job_id bigint NOT NULL,
51
+ operator text NOT NULL,
52
+ reason text NOT NULL,
53
+ read_at timestamptz NOT NULL DEFAULT now()
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS ${p}_job_replays (
57
+ id bigserial PRIMARY KEY,
58
+ job_id bigint NOT NULL,
59
+ operator text NOT NULL,
60
+ replayed boolean NOT NULL,
61
+ replayed_at timestamptz NOT NULL DEFAULT now()
62
+ );
63
+
64
+ COMMIT;
65
+ `;
66
+ }
67
+ /** The tables `hostSchemaSql` makes, by name: what a test and an operator check. */
68
+ export function hostTables(tablePrefix) {
69
+ const p = tablePrefixOf(tablePrefix);
70
+ return [
71
+ `${p}_event_claims`,
72
+ `${p}_jobs`,
73
+ `${p}_usage`,
74
+ `${p}_payload_access`,
75
+ `${p}_job_replays`,
76
+ ];
77
+ }
78
+ /** Apply the shared tables of one host: idempotent, before the service's own migrations. */
79
+ export async function applyHostSchema(database, tablePrefix) {
80
+ await database.query(hostSchemaSql(tablePrefix));
81
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Secrets at rest (SLACK §9, DISCORD §9): AES-256-GCM, each value bound to
3
+ * what it belongs to (its associated data, an installation's key), with the
4
+ * key's version kept beside it. Moved from the Slack service
5
+ * (plans/DISCORD.md DC-004).
6
+ *
7
+ * @module portable/hosts/secretCipher
8
+ */
9
+ export type EncryptedSecret = Readonly<{
10
+ ciphertext: string;
11
+ iv: string;
12
+ keyVersion: number;
13
+ tag: string;
14
+ }>;
15
+ export declare class SecretCipher {
16
+ #private;
17
+ readonly keyVersion: number;
18
+ constructor(keyBase64: string, keyVersion: number,
19
+ /** The setting the key is read from, named when it is refused. */
20
+ keyName?: string);
21
+ encrypt(value: string, associatedData: string): EncryptedSecret;
22
+ decrypt(secret: EncryptedSecret, associatedData: string): string;
23
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
6
+ export class SecretCipher {
7
+ keyVersion;
8
+ #key;
9
+ constructor(keyBase64, keyVersion,
10
+ /** The setting the key is read from, named when it is refused. */
11
+ keyName = 'The encryption key') {
12
+ this.keyVersion = keyVersion;
13
+ this.#key = Buffer.from(keyBase64, 'base64');
14
+ if (this.#key.length !== 32) {
15
+ throw new Error(`${keyName} must be a base64-encoded 32-byte key.`);
16
+ }
17
+ }
18
+ encrypt(value, associatedData) {
19
+ const iv = randomBytes(12);
20
+ const cipher = createCipheriv('aes-256-gcm', this.#key, iv);
21
+ cipher.setAAD(Buffer.from(associatedData));
22
+ const ciphertext = Buffer.concat([
23
+ cipher.update(value, 'utf8'),
24
+ cipher.final(),
25
+ ]);
26
+ return {
27
+ ciphertext: ciphertext.toString('base64'),
28
+ iv: iv.toString('base64'),
29
+ keyVersion: this.keyVersion,
30
+ tag: cipher.getAuthTag().toString('base64'),
31
+ };
32
+ }
33
+ decrypt(secret, associatedData) {
34
+ if (secret.keyVersion !== this.keyVersion) {
35
+ throw new Error(`No decryption key is configured for version ${secret.keyVersion}.`);
36
+ }
37
+ const decipher = createDecipheriv('aes-256-gcm', this.#key, Buffer.from(secret.iv, 'base64'));
38
+ decipher.setAAD(Buffer.from(associatedData));
39
+ decipher.setAuthTag(Buffer.from(secret.tag, 'base64'));
40
+ return Buffer.concat([
41
+ decipher.update(Buffer.from(secret.ciphertext, 'base64')),
42
+ decipher.final(),
43
+ ]).toString('utf8');
44
+ }
45
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `<kind>-<40 hex>`: the SHA-256 of the thread's parts joined by `:`, and
3
+ * `:<generation>` once the thread started over or switched application. The
4
+ * same parts give the same uid (Slack: app, enterprise or `-`, team,
5
+ * channel, thread ts; Discord: application, guild, channel, thread).
6
+ */
7
+ export declare function hostSessionUid(kind: string, parts: readonly string[], generation?: number): string;
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * One host thread is one Datalayer session: its uid, derived from where the
7
+ * thread is, so that a retry names it again (SLACK §2 rule 2, DISCORD §2
8
+ * rule 2). Moved from the Slack service (plans/DISCORD.md DC-004).
9
+ *
10
+ * @module portable/hosts/sessionUid
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ /**
14
+ * `<kind>-<40 hex>`: the SHA-256 of the thread's parts joined by `:`, and
15
+ * `:<generation>` once the thread started over or switched application. The
16
+ * same parts give the same uid (Slack: app, enterprise or `-`, team,
17
+ * channel, thread ts; Discord: application, guild, channel, thread).
18
+ */
19
+ export function hostSessionUid(kind, parts, generation) {
20
+ if (!/^[a-z][a-z0-9-]{1,31}$/.test(kind)) {
21
+ throw new Error(`A host kind is lowercase letters and digits: ${kind}.`);
22
+ }
23
+ if (parts.length === 0 || parts.some(part => part === '')) {
24
+ throw new Error('A thread is named by its parts, none of them empty.');
25
+ }
26
+ const key = `${parts.join(':')}${generation ? `:${generation}` : ''}`;
27
+ return `${kind}-${createHash('sha256').update(key).digest('hex').slice(0, 40)}`;
28
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A turn's state as a chat host shows it, from its semantic events: the
3
+ * answer so far, the tools, the status line, the approvals waited on, the
4
+ * question it ends on, and how it ended. Each host renders it in its own
5
+ * blocks (Slack's Block Kit, Discord's components); nothing here is a
6
+ * host's. Moved from the Slack service (plans/DISCORD.md DC-004).
7
+ *
8
+ * @module portable/hosts/turnState
9
+ */
10
+ import type { AskQuestion, SemanticEvent } from '../semantic';
11
+ export type TurnEnding = 'completion' | 'stop' | 'error';
12
+ export type TurnTool = Readonly<{
13
+ name: string;
14
+ status: string;
15
+ }>;
16
+ export type TurnFile = Readonly<{
17
+ mediaType?: string;
18
+ name: string;
19
+ url: string;
20
+ }>;
21
+ export declare class TurnState {
22
+ #private;
23
+ /** Whether the turn finished while it streamed: else a `turn.completed` delivery finishes it. */
24
+ get complete(): boolean;
25
+ /** Whether the turn ended (answered, stopped, failed) or waits on a question. */
26
+ get ended(): boolean;
27
+ /** Whether the turn still runs: not ended, and asking nothing. */
28
+ get running(): boolean;
29
+ /** The answer so far, its tables and citations as Markdown. */
30
+ get answer(): string;
31
+ /** The question the turn ends on, if any. */
32
+ get asking(): AskQuestion | undefined;
33
+ /** The tools whose approval the turn waited on, in order. */
34
+ get approvals(): readonly string[];
35
+ /** How the turn ended, if it did. */
36
+ get ending(): TurnEnding | undefined;
37
+ /** What the run said when it failed. */
38
+ get error(): string | undefined;
39
+ get files(): readonly TurnFile[];
40
+ /** The status line: what the turn is doing now. */
41
+ get status(): string;
42
+ /** Each tool the turn used, by its call, in the order first seen. */
43
+ get tools(): ReadonlyMap<string, TurnTool>;
44
+ apply(event: SemanticEvent): void;
45
+ }
@@ -0,0 +1,113 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export class TurnState {
6
+ #answer = '';
7
+ #asking;
8
+ #approvals = [];
9
+ #ended;
10
+ #error;
11
+ #files = [];
12
+ #status = 'Working…';
13
+ #tools = new Map();
14
+ /** Whether the turn finished while it streamed: else a `turn.completed` delivery finishes it. */
15
+ get complete() {
16
+ return this.#ended === 'completion';
17
+ }
18
+ /** Whether the turn ended (answered, stopped, failed) or waits on a question. */
19
+ get ended() {
20
+ return this.#ended !== undefined || this.#asking !== undefined;
21
+ }
22
+ /** Whether the turn still runs: not ended, and asking nothing. */
23
+ get running() {
24
+ return !this.ended;
25
+ }
26
+ /** The answer so far, its tables and citations as Markdown. */
27
+ get answer() {
28
+ return this.#answer;
29
+ }
30
+ /** The question the turn ends on, if any. */
31
+ get asking() {
32
+ return this.#asking;
33
+ }
34
+ /** The tools whose approval the turn waited on, in order. */
35
+ get approvals() {
36
+ return this.#approvals;
37
+ }
38
+ /** How the turn ended, if it did. */
39
+ get ending() {
40
+ return this.#ended;
41
+ }
42
+ /** What the run said when it failed. */
43
+ get error() {
44
+ return this.#error;
45
+ }
46
+ get files() {
47
+ return this.#files;
48
+ }
49
+ /** The status line: what the turn is doing now. */
50
+ get status() {
51
+ return this.#status;
52
+ }
53
+ /** Each tool the turn used, by its call, in the order first seen. */
54
+ get tools() {
55
+ return this.#tools;
56
+ }
57
+ apply(event) {
58
+ switch (event.kind) {
59
+ case 'text':
60
+ this.#answer += event.text;
61
+ break;
62
+ case 'progress':
63
+ this.#status = event.label;
64
+ break;
65
+ case 'tool':
66
+ this.#tools.set(event.id || event.name, {
67
+ name: event.name,
68
+ status: event.status,
69
+ });
70
+ this.#status = `${event.status === 'running' ? 'Using' : event.status === 'failed' ? 'Failed:' : 'Used'} ${event.name}`;
71
+ break;
72
+ case 'approval':
73
+ this.#approvals.push(event.toolName);
74
+ this.#status = `Waiting for approval to use ${event.toolName}`;
75
+ break;
76
+ case 'file':
77
+ this.#files.push({
78
+ mediaType: event.mediaType,
79
+ name: event.name,
80
+ url: event.url,
81
+ });
82
+ break;
83
+ case 'table':
84
+ this.#answer += `\n\n| ${event.columns.join(' | ')} |\n|${event.columns.map(() => '---').join('|')}|\n${event.rows.map(row => `| ${row.join(' | ')} |`).join('\n')}\n`;
85
+ break;
86
+ case 'citation':
87
+ this.#answer += `\n[${event.title}](${event.url})`;
88
+ break;
89
+ case 'ask':
90
+ this.#asking = event.question;
91
+ this.#status = 'Waiting for your answer';
92
+ break;
93
+ case 'completion':
94
+ // The streamed words are the answer: a completion's text only
95
+ // stands in for none, never repeated after them.
96
+ if (event.text && !this.#answer) {
97
+ this.#answer = event.text;
98
+ }
99
+ this.#ended = 'completion';
100
+ this.#status = 'Complete';
101
+ break;
102
+ case 'stop':
103
+ this.#ended = 'stop';
104
+ this.#status = 'Stopped';
105
+ break;
106
+ case 'error':
107
+ this.#ended = 'error';
108
+ this.#error = event.message;
109
+ this.#status = 'Could not finish';
110
+ break;
111
+ }
112
+ }
113
+ }
@@ -9,6 +9,8 @@
9
9
  export { messageOfApproval, normalizeApproval, whyAsked, type ApprovalArgs, type ApprovalRecord, type MessageAsked, } from './approvals';
10
10
  export { actionOf, APPROVAL_CARD_WORDS, approvalCardOf, argumentsShown, type ApprovalCard, type ApprovalCardProps, type ApprovalCardSource, } from './approvalCard';
11
11
  export { AgUiFrames, endsRun, eventsOfStream, NOT_STREAMING, REACTOR_UI_NAME, readRunStream, RUN_CUT_SHORT, toolResultOf, UI_ON_RUN, type AgUiEvent, type ToolResultSaid, } from './agui';
12
+ export { answerParts, isTerminal, SEMANTIC_EVENTS_VERSION, SEMANTIC_KINDS, type AnswerPart, type AskQuestion, type SemanticEvent, type SemanticKind, } from './semantic';
13
+ export { askQuestionOf, semanticOf, turnOf, type TurnInfo, } from './semanticOfAgUi';
12
14
  export { AG_UI_NOT_ALLOWED, AG_UI_NOT_ON_RUNTIME, agUiRefusal, refusalBody, refusalDetail, } from './agUiRefusal';
13
15
  export { ASKED_FOR_TAG, askedOf, RUN_END_WORDS } from './records';
14
16
  export { messagesOfThread, openThread, snapshotOfStream, THREAD_WORDS, } from './thread';
@@ -15,6 +15,9 @@ export { messageOfApproval, normalizeApproval, whyAsked, } from './approvals';
15
15
  export { actionOf, APPROVAL_CARD_WORDS, approvalCardOf, argumentsShown, } from './approvalCard';
16
16
  // A run's stream, read as every client reads it.
17
17
  export { AgUiFrames, endsRun, eventsOfStream, NOT_STREAMING, REACTOR_UI_NAME, readRunStream, RUN_CUT_SHORT, toolResultOf, UI_ON_RUN, } from './agui';
18
+ // A run's stream reduced to what a chat host shows (DISCORD DC-005, SLACK S-205).
19
+ export { answerParts, isTerminal, SEMANTIC_EVENTS_VERSION, SEMANTIC_KINDS, } from './semantic';
20
+ export { askQuestionOf, semanticOf, turnOf, } from './semanticOfAgUi';
18
21
  export { AG_UI_NOT_ALLOWED, AG_UI_NOT_ON_RUNTIME, agUiRefusal, refusalBody, refusalDetail, } from './agUiRefusal';
19
22
  // The record's words, and a person's conversation read again.
20
23
  export { ASKED_FOR_TAG, askedOf, RUN_END_WORDS } from './records';