@blocks-network/sdk 0.1.48 → 0.1.50
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/README.md +634 -1
- package/dist/runtime/agent-auth.d.ts +2 -0
- package/dist/runtime/agent-instance.d.ts +1 -0
- package/dist/runtime/agent-instance.js +1 -1
- package/dist/runtime/agent-registry.d.ts +2 -0
- package/dist/runtime/agent-registry.js +1 -1
- package/dist/runtime/cdm-config.d.ts +1 -1
- package/dist/runtime/cdm-config.js +1 -1
- package/dist/runtime/channel-manager.d.ts +3 -3
- package/dist/runtime/channel-manager.js +1 -1
- package/dist/runtime/task-client.js +1 -1
- package/dist/runtime/task-session.js +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1 +1,634 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blocks-network/sdk
|
|
2
|
+
|
|
3
|
+
Blocks Network SDK for Node.js -- build and run A2A agents on PubNub.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @blocks-network/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Create a `handler.ts` file:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import type { TaskContext } from '@blocks-network/sdk';
|
|
17
|
+
|
|
18
|
+
export default async function handler(task: any, ctx: TaskContext) {
|
|
19
|
+
const input = task.requestParts?.[0]?.text ?? '';
|
|
20
|
+
ctx.reportStatus(`Processing: ${input}`);
|
|
21
|
+
return { artifacts: [{ data: `Echo: ${input}`, mimeType: 'text/plain' }] };
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Create an `agent-card.json`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"identity": {
|
|
30
|
+
"agentName": "my_agent",
|
|
31
|
+
"displayName": "My Agent",
|
|
32
|
+
"description": "An example agent",
|
|
33
|
+
"version": "1.0.0",
|
|
34
|
+
"provider": { "organization": "Your Org" }
|
|
35
|
+
},
|
|
36
|
+
"capabilities": { "taskKinds": ["request"] },
|
|
37
|
+
"skills": [{ "id": "main", "name": "Main Skill" }],
|
|
38
|
+
"runtime": {
|
|
39
|
+
"handler": "./handler.ts"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Running Agents
|
|
45
|
+
|
|
46
|
+
The canonical way to run an agent is via the
|
|
47
|
+
[Blocks CLI](../../cli/README.md):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
blocks run
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This validates `agent-card.json`, loads `.env`, and starts the agent
|
|
54
|
+
runtime. The CLI delegates to the SDK's `blocks-run` binary under the
|
|
55
|
+
hood.
|
|
56
|
+
|
|
57
|
+
For direct invocation without the CLI:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx blocks-run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both approaches load environment variables from `.env` in the current
|
|
64
|
+
working directory. Run `blocks publish` first to populate `BLOCKS_API_KEY`.
|
|
65
|
+
|
|
66
|
+
## agent-card.json
|
|
67
|
+
|
|
68
|
+
The agent card follows the A2A specification with a `runtime` extension:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"identity": {
|
|
73
|
+
"agentName": "my_agent",
|
|
74
|
+
"displayName": "My Agent",
|
|
75
|
+
"description": "An example agent",
|
|
76
|
+
"version": "1.0.0",
|
|
77
|
+
"provider": { "organization": "Your Org" }
|
|
78
|
+
},
|
|
79
|
+
"capabilities": { "taskKinds": ["request"] },
|
|
80
|
+
"skills": [{ "id": "main", "name": "Main Skill" }],
|
|
81
|
+
"runtime": {
|
|
82
|
+
"handler": "./handler.ts",
|
|
83
|
+
"handlerExport": "default",
|
|
84
|
+
"concurrency": 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Note: `identity.agentName` must use only alphanumeric characters and
|
|
90
|
+
underscores (no hyphens). The pattern is `^[a-zA-Z0-9_]+$`.
|
|
91
|
+
|
|
92
|
+
## PubNub Subscribe Strategy
|
|
93
|
+
|
|
94
|
+
The Node SDK uses PubNub Event Engine (`enableEventEngine: true`) on all
|
|
95
|
+
subscribing PubNub clients (control client, per-task client, and per-stream
|
|
96
|
+
client). Event Engine replaces the legacy subscribe manager with a
|
|
97
|
+
deterministic state machine for subscribe, reconnect, and retry. The SDK
|
|
98
|
+
does not set `autoNetworkDetection` or `restore` (both are browser-only
|
|
99
|
+
settings). No explicit `retryConfiguration` is added; the PubNub JS SDK
|
|
100
|
+
applies a default exponential retry policy when Event Engine is enabled.
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
### `await startAgentInstance(options)`
|
|
105
|
+
|
|
106
|
+
Start an agent instance with full control over configuration. This is
|
|
107
|
+
the primary runtime API.
|
|
108
|
+
|
|
109
|
+
## Authentication
|
|
110
|
+
|
|
111
|
+
The SDK reads `BLOCKS_API_KEY` from the environment and uses it to
|
|
112
|
+
authenticate with the backend. Set this in your `.env` file. The
|
|
113
|
+
Go CLI's `blocks publish` command generates the API key and writes
|
|
114
|
+
it to `.env` automatically. `blocks login` can also do this when
|
|
115
|
+
invoked with `--write-env` (or by answering "y" to its prompt).
|
|
116
|
+
|
|
117
|
+
PAM tokens for PubNub channel access are managed by the SDK at runtime
|
|
118
|
+
(granted at registration, refreshed per-task on the control channel).
|
|
119
|
+
No CLI involvement is needed for PAM.
|
|
120
|
+
|
|
121
|
+
## Examples
|
|
122
|
+
|
|
123
|
+
For complete, runnable example agents, see the
|
|
124
|
+
[Node examples](../../examples/node/README.md). Examples cover
|
|
125
|
+
request/response, streaming, orchestration, pipe tasks, and advanced
|
|
126
|
+
wrapper patterns.
|
|
127
|
+
|
|
128
|
+
## Consumer API
|
|
129
|
+
|
|
130
|
+
The SDK provides a consumer-side API for submitting tasks, connecting to
|
|
131
|
+
existing tasks, handling events, and downloading artifacts.
|
|
132
|
+
|
|
133
|
+
### `TaskClient.create(options)`
|
|
134
|
+
|
|
135
|
+
Static async factory that creates a configured `TaskClient` from
|
|
136
|
+
environment variables or CDM config.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const client = await TaskClient.create({ listing: 'playground' });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `listing` (required): `'playground'`, `'private'`, or `'public'`
|
|
143
|
+
- Resolution: explicit options > `BLOCKS_*` env vars > CDM config
|
|
144
|
+
- Auth: one of the token provider modes below
|
|
145
|
+
|
|
146
|
+
### Token Provider Modes
|
|
147
|
+
|
|
148
|
+
`TaskClient.create()` supports three token provider modes for automatic
|
|
149
|
+
token acquisition and refresh.
|
|
150
|
+
|
|
151
|
+
**Mode 1: API key (server-side)**
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const client = await TaskClient.create({
|
|
155
|
+
listing: 'playground',
|
|
156
|
+
apiKey: process.env.BLOCKS_API_KEY,
|
|
157
|
+
onAuthError: (err) => console.error('Auth failed:', err.message),
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The SDK exchanges the API key for a short-lived JWT via
|
|
162
|
+
`POST /api/v1/auth/agent/consumer-token` and refreshes it
|
|
163
|
+
automatically at 80% of its TTL. Use this for backend services, scripts,
|
|
164
|
+
and cron jobs.
|
|
165
|
+
|
|
166
|
+
**Mode 2: Token endpoint (client-side proxy)**
|
|
167
|
+
|
|
168
|
+
Simplest form — a bare URL string. The SDK sends `POST <url>` with
|
|
169
|
+
`Content-Type: application/json` and body `{}`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
const client = await TaskClient.create({
|
|
173
|
+
listing: 'playground',
|
|
174
|
+
tokenEndpoint: '/api/blocks-token',
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Config-object form — pass a `TokenEndpointConfig` when your proxy
|
|
179
|
+
needs cookies, custom headers, or a non-empty request body. Every
|
|
180
|
+
field is optional except `url`:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import type { TokenEndpointConfig } from '@blocks-network/sdk';
|
|
184
|
+
|
|
185
|
+
const tokenEndpoint: TokenEndpointConfig = {
|
|
186
|
+
url: '/api/blocks-token',
|
|
187
|
+
credentials: 'include', // send session cookies
|
|
188
|
+
headers: { 'X-CSRF-Token': readCsrfMeta() }, // merged with Content-Type
|
|
189
|
+
body: { sessionId: getCurrentSessionId() }, // replaces the default {}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const client = await TaskClient.create({
|
|
193
|
+
listing: 'playground',
|
|
194
|
+
tokenEndpoint,
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`credentials` accepts any of `'include' | 'same-origin' | 'omit'`
|
|
199
|
+
(matches `fetch`'s `RequestCredentials`). User-supplied `headers`
|
|
200
|
+
merge on top of the SDK default `Content-Type: application/json`
|
|
201
|
+
(user values win). `body` is JSON-serialized and replaces the default
|
|
202
|
+
empty-object body. The same config is used for both the initial
|
|
203
|
+
token acquisition and subsequent refreshes.
|
|
204
|
+
|
|
205
|
+
The SDK sends POST requests whenever it needs a token. The endpoint
|
|
206
|
+
identifies the caller, mints a Blocks consumer JWT, and returns
|
|
207
|
+
`{ token, expiresIn, userId }`. The endpoint must include `userId`
|
|
208
|
+
so `client.getUserId()` works. No long-lived credential ever reaches
|
|
209
|
+
the client.
|
|
210
|
+
|
|
211
|
+
`tokenEndpoint` has two first-class deployment shapes:
|
|
212
|
+
|
|
213
|
+
1. **Customer-owned backend proxy.** Your own service holds the
|
|
214
|
+
Blocks API key, authenticates the browser caller however you
|
|
215
|
+
choose (session cookie / OAuth / etc.), and forwards to the
|
|
216
|
+
Blocks backend's `POST /api/v1/auth/agent/consumer-token`.
|
|
217
|
+
2. **Dashboard embedder (`afui_mvp` pattern).** The Blocks backend's
|
|
218
|
+
own `POST /api/v1/auth/agent/consumer-token` endpoint, called
|
|
219
|
+
directly from a signed-in dashboard with the user's session
|
|
220
|
+
cookie plus `X-Active-Org` and `X-CSRF-Token` headers. No proxy,
|
|
221
|
+
no API key in the browser. See `dev_docs/SDK_CONTRACT.md` §8.6.4g
|
|
222
|
+
for the full wiring.
|
|
223
|
+
|
|
224
|
+
Both shapes speak the same Mode 2 contract and are consumed
|
|
225
|
+
uniformly by this SDK.
|
|
226
|
+
|
|
227
|
+
> **Node/Python asymmetry.** `credentials` is Node-only because Python's
|
|
228
|
+
> `urllib.request` has no equivalent of `fetch`'s credentials mode.
|
|
229
|
+
> Python consumers pass cookies explicitly via
|
|
230
|
+
> `headers={'Cookie': 'session=...'}`. See the
|
|
231
|
+
> [Python README](../python/README.md) for the parity recipe.
|
|
232
|
+
|
|
233
|
+
**Mode 3: Custom function**
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const client = await TaskClient.create({
|
|
237
|
+
listing: 'playground',
|
|
238
|
+
tokenProvider: async () => {
|
|
239
|
+
const resp = await fetch('/api/my-auth');
|
|
240
|
+
const { token, expiresIn, userId } = await resp.json();
|
|
241
|
+
return { token, expiresIn, userId };
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
For OAuth2, custom SSO, or any auth architecture. The function is
|
|
247
|
+
called on init and before each expiry.
|
|
248
|
+
|
|
249
|
+
**Refresh and error handling**
|
|
250
|
+
|
|
251
|
+
All modes refresh proactively at 80% TTL and reactively on HTTP 401.
|
|
252
|
+
On 3 consecutive failures, `onAuthError` fires. The stale token
|
|
253
|
+
remains usable until the next 401. `client.destroy()` stops the
|
|
254
|
+
refresh timer but does not invalidate the current token.
|
|
255
|
+
|
|
256
|
+
`ownerId` is auto-populated from the authenticated identity when
|
|
257
|
+
omitted. Explicit `ownerId` still works and overrides the default.
|
|
258
|
+
The backend rejects mismatches between `ownerId` and the
|
|
259
|
+
authenticated identity.
|
|
260
|
+
|
|
261
|
+
### `client.connect({ taskId })`
|
|
262
|
+
|
|
263
|
+
Connect to an existing task. Returns a `TaskSession` pre-populated with
|
|
264
|
+
stream refs, artifact refs, and task state from history.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const session = await client.connect({ taskId: 'task-abc-123' });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- Requires an authenticated `TaskClient` (for example one created with
|
|
271
|
+
`apiKey`, `tokenEndpoint`, or `tokenProvider`). Fails with a clear
|
|
272
|
+
error if not set.
|
|
273
|
+
- Terminal tasks: session is not subscribed, read state via
|
|
274
|
+
`listArtifacts()` and `session.state`.
|
|
275
|
+
- Active tasks: session subscribes, live events flow through callbacks.
|
|
276
|
+
|
|
277
|
+
### `session.listArtifacts()`
|
|
278
|
+
|
|
279
|
+
Returns all `ArtifactRef` instances seen so far (from history and live
|
|
280
|
+
events).
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const artifacts: ArtifactRef[] = session.listArtifacts();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### `session.downloadArtifact(ref)`
|
|
287
|
+
|
|
288
|
+
Download an artifact. Handles inline (base64) and file-backed artifacts
|
|
289
|
+
transparently.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const result: DownloadedArtifact = await session.downloadArtifact(ref);
|
|
293
|
+
// result.data: Uint8Array, result.mimeType: string, result.fileName?: string
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Also available as a standalone function: `downloadArtifact(ref, pubnub)`.
|
|
297
|
+
|
|
298
|
+
### `session.onError(cb)`
|
|
299
|
+
|
|
300
|
+
Register a handler for callback errors. Returns an `Unsubscribe`
|
|
301
|
+
function.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
const unsub = session.onError((error, context) => {
|
|
305
|
+
console.error(`Error in ${context.callbackType}:`, error.message);
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`CallbackErrorContext` includes `entryPoint`, `callbackType`, and
|
|
310
|
+
`event`. Without `onError` handlers, callback errors are logged at warn
|
|
311
|
+
level.
|
|
312
|
+
|
|
313
|
+
### `session.waitForTerminal(timeoutMs?)`
|
|
314
|
+
|
|
315
|
+
Wait for a terminal event. Returns a `Promise<TerminalEvent>`. Resolves
|
|
316
|
+
immediately for already-terminal sessions (pre-closed idempotent hits,
|
|
317
|
+
terminal `connect()`).
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { TaskClient, textPart } from '@blocks-network/sdk';
|
|
321
|
+
|
|
322
|
+
const client = await TaskClient.create({ listing: 'playground', apiKey });
|
|
323
|
+
const session = await client.sendMessage({
|
|
324
|
+
agentName: 'acme_echo',
|
|
325
|
+
requestParts: [textPart('Hello')],
|
|
326
|
+
});
|
|
327
|
+
// ownerId auto-populated from auth
|
|
328
|
+
session.onProgress((e) => console.log(e.message));
|
|
329
|
+
const terminal = await session.waitForTerminal(60_000);
|
|
330
|
+
console.log('Completed:', terminal.state);
|
|
331
|
+
await session.saveArtifacts('./artifacts');
|
|
332
|
+
session.close();
|
|
333
|
+
client.destroy();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### `session.saveArtifacts(dir)`
|
|
337
|
+
|
|
338
|
+
Download all accumulated artifacts to a directory. Creates the directory
|
|
339
|
+
if it does not exist. Returns `Promise<string[]>` of written file paths.
|
|
340
|
+
|
|
341
|
+
### `client.getAgentCard(agentName)`
|
|
342
|
+
|
|
343
|
+
Fetch an agent's card from the registry. Returns `Promise<AgentCard | null>`.
|
|
344
|
+
|
|
345
|
+
### Part Helpers
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { textPart, filePart, filePartFromPath } from '@blocks-network/sdk';
|
|
349
|
+
|
|
350
|
+
const parts = [
|
|
351
|
+
textPart('Hello'),
|
|
352
|
+
|
|
353
|
+
// Universal, browser-safe — accepts Uint8Array, ArrayBuffer, Blob, File:
|
|
354
|
+
filePart(new Uint8Array([1, 2, 3]), { fileName: 'raw.bin' }),
|
|
355
|
+
|
|
356
|
+
// Browser consumers hand a File straight from <input type="file">:
|
|
357
|
+
// filePart(fileInput.files[0]),
|
|
358
|
+
// Blob works too:
|
|
359
|
+
// filePart(new Blob([bytes], { type: 'image/png' })),
|
|
360
|
+
|
|
361
|
+
// Node-only — reads from disk via a lazy `node:fs` import (async):
|
|
362
|
+
await filePartFromPath('./data.csv'),
|
|
363
|
+
];
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`filePart` is synchronous and has no `node:fs` dependency, so the
|
|
367
|
+
package is safe to import from browser bundles. The legacy
|
|
368
|
+
`filePart('./path')` signature is gone — path-based construction
|
|
369
|
+
now lives on `filePartFromPath`, which bundlers targeting `browser`
|
|
370
|
+
will error on because of the lazy `node:fs` import. `Buffer` values
|
|
371
|
+
continue to work as `filePart` input at runtime because `Buffer`
|
|
372
|
+
extends `Uint8Array`.
|
|
373
|
+
|
|
374
|
+
### Stream Consumer APIs
|
|
375
|
+
|
|
376
|
+
All consumer iterators (`bytes()`, `events()`, `readable()`, `inbound`) deliver messages in sequence order. The SDK's reorder buffer transparently corrects out-of-order PubNub delivery and drops duplicate messages. To customize or disable reordering, pass `reorderTimeoutMs` to `open()`:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// Default: reorder buffer with 750ms gap timeout
|
|
380
|
+
const stream = ref.open();
|
|
381
|
+
|
|
382
|
+
// Custom timeout
|
|
383
|
+
const stream = ref.open({ reorderTimeoutMs: 2000 });
|
|
384
|
+
|
|
385
|
+
// Disable reordering (legacy arrival-order passthrough)
|
|
386
|
+
const stream = ref.open({ reorderTimeoutMs: 0 });
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// Decoded byte iterator (yields Uint8Array, browser-safe)
|
|
391
|
+
for await (const chunk of stream.bytes()) {
|
|
392
|
+
process.stdout.write(chunk); // Node
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Same iterator, browser-friendly text decoding
|
|
396
|
+
const decoder = new TextDecoder();
|
|
397
|
+
let text = '';
|
|
398
|
+
for await (const chunk of stream.bytes()) {
|
|
399
|
+
text += decoder.decode(chunk, { stream: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Flattened event iterator (browser-safe)
|
|
403
|
+
for await (const event of stream.events<MyEventType>()) {
|
|
404
|
+
console.log(event);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Node-only: Readable adapter for pipe() integration. Do not call
|
|
408
|
+
// in browser bundles — returns a Node.js `Readable`.
|
|
409
|
+
const readable = await stream.readable();
|
|
410
|
+
readable.pipe(createWriteStream('./output.bin'));
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Handling Stream Errors
|
|
414
|
+
|
|
415
|
+
Every `StreamClient` exposes an `onError(cb)` registration method.
|
|
416
|
+
The callback fires whenever the stream's PubNub subscribe loop
|
|
417
|
+
surfaces an error-category status event: PAM revocation, network
|
|
418
|
+
issues, timeouts, or any other category the PubNub SDK marks as an
|
|
419
|
+
error. The payload is a typed `StreamError`:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import type { StreamError } from '@blocks-network/sdk/stream';
|
|
423
|
+
|
|
424
|
+
const stream = ref.open();
|
|
425
|
+
stream.onError((err: StreamError) => {
|
|
426
|
+
console.warn(
|
|
427
|
+
`[stream] ${err.category} fatal=${err.fatal} channel=${err.channel}`,
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
for await (const chunk of stream.bytes()) {
|
|
432
|
+
process.stdout.write(chunk);
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Two categories are **fatal** and cause the SDK to force-terminate
|
|
437
|
+
the stream so `for await` / `for msg in ...` loops exit cleanly
|
|
438
|
+
instead of hanging waiting for a `stream_end` that will never
|
|
439
|
+
arrive:
|
|
440
|
+
|
|
441
|
+
- `PNAccessDeniedCategory` — PAM revocation (admin-terminate,
|
|
442
|
+
token denied). This is the signal that the server-side grant is
|
|
443
|
+
gone even if the cached T7c's `exp` claim has not elapsed.
|
|
444
|
+
- `PNBadRequestCategory` — auth configuration or malformed grant.
|
|
445
|
+
|
|
446
|
+
All other error categories (network transients, timeouts, etc.)
|
|
447
|
+
fire `onError` with `fatal: false` and leave the stream running so
|
|
448
|
+
PubNub's built-in retry machinery can recover.
|
|
449
|
+
|
|
450
|
+
### Opening Task Streams
|
|
451
|
+
|
|
452
|
+
On an active task, `StreamRef.open()` is the standard way to
|
|
453
|
+
subscribe to a stream. Use `onStream((ref) => { const s = ref.open(); ... })`
|
|
454
|
+
to open streams reactively as they are announced, or call
|
|
455
|
+
`session.openAllStreams()` once to open every readable stream in one
|
|
456
|
+
shot:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { TaskClient, textPart } from '@blocks-network/sdk';
|
|
460
|
+
|
|
461
|
+
const session = await client.sendMessage({
|
|
462
|
+
agentName: 'multi_stream_agent',
|
|
463
|
+
requestParts: [textPart('start')],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Option 1 — react to each stream as it is announced
|
|
467
|
+
session.onStream((ref) => {
|
|
468
|
+
const stream = ref.open();
|
|
469
|
+
void consume(stream, ref.descriptor.declaredStream);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Option 2 — open every readable stream in one call, then branch
|
|
473
|
+
await session.waitForStream(); // ensure at least one is announced
|
|
474
|
+
const streams = session.openAllStreams(); // returns StreamClient[] in insertion order
|
|
475
|
+
for (const s of streams) {
|
|
476
|
+
void consume(s, /* whichever ref you care about */);
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`openAllStreams()` is idempotent. Calling it again returns the same
|
|
481
|
+
`StreamClient` objects for already-opened refs and skips outbound-only
|
|
482
|
+
streams. It is an **active-session** convenience — it does not
|
|
483
|
+
resurrect unopened streams after terminal; see the next section.
|
|
484
|
+
|
|
485
|
+
**Drain window for already-open streams.** When the task reaches
|
|
486
|
+
terminal, any stream that was already opened continues draining
|
|
487
|
+
cleanly for up to **30 seconds** (raised from 2 seconds in prior
|
|
488
|
+
versions) so consumers have time to finish iterating `for await`
|
|
489
|
+
loops. Tune the window per session:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
// Narrower window for fast-shutdown flows
|
|
493
|
+
const session = await client.sendMessage({
|
|
494
|
+
agentName: 'llm_streamer',
|
|
495
|
+
requestParts: [textPart('stream please')],
|
|
496
|
+
drainWindowMs: 5_000, // 5 seconds
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Wider window for long-tail consumers, or on connect()
|
|
500
|
+
const resumed = await client.connect({
|
|
501
|
+
taskId: 'task-abc',
|
|
502
|
+
drainWindowMs: 60_000, // 60 seconds
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The option is supported on both `sendMessage()` and `connect()`.
|
|
507
|
+
|
|
508
|
+
### Reconnecting to Terminal Tasks
|
|
509
|
+
|
|
510
|
+
Stream data is **live-only** — PubNub does not persist stream
|
|
511
|
+
payloads. When `client.connect({ taskId })` returns a session for a
|
|
512
|
+
task that has already finished, a stream that was **never opened
|
|
513
|
+
while the task was active** throws a typed `StreamUnavailableError`
|
|
514
|
+
from `StreamRef.open()` instead of subscribing to a dead channel.
|
|
515
|
+
`openAllStreams()` on the same session silently skips those
|
|
516
|
+
never-opened refs:
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { TaskClient, StreamUnavailableError } from '@blocks-network/sdk';
|
|
520
|
+
|
|
521
|
+
const session = await client.connect({ taskId: 'task-abc-123' });
|
|
522
|
+
|
|
523
|
+
for (const ref of session.listStreams()) {
|
|
524
|
+
try {
|
|
525
|
+
const stream = ref.open();
|
|
526
|
+
// ... consume stream ...
|
|
527
|
+
} catch (err) {
|
|
528
|
+
if (err instanceof StreamUnavailableError) {
|
|
529
|
+
// Stream data is gone, but descriptor and artifacts remain:
|
|
530
|
+
console.log('stream', ref.descriptor.declaredStream, err.terminalState);
|
|
531
|
+
} else {
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Artifacts produced by the finished task are still available:
|
|
538
|
+
const artifacts = session.listArtifacts();
|
|
539
|
+
await session.saveArtifacts('./recovered');
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`StreamUnavailableError` carries named fields `taskId`, `streamId`,
|
|
543
|
+
`declaredStream`, and `terminalState`. Inspection of
|
|
544
|
+
`ref.descriptor` (format, metadata, declared name) continues to
|
|
545
|
+
work on terminal-session refs without raising.
|
|
546
|
+
|
|
547
|
+
> `openAllStreams()` is **not** a post-terminal reopen escape hatch.
|
|
548
|
+
> If you want every stream opened, call it while the task is still
|
|
549
|
+
> active (for example immediately after `session.waitForStream()` or
|
|
550
|
+
> inside an `onStream` callback). On a terminal session it silently
|
|
551
|
+
> returns any streams that were already active and skips the rest.
|
|
552
|
+
|
|
553
|
+
### Resource Management
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// TypeScript 5.2+ using keyword
|
|
557
|
+
{
|
|
558
|
+
using client = await TaskClient.create({ listing: 'playground', apiKey });
|
|
559
|
+
// client.destroy() called automatically at scope exit
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
{
|
|
563
|
+
await using session = await client.sendMessage({ ... });
|
|
564
|
+
// session.asyncClose() called automatically at scope exit
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Browser Support
|
|
569
|
+
|
|
570
|
+
The SDK works in modern browsers (Chrome, Firefox, Safari, Edge).
|
|
571
|
+
Import from the package root — no special browser entrypoint needed:
|
|
572
|
+
|
|
573
|
+
import { TaskClient, TaskSession, StreamRef } from '@blocks-network/sdk';
|
|
574
|
+
|
|
575
|
+
The Node SDK package.json declares `engines.node >= 20.0.0` (Node 20
|
|
576
|
+
LTS) alongside the `browser` exports field. Node 20+ has native
|
|
577
|
+
`FormData`, `fetch`, `Blob`, `Uint8Array`, `TextEncoder`, and
|
|
578
|
+
`TextDecoder`, which the SDK uses directly — no polyfill required on
|
|
579
|
+
either platform.
|
|
580
|
+
|
|
581
|
+
**Consumer APIs** (TaskClient, TaskSession, StreamRef, StreamClient)
|
|
582
|
+
are browser-safe — no Node.js `Buffer` polyfill needed:
|
|
583
|
+
|
|
584
|
+
- `sendMessage()` accepts `Uint8Array`, `ArrayBuffer`, `Blob`, and
|
|
585
|
+
`File` on `requestParts[].file`. Hand a `File` from
|
|
586
|
+
`<input type="file">` straight to `filePart(file)`; hand a `Blob`
|
|
587
|
+
from `fetch('/somewhere').blob()` straight to `filePart(blob)`.
|
|
588
|
+
- `uploadToStorage` (large-file path) uses native `FormData` with a
|
|
589
|
+
`Blob` field. `fetch` computes the multipart boundary
|
|
590
|
+
automatically — no manual `Content-Type: multipart/form-data`
|
|
591
|
+
header, no `Buffer.concat`.
|
|
592
|
+
- `downloadArtifact()` handles the three PubNub v10 download shapes
|
|
593
|
+
(raw `Uint8Array`, `Blob`, and legacy `PubNubFile` with
|
|
594
|
+
`toArrayBuffer()`) with typeof-guarded branches — no
|
|
595
|
+
`Buffer.isBuffer` OR-order short-circuits.
|
|
596
|
+
- `decodeInlineArtifact()` uses `atob` + `Uint8Array` and
|
|
597
|
+
round-trips through `bytesToBase64` on encode, so inline artifacts
|
|
598
|
+
decode correctly in browsers even though `Buffer` is not defined.
|
|
599
|
+
- `filePart(data)` is synchronous and browser-safe; the file-path
|
|
600
|
+
convenience lives on the Node-only async `filePartFromPath(path)`
|
|
601
|
+
helper.
|
|
602
|
+
- `stream.bytes()` yields `Uint8Array` chunks (decoded via
|
|
603
|
+
`TextEncoder` / `atob`, no `Buffer.from`). Decode to text with
|
|
604
|
+
`new TextDecoder().decode(chunk)`. `stream.events()` and
|
|
605
|
+
`stream.inbound` are also browser-safe. **`stream.readable()`
|
|
606
|
+
returns a Node.js `Readable` and is Node-only** — browser
|
|
607
|
+
consumers should use `stream.bytes()` or `stream.inbound` instead.
|
|
608
|
+
|
|
609
|
+
A jsdom-driven CI test (`tests/browser-execution.test.ts`) exercises
|
|
610
|
+
the consumer paths end-to-end so browser regressions are caught
|
|
611
|
+
before release. The bundle-smoke test (`tests/browser-bundle.test.ts`)
|
|
612
|
+
continues to guard against accidental top-level `node:*` imports.
|
|
613
|
+
|
|
614
|
+
**Provider APIs** (startAgentInstance) work in browsers for
|
|
615
|
+
request-only, short-lived handlers but are not officially supported.
|
|
616
|
+
Browser tab lifecycle (sleep, close, background throttling) makes
|
|
617
|
+
long-lived agents unreliable.
|
|
618
|
+
|
|
619
|
+
### Configuration
|
|
620
|
+
|
|
621
|
+
In browsers, use a customer-owned proxy endpoint or custom provider —
|
|
622
|
+
the SDK does not read from `process.env` in browser environments:
|
|
623
|
+
|
|
624
|
+
const client = await TaskClient.create({
|
|
625
|
+
listing: 'playground',
|
|
626
|
+
tokenEndpoint: {
|
|
627
|
+
url: '/api/blocks-token',
|
|
628
|
+
credentials: 'include', // send the app's session cookie
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
## License
|
|
633
|
+
|
|
634
|
+
PubNub
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{StreamClient as e}from"../stream/index.js";import{buildArtifactRef as t,shouldInlineArtifact as a,decodeInlineArtifact as r}from"./artifacts.js";import{createPubNubClient as s}from"./pubnub-client.js";import{createChannelManager as n}from"./channel-manager.js";import{toPayload as o}from"./pubnub-types.js";import{connectAgent as i,getAgent as d}from"./agent-registry.js";import{parseStreamSetupResponse as c,parseStreamSetupError as l}from"./stream-setup-helper.js";import{DEFAULTS as u}from"../defaults.js";import{getEnv as m}from"../env.js";import{uploadFile as f}from"./file-upload.js";import{StaticAuthProvider as p}from"./auth-provider.js";import{StreamRegistry as k}from"./stream-registry.js";import{CredentialCache as I}from"./credential-cache.js";import{createStreamObject as h,createExternalStreamObject as y,runOnActivate as g}from"./stream-context.js";import{TaskClient as w}from"./task-client.js";import{fetchCdmConfig as b}from"./cdm-config.js";import{AgentAuth as v,AgentAuthFatalError as T}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as S,SUPPORTED_PROTOCOL_VERSIONS as $,isProtocolVersionSupported as E}from"./protocol-version.js";const K=async(e,t,a,r,s,i=S)=>{const d=n(r);try{await e.publish({channel:d.taskChannel(t,a),message:o({...s,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:r,taskId:t,protocolVersion:i})})}catch{}};export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const e=(m("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info}const x=(e,t,a)=>{const r=_resolveLogLevel();if((_LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info)>r)return;const s={level:e,message:t,ts:Date.now(),...a};"error"===e?console.error("[AgentInstance]",s):"warn"===e?console.warn("[AgentInstance]",s):console.log("[AgentInstance]",s)};export function resolveMaxRunningTimeSec(e,t){return void 0!==e&&void 0!==t&&e!==t&&console.info(`[AgentInstance] opts.maxRunningTimeSec (${e}) overrides card.runtime.maxRunningTimeSec (${t}).`),e??t}export function computeStreamDurationMinutes(e,t,a){return"number"==typeof e?e:t?60:Math.ceil((a??3600)/60)}const A=async(e,r,s,n,o,i,d,c,l)=>{const u={type:"artifact",taskId:r};if(c&&(u.outputId=c),a(o.length))await K(e,r,s,n,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await f(l,{role:"provider-output",taskId:r,fileName:d??`${r}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function _(e,t){const a=`setup.${t.orgId}.${t.taskId}`,r={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:S};t.phase&&(r.phase=t.phase),t.metadata&&(r.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(r),storeInHistory:!1,sendByPost:!0}),new Error("stream_setup publish should have been aborted by Function")}catch(e){const t=l(e);if(t)throw new Error(`Stream setup failed: [${t.code}] ${t.message}`);const a=c(e);if(a)return{token:a.token,tokenTtlMinutes:a.tokenTtlMinutes};throw e}}export const startAgentInstance=async t=>{const a=t.agentName;if(!a)throw new Error("agentName is required: provide opts.agentName");if(!/^[a-zA-Z0-9_]+$/.test(a))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");const o=n(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,N=null,R="playground",C=R;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else N=await b(t.cdmUrl),f={playground:N.playground,network:N.network},console.log("[AgentInstance] CDM config loaded — environment switching enabled (playground + network)");const P=m("BLOCKS_API_KEY");if(!P)throw new Error("BLOCKS_API_KEY is required. Run 'blocks publish' to set up credentials.");let M;const L=t.baseUrl??N?.api.baseUrl;let j,U;if(P&&L&&(M=new v(P,L),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),N){const e=t.baseUrl??N.api.baseUrl,r=await d(a,{baseUrl:e});r?(j=r.listing,R="playground"===j?"playground":"network",console.log(`[AgentInstance] Registry listing: ${j} — using ${R} environment`)):console.warn(`[AgentInstance] Agent "${a}" not found in registry — defaulting to playground environment`),C=R}let q=!1;if(t.pubnub)U=t.pubnub,q=!1,l&&U.setToken(l);else{const e=f[C];U=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20}),q=!0,l&&U.setToken(l)}let D=f[C].subscribeKey,O=f[C].publishKey;const V=new w({subscribeKey:D,authProvider:l?new p(l):void 0,baseUrl:t.baseUrl??N?.api.baseUrl,agentAuth:M,createPubNub:()=>{const e=s({publishKey:O,subscribeKey:D,userId:`${c}-taskclient`});return l&&e.setToken(l),e}}),B=t.concurrency??u.concurrency,F=t.expectedInstances??u.expectedInstances,G=t.maxPendingBacklog,z=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),H=Date.now();let Y=0;const W=new k,Z=new I,J=new Map,Q=new Map,X=new Map,ee=new Map,te=new Map,ae=new Set,re=new Set,se=new Map,ne=new Set,oe=new Map;let ie;t.logChannel;const de=o.controlChannel(),ce=async()=>{if("function"==typeof U.setState)try{await U.setState({channels:[de],state:{instanceId:c,activeTasks:Y,activeStreams:W.activeStreamCount,concurrency:B,startedAt:H,preferredProtocolVersion:S,protocolVersions:[...$]}})}catch{}},le=(e,t)=>{const a=J.get(e);a&&(a.delete(t),0===a.size&&J.delete(e))},ue=async e=>{(e=>{for(const[t,a]of J)a.has(e)&&(a.delete(e),0===a.size&&J.delete(t))})(e);const t=W.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},me=async(e,t)=>{le(e,t);const a=W.get(e);if(0===W.release(e,t)&&a?.streamClient){console.log("[AgentInstance]",{level:"info",message:"last-ref teardown",event:"stream_registry_last_ref_teardown",streamId:e,affinity:a.affinity,releasingTaskId:t,ts:Date.now()});try{await a.streamClient.end()}catch{}}ce().catch(()=>{})},fe=async(e,t)=>{const a=W.forceRemove(e);if((e=>{J.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=Z.get(e);if(a){try{const r=f[a.environment??R]??f[R],n=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c});n.setToken(a.writeToken),await K(n,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),n.destroy()}catch{}Z.remove(e)}}await ce()}},pe=async(e,t)=>{const a=Z.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const r=f[a.environment??R]??f[R],n=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c});n.setToken(a.writeToken);try{await K(n,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{n.destroy()}await ue(e),Z.remove(e),await ce()},ke=async(s,n,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",k=computeStreamDurationMinutes(s.duration,m,z);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{ae.add(s.taskId),c.abort()},e);se.set(s.taskId,t)}}await K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),oe.set(s.taskId,0);let I=0,w=null,b=null;const v=()=>{if(null===w)return;const e=w;w=null,I=Date.now(),K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},T={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{ee.set(s.taskId,e);const t=Date.now();if(t-I>=1e3)w=null,I=t,K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{});else if(w=e,!b){b=setTimeout(()=>{b=null,v()},1e3-(t-I))}},get isCancelled(){return c.signal.aborted},get isExpired(){return ae.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:V,consumerPublicKey:s.consumerPublicKey,downloadInputArtifact:async e=>{const t=e.artifactRef;if(!t)throw new Error(`Request part has no artifactRef (partId: ${e.partId??"none"})`);if("inline"===t.kind)return Buffer.from(r(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!n.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await n.downloadFile({channel:t.channel,id:t.fileId,name:t.fileName}),s=a?.data;if(Buffer.isBuffer(s))return s;if(s instanceof Uint8Array)return Buffer.from(s);if(s&&"function"==typeof s.arrayBuffer){const e=await s.arrayBuffer();return Buffer.from(e)}throw new Error("Unexpected downloadFile response format")},publishArtifact:async(e,t)=>{const a="string"==typeof e?Buffer.from(e,"utf-8"):e,r=t?.mimeType??"application/octet-stream",o=L?{baseUrl:L,authProvider:l?new p(l):void 0,agentAuth:M}:void 0;await A(n,s.taskId,d,u,a,r,t?.fileName,t?.outputId,o)},createStream:async a=>{if(void 0!==a&&("object"!=typeof a||null===a||Array.isArray(a)))throw new TypeError("createStream expects an options object. The positional streamId argument was removed; move any declared-stream key into CreateStreamOptions.declaredStream. See SDK_CONTRACT §8.2.1.");if(!s.hasStream)throw new Error("Streaming was not negotiated for this task. Ensure the agent card is registered with streaming capability.");const r=t.card.streams;if(!r||0===Object.keys(r).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(r);let c;if(a?.declaredStream)c=a.declaredStream;else{if(1!==i.length)throw new Error(`Card declares multiple streams (${i.join(", ")}). Specify declaredStream in CreateStreamOptions.`);c=i[0]}const l=r[c];if(!l)throw new Error(`Stream "${c}" is not declared in the agent card. Available streams: ${i.join(", ")}`);const p=a?.direction??l.direction,I=a?.format??l.format;if(a?.direction&&a.direction!==l.direction)throw new Error(`Direction "${a.direction}" conflicts with card declaration "${l.direction}" for stream "${c}".`);if(a?.format&&a.format!==l.format)throw new Error(`Format "${a.format}" conflicts with card declaration "${l.format}" for stream "${c}".`);const w=a?.external??!1,b=a?.metadata,v=a?.onActivate,T=l.affinity??"dedicated";if("shared"===T&&w)throw new Error(`Shared-affinity external streams are not supported. Declared stream '${c}' has affinity: 'shared' and createStream was called with external: true. Shared affinity requires a single SDK-managed writer with per-task ref-counting; external streams delegate the writer entirely. Use affinity: 'dedicated' with external: true, or affinity: 'shared' without external.`);if(!m){if("shared"===T)throw new Error(`Shared-affinity streams are not supported on request tasks. Declared stream '${c}' has affinity: 'shared'. Request tasks are single-shot; cross-task broadcast is inherently a pipe-task concept. Use affinity: 'dedicated' or remove 'request' from the agent card's taskKinds.`);if("outbound"!==p)throw new Error("Request tasks only support outbound streams");if(w)throw new Error("Request tasks cannot use external streams")}let S;if("shared"===T)S=c;else{const e=(oe.get(s.taskId)??0)+1;oe.set(s.taskId,e),S=`${s.taskId}-${e}`}const $=o.streamChannel(S);if("shared"===T){const e=J.get(S)?.get(s.taskId);if(e)return e}const{entry:E,isNew:K,isNewForTask:x}=W.acquire(S,s.taskId,{direction:p,format:I,external:w,affinity:T});Z.addStream(s.taskId,S);const A=e=>{if("shared"!==T)return;let t=J.get(S);t||(t=new Map,J.set(S,t)),t.set(s.taskId,e)};if(!x){E.setupPromise&&await E.setupPromise;const e=J.get(S)?.get(s.taskId);if(e)return e;if(E.streamClient){const e=h(S,E.streamClient,s.taskId,{releaseStream:me});return A(e),e}throw new Error(`Stream "${S}" same-task reacquire reached an impossible state: setup completed but no client installed. This indicates a logic error in the shared-stream handle cache.`)}if(!K)try{if(E.setupPromise&&await E.setupPromise,"shared"===T&&await(async()=>{await _(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:$,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:"activate",metadata:b,declaredStream:c})})(),E.streamClient){const e=h(S,E.streamClient,s.taskId,{releaseStream:me});return A(e),e}if(w)return y(S,$,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${S}" exists but has no client`)}catch(e){le(S,s.taskId);try{await W.release(S,s.taskId)}catch{}throw e}let N,R;E.setupPromise=new Promise((e,t)=>{N=e,R=t}),E.setupPromise.catch(()=>{});const C=w?"token_request":"embedded";let P;try{P=await _(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:$,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:C,metadata:b,declaredStream:c})}catch(e){R(e),le(S,s.taskId);try{await W.release(S,s.taskId)}catch{}throw e}if(w){N(),E.setupPromise=null;const e=P.token??"";return y(S,$,e,async e=>{await _(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:$,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:"activate",metadata:e?.metadata??b,declaredStream:c})})}const M=P.token;if(!M){const e=new Error("No T7a token received from stream setup handshake");R(e),le(S,s.taskId);try{await W.release(S,s.taskId)}catch{}throw e}const L=new e({subscribeKey:D,publishKey:O,token:M,agentName:u,streamId:S,channel:$,format:I,direction:p,affinity:T,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});E.streamClient=L;const j=h(S,L,s.taskId,{releaseStream:me});if(A(j),N(),E.setupPromise=null,"outbound"===p||"bidirectional"===p){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return v&&(E.activated=!0,E.activatePromise=g(S,j,v,fe)),j}};let S;try{S=t.handler?await t.handler(s,T):void 0}finally{oe.delete(s.taskId),b&&(clearTimeout(b),b=null),v()}const $=L?{baseUrl:L,authProvider:l?new p(l):void 0,agentAuth:M}:void 0;if(S?.artifacts&&S.artifacts.length>0)for(const e of S.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await A(n,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,$)}const E=c.signal.aborted;if(m){const e=ae.has(s.taskId),t=re.has(s.taskId);if(E||e||t){await ue(s.taskId);const a=E&&!e?"canceled":"completed";await K(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),Z.remove(s.taskId),x("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else x("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=re.has(s.taskId);await ue(s.taskId);const t=E?"canceled":"completed";await K(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),Z.remove(s.taskId),x("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},Ie=(e,t)=>{if(x("info",`SwitchEnvironment requested: ${C} -> ${e}`,{pamToken:t?"present":"absent"}),"playground"===e&&"network"===C)return void x("error",`SwitchEnvironment rejected: cannot demote from ${C} to ${e}. Forbidden listing transition.`);if(!t)return void x("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=U,r=q;try{a.removeListener(ye),a.unsubscribe({channels:[de]})}catch{}const n=f[e];if(U=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,presenceTimeout:20}),q=!0,ie=void 0,U.setToken(t),0!==F&&"function"==typeof U.setFilterExpression&&U.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),U.addListener(ye),U.subscribe({channels:[de]}),r)try{a.destroy()}catch{}C=e,D=n.subscribeKey,O=n.publishKey,V.updateKeys(n.subscribeKey,n.publishKey),x("info",`Switched to ${e} environment`),ce().catch(()=>{})},he=(()=>{let e=!1;return t=>{if("PNAccessDeniedCategory"===t&&!e){e=!0,console.error(`[AgentInstance] PAM token expired or revoked — agent ${c} is no longer receiving tasks. Re-register the agent to resume.`);try{U.destroy()}catch{}}}})(),ye={status:e=>{const t=e,a=String(t.category??""),r=String(t.operation??"");"PNConnectedCategory"===a?console.log(`[AgentInstance] PubNub connected to ${de}`):t.error&&console.error(`[AgentInstance] PubNub status: ${a} (op=${r}, code=${t.statusCode})`),he(a)},message:e=>{const r=e.message;if(!r)return;if("SwitchEnvironment"===r.type){const e=r,t=e.environment;return void(t&&t!==C&&t in f&&Ie(t,e.pamToken))}(async(e,r)=>{if("StartTask"===e.type){const n=C,o=((e,t)=>{if("string"==typeof e&&e.length>0)return e;const a=t?.sub;return"string"==typeof a&&a.length>0?a:"anonymous"})(e.ownerId,e.callerClaims),i=e.orgId??o;if(ne.has(e.taskId))return void x("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(r?.broadcast);if(e.protocolVersion&&!E(e.protocolVersion))return d?void x("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await K(U,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void x("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,r=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof r||r<=0)return x("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${r})`,{taskId:e.taskId,instanceId:c}),void await K(U,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=Y+W.activeStreamCount;if(B>0&&l>=B)return d?void x("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${B})`,{taskId:e.taskId,instanceId:c}):(await K(U,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void x("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:Y}));Q.set(e.taskId,o),X.set(e.taskId,i),ne.add(e.taskId),Y++,e.controlToken&&(ie=e.controlToken);const u=new AbortController;te.set(e.taskId,u);let m=null;try{const r=f[n];if(!r.publishKey||!r.subscribeKey)throw new Error("Per-task PubNub client requires valid pub/sub keys. When using an injected PubNub client, ensure it was created with publishKey and subscribeKey.");m=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c}),e.writeToken&&m.setToken(e.writeToken),Z.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:n}),await ce(),t.onStartTask?await t.onStartTask(e,m):await ke(e,m,o,i,u)}catch(r){const s=r?.message??"Agent instance error",n=m??U;try{await ue(e.taskId)}catch{}await K(n,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:s}),t.onError&&await t.onError(e.taskId,r),x("error",s,{taskId:e.taskId}),Z.remove(e.taskId)}finally{const t=se.get(e.taskId);if(t&&(clearTimeout(t),se.delete(e.taskId)),ne.delete(e.taskId),Q.delete(e.taskId),X.delete(e.taskId),ee.delete(e.taskId),te.delete(e.taskId),ae.delete(e.taskId),re.delete(e.taskId),m)try{m.destroy()}catch{}Y--,0===Y&&ie&&U.setToken(ie),await ce()}}else if("CancelTask"===e.type){const t=te.get(e.taskId);t?(t.abort(),x("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):Z.get(e.taskId)&&await pe(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)te.has(e.taskId)?(ae.add(e.taskId),te.get(e.taskId)?.abort(),x("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):Z.get(e.taskId)&&await pe(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)te.has(e.taskId)?(re.add(e.taskId),te.get(e.taskId)?.abort()):Z.get(e.taskId)&&await pe(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(ne.has(e.taskId)){const t=X.get(e.taskId)??"anonymous";await K(U,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(ne.has(e.taskId)){const t=X.get(e.taskId)??"anonymous";await K(U,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&x("info","RetryTask received (no-op)",{taskId:e.taskId})})(r,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{console.error("[AgentInstance] Unhandled error in message handler:",e)})}};U.addListener(ye),i(a,{instanceId:c,description:t.description,scaling:{expectedInstances:F,concurrency:B,maxPendingBacklog:G,maxRunningTimeSec:z},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:j??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??N?.api.baseUrl,agentAuth:M}).then(e=>{console.log(`[AgentInstance] Registered agent: ${a} (instance: ${c})`),e.pamToken&&U.setToken(e.pamToken),0!==F&&"function"==typeof U.setFilterExpression&&U.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),U.subscribe({channels:[de]}),ce().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);console.error(`[AgentInstance] Failed to register agent: ${a} — ${t}`),e instanceof T&&process.exit(1)}),x("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{V.destroy(),U.removeListener(ye),U.unsubscribe({channels:[de]}),q&&U.destroy();for(const e of W.streamIds()){const t=W.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}W.clear(),J.clear(),Z.clear()}catch{}},agentName:a,instanceId:c,get pubnub(){return U},get subscribeKey(){return f[C].subscribeKey},taskClient:V,publishTerminal:pe,failStream:fe,cdmConfig:N}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
|
|
1
|
+
import{StreamClient as e}from"../stream/index.js";import{buildArtifactRef as t,shouldInlineArtifact as a,decodeInlineArtifact as r}from"./artifacts.js";import{createPubNubClient as s}from"./pubnub-client.js";import{createChannelManager as n}from"./channel-manager.js";import{toPayload as o}from"./pubnub-types.js";import{connectAgent as i,getAgent as d}from"./agent-registry.js";import{parseStreamSetupResponse as c,parseStreamSetupError as l}from"./stream-setup-helper.js";import{DEFAULTS as u}from"../defaults.js";import{getEnv as m}from"../env.js";import{uploadFile as f}from"./file-upload.js";import{StaticAuthProvider as p}from"./auth-provider.js";import{StreamRegistry as k}from"./stream-registry.js";import{CredentialCache as I}from"./credential-cache.js";import{createStreamObject as h,createExternalStreamObject as y,runOnActivate as g}from"./stream-context.js";import{TaskClient as w}from"./task-client.js";import{fetchCdmConfig as b}from"./cdm-config.js";import{AgentAuth as v,AgentAuthFatalError as T}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as S,SUPPORTED_PROTOCOL_VERSIONS as $,isProtocolVersionSupported as E}from"./protocol-version.js";const K=async(e,t,a,r,s,i=S)=>{const d=n(r);try{await e.publish({channel:d.taskChannel(t,a),message:o({...s,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:r,taskId:t,protocolVersion:i})})}catch{}};export const _LOG_LEVEL_ORDER={error:0,warn:1,info:2,debug:3};export function _resolveLogLevel(){const e=(m("LOG_LEVEL")??"info").toLowerCase();return _LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info}const x=(e,t,a)=>{const r=_resolveLogLevel();if((_LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info)>r)return;const s={level:e,message:t,ts:Date.now(),...a};"error"===e?console.error("[AgentInstance]",s):"warn"===e?console.warn("[AgentInstance]",s):console.log("[AgentInstance]",s)};export function resolveMaxRunningTimeSec(e,t){return void 0!==e&&void 0!==t&&e!==t&&console.info(`[AgentInstance] opts.maxRunningTimeSec (${e}) overrides card.runtime.maxRunningTimeSec (${t}).`),e??t}export function computeStreamDurationMinutes(e,t,a){return"number"==typeof e?e:t?60:Math.ceil((a??3600)/60)}const A=async(e,r,s,n,o,i,d,c,l)=>{const u={type:"artifact",taskId:r};if(c&&(u.outputId=c),a(o.length))await K(e,r,s,n,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await f(l,{role:"provider-output",taskId:r,fileName:d??`${r}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function C(e,t){const a=`setup.${t.orgId}.${t.taskId}`,r={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:S};t.phase&&(r.phase=t.phase),t.metadata&&(r.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(r),storeInHistory:!1,sendByPost:!0}),new Error("stream_setup publish should have been aborted by Function")}catch(e){const t=l(e);if(t)throw new Error(`Stream setup failed: [${t.code}] ${t.message}`);const a=c(e);if(a)return{token:a.token,tokenTtlMinutes:a.tokenTtlMinutes};throw e}}export const startAgentInstance=async t=>{const a=t.agentName;if(!a)throw new Error("agentName is required: provide opts.agentName");if(!/^[a-zA-Z0-9_]+$/.test(a))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");const o=n(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,_=null,N="playground",R=N;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else _=await b(t.cdmUrl),f={playground:_.playground,network:_.network},console.log("[AgentInstance] CDM config loaded — environment switching enabled (playground + network)");const P=m("BLOCKS_API_KEY");if(!P)throw new Error("BLOCKS_API_KEY is required. Run 'blocks publish' to set up credentials.");let M;const L=t.baseUrl??_?.api.baseUrl;let j,U;if(P&&L&&(M=new v(P,L),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),_){const e=t.baseUrl??_.api.baseUrl,r=await d(a,{baseUrl:e});r?(j=r.listing,N="playground"===j?"playground":"network",console.log(`[AgentInstance] Registry listing: ${j} — using ${N} environment`)):console.warn(`[AgentInstance] Agent "${a}" not found in registry — defaulting to playground environment`),R=N}let q=!1;if(t.pubnub)U=t.pubnub,q=!1,l&&U.setToken(l);else{const e=f[R];U=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20}),q=!0,l&&U.setToken(l)}let D=f[R].subscribeKey,O=f[R].publishKey;const V=new w({subscribeKey:D,authProvider:l?new p(l):void 0,baseUrl:t.baseUrl??_?.api.baseUrl,agentAuth:M,createPubNub:()=>{const e=s({publishKey:O,subscribeKey:D,userId:`${c}-taskclient`});return l&&e.setToken(l),e}}),B=t.concurrency??u.concurrency,F=t.expectedInstances??u.expectedInstances,G=t.maxPendingBacklog,z=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),H=Date.now();let Y=0;const W=new k,Z=new I,J=new Map,Q=new Map,X=new Map,ee=new Map,te=new Map,ae=new Set,re=new Set,se=new Map,ne=new Set,oe=new Map;let ie,de;t.logChannel;const ce=async()=>{if("function"==typeof U.setState&&de)try{await U.setState({channels:[de],state:{instanceId:c,activeTasks:Y,activeStreams:W.activeStreamCount,concurrency:B,startedAt:H,preferredProtocolVersion:S,protocolVersions:[...$]}})}catch{}},le=(e,t)=>{const a=J.get(e);a&&(a.delete(t),0===a.size&&J.delete(e))},ue=async e=>{(e=>{for(const[t,a]of J)a.has(e)&&(a.delete(e),0===a.size&&J.delete(t))})(e);const t=W.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},me=async(e,t)=>{le(e,t);const a=W.get(e);if(0===W.release(e,t)&&a?.streamClient){console.log("[AgentInstance]",{level:"info",message:"last-ref teardown",event:"stream_registry_last_ref_teardown",streamId:e,affinity:a.affinity,releasingTaskId:t,ts:Date.now()});try{await a.streamClient.end()}catch{}}ce().catch(()=>{})},fe=async(e,t)=>{const a=W.forceRemove(e);if((e=>{J.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=Z.get(e);if(a){try{const r=f[a.environment??N]??f[N],n=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c});n.setToken(a.writeToken),await K(n,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),n.destroy()}catch{}Z.remove(e)}}await ce()}},pe=async(e,t)=>{const a=Z.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const r=f[a.environment??N]??f[N],n=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c});n.setToken(a.writeToken);try{await K(n,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{n.destroy()}await ue(e),Z.remove(e),await ce()},ke=async(s,n,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",k=computeStreamDurationMinutes(s.duration,m,z);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{ae.add(s.taskId),c.abort()},e);se.set(s.taskId,t)}}await K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),oe.set(s.taskId,0);let I=0,w=null,b=null;const v=()=>{if(null===w)return;const e=w;w=null,I=Date.now(),K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},T={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{ee.set(s.taskId,e);const t=Date.now();if(t-I>=1e3)w=null,I=t,K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{});else if(w=e,!b){b=setTimeout(()=>{b=null,v()},1e3-(t-I))}},get isCancelled(){return c.signal.aborted},get isExpired(){return ae.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:V,consumerPublicKey:s.consumerPublicKey,downloadInputArtifact:async e=>{const t=e.artifactRef;if(!t)throw new Error(`Request part has no artifactRef (partId: ${e.partId??"none"})`);if("inline"===t.kind)return Buffer.from(r(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!n.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await n.downloadFile({channel:t.channel,id:t.fileId,name:t.fileName}),s=a?.data;if(Buffer.isBuffer(s))return s;if(s instanceof Uint8Array)return Buffer.from(s);if(s&&"function"==typeof s.arrayBuffer){const e=await s.arrayBuffer();return Buffer.from(e)}throw new Error("Unexpected downloadFile response format")},publishArtifact:async(e,t)=>{const a="string"==typeof e?Buffer.from(e,"utf-8"):e,r=t?.mimeType??"application/octet-stream",o=L?{baseUrl:L,authProvider:l?new p(l):void 0,agentAuth:M}:void 0;await A(n,s.taskId,d,u,a,r,t?.fileName,t?.outputId,o)},createStream:async a=>{if(void 0!==a&&("object"!=typeof a||null===a||Array.isArray(a)))throw new TypeError("createStream expects an options object. The positional streamId argument was removed; move any declared-stream key into CreateStreamOptions.declaredStream. See SDK_CONTRACT §8.2.1.");if(!s.hasStream)throw new Error("Streaming was not negotiated for this task. Ensure the agent card is registered with streaming capability.");const r=t.card.streams;if(!r||0===Object.keys(r).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(r);let c;if(a?.declaredStream)c=a.declaredStream;else{if(1!==i.length)throw new Error(`Card declares multiple streams (${i.join(", ")}). Specify declaredStream in CreateStreamOptions.`);c=i[0]}const l=r[c];if(!l)throw new Error(`Stream "${c}" is not declared in the agent card. Available streams: ${i.join(", ")}`);const p=a?.direction??l.direction,I=a?.format??l.format;if(a?.direction&&a.direction!==l.direction)throw new Error(`Direction "${a.direction}" conflicts with card declaration "${l.direction}" for stream "${c}".`);if(a?.format&&a.format!==l.format)throw new Error(`Format "${a.format}" conflicts with card declaration "${l.format}" for stream "${c}".`);const w=a?.external??!1,b=a?.metadata,v=a?.onActivate,T=l.affinity??"dedicated";if("shared"===T&&w)throw new Error(`Shared-affinity external streams are not supported. Declared stream '${c}' has affinity: 'shared' and createStream was called with external: true. Shared affinity requires a single SDK-managed writer with per-task ref-counting; external streams delegate the writer entirely. Use affinity: 'dedicated' with external: true, or affinity: 'shared' without external.`);if(!m){if("shared"===T)throw new Error(`Shared-affinity streams are not supported on request tasks. Declared stream '${c}' has affinity: 'shared'. Request tasks are single-shot; cross-task broadcast is inherently a pipe-task concept. Use affinity: 'dedicated' or remove 'request' from the agent card's taskKinds.`);if("outbound"!==p)throw new Error("Request tasks only support outbound streams");if(w)throw new Error("Request tasks cannot use external streams")}let S;if("shared"===T)S=c;else{const e=(oe.get(s.taskId)??0)+1;oe.set(s.taskId,e),S=`${s.taskId}-${e}`}const $=o.streamChannel(S);if("shared"===T){const e=J.get(S)?.get(s.taskId);if(e)return e}const{entry:E,isNew:K,isNewForTask:x}=W.acquire(S,s.taskId,{direction:p,format:I,external:w,affinity:T});Z.addStream(s.taskId,S);const A=e=>{if("shared"!==T)return;let t=J.get(S);t||(t=new Map,J.set(S,t)),t.set(s.taskId,e)};if(!x){E.setupPromise&&await E.setupPromise;const e=J.get(S)?.get(s.taskId);if(e)return e;if(E.streamClient){const e=h(S,E.streamClient,s.taskId,{releaseStream:me});return A(e),e}throw new Error(`Stream "${S}" same-task reacquire reached an impossible state: setup completed but no client installed. This indicates a logic error in the shared-stream handle cache.`)}if(!K)try{if(E.setupPromise&&await E.setupPromise,"shared"===T&&await(async()=>{await C(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:$,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:"activate",metadata:b,declaredStream:c})})(),E.streamClient){const e=h(S,E.streamClient,s.taskId,{releaseStream:me});return A(e),e}if(w)return y(S,$,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${S}" exists but has no client`)}catch(e){le(S,s.taskId);try{await W.release(S,s.taskId)}catch{}throw e}let _,N;E.setupPromise=new Promise((e,t)=>{_=e,N=t}),E.setupPromise.catch(()=>{});const R=w?"token_request":"embedded";let P;try{P=await C(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:$,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:R,metadata:b,declaredStream:c})}catch(e){N(e),le(S,s.taskId);try{await W.release(S,s.taskId)}catch{}throw e}if(w){_(),E.setupPromise=null;const e=P.token??"";return y(S,$,e,async e=>{await C(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:$,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:"activate",metadata:e?.metadata??b,declaredStream:c})})}const M=P.token;if(!M){const e=new Error("No T7a token received from stream setup handshake");N(e),le(S,s.taskId);try{await W.release(S,s.taskId)}catch{}throw e}const L=new e({subscribeKey:D,publishKey:O,token:M,agentName:u,streamId:S,channel:$,format:I,direction:p,affinity:T,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});E.streamClient=L;const j=h(S,L,s.taskId,{releaseStream:me});if(A(j),_(),E.setupPromise=null,"outbound"===p||"bidirectional"===p){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return v&&(E.activated=!0,E.activatePromise=g(S,j,v,fe)),j}};let S;try{S=t.handler?await t.handler(s,T):void 0}finally{oe.delete(s.taskId),b&&(clearTimeout(b),b=null),v()}const $=L?{baseUrl:L,authProvider:l?new p(l):void 0,agentAuth:M}:void 0;if(S?.artifacts&&S.artifacts.length>0)for(const e of S.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await A(n,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,$)}const E=c.signal.aborted;if(m){const e=ae.has(s.taskId),t=re.has(s.taskId);if(E||e||t){await ue(s.taskId);const a=E&&!e?"canceled":"completed";await K(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),Z.remove(s.taskId),x("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else x("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=re.has(s.taskId);await ue(s.taskId);const t=E?"canceled":"completed";await K(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),Z.remove(s.taskId),x("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},Ie=(e,t)=>{if(x("info",`SwitchEnvironment requested: ${R} -> ${e}`,{pamToken:t?"present":"absent"}),"playground"===e&&"network"===R)return void x("error",`SwitchEnvironment rejected: cannot demote from ${R} to ${e}. Forbidden listing transition.`);if(!t)return void x("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=U,r=q;try{a.removeListener(ye),de&&a.unsubscribe({channels:[de]})}catch{}const n=f[e];if(U=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,presenceTimeout:20}),q=!0,ie=void 0,U.setToken(t),0!==F&&"function"==typeof U.setFilterExpression&&U.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),U.addListener(ye),de&&U.subscribe({channels:[de]}),r)try{a.destroy()}catch{}R=e,D=n.subscribeKey,O=n.publishKey,V.updateKeys(n.subscribeKey,n.publishKey),x("info",`Switched to ${e} environment`),ce().catch(()=>{})},he=(()=>{let e=!1;return t=>{if("PNAccessDeniedCategory"===t&&!e){e=!0,console.error(`[AgentInstance] PAM token expired or revoked — agent ${c} is no longer receiving tasks. Re-register the agent to resume.`);try{U.destroy()}catch{}}}})(),ye={status:e=>{const t=e,a=String(t.category??""),r=String(t.operation??"");"PNConnectedCategory"===a?console.log(`[AgentInstance] PubNub connected to ${de}`):t.error&&console.error(`[AgentInstance] PubNub status: ${a} (op=${r}, code=${t.statusCode})`),he(a)},message:e=>{const r=e.message;if(!r)return;if("SwitchEnvironment"===r.type){const e=r,t=e.environment;return void(t&&t!==R&&t in f&&Ie(t,e.pamToken))}(async(e,r)=>{if("StartTask"===e.type){const n=R,o=((e,t)=>{if("string"==typeof e&&e.length>0)return e;const a=t?.sub;return"string"==typeof a&&a.length>0?a:"anonymous"})(e.ownerId,e.callerClaims),i=e.orgId??o;if(ne.has(e.taskId))return void x("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(r?.broadcast);if(e.protocolVersion&&!E(e.protocolVersion))return d?void x("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await K(U,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void x("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,r=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof r||r<=0)return x("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${r})`,{taskId:e.taskId,instanceId:c}),void await K(U,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=Y+W.activeStreamCount;if(B>0&&l>=B)return d?void x("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${B})`,{taskId:e.taskId,instanceId:c}):(await K(U,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void x("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:Y}));Q.set(e.taskId,o),X.set(e.taskId,i),ne.add(e.taskId),Y++,e.controlToken&&(ie=e.controlToken);const u=new AbortController;te.set(e.taskId,u);let m=null;try{const r=f[n];if(!r.publishKey||!r.subscribeKey)throw new Error("Per-task PubNub client requires valid pub/sub keys. When using an injected PubNub client, ensure it was created with publishKey and subscribeKey.");m=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c}),e.writeToken&&m.setToken(e.writeToken),Z.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:n}),await ce(),t.onStartTask?await t.onStartTask(e,m):await ke(e,m,o,i,u)}catch(r){const s=r?.message??"Agent instance error",n=m??U;try{await ue(e.taskId)}catch{}await K(n,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:s}),t.onError&&await t.onError(e.taskId,r),x("error",s,{taskId:e.taskId}),Z.remove(e.taskId)}finally{const t=se.get(e.taskId);if(t&&(clearTimeout(t),se.delete(e.taskId)),ne.delete(e.taskId),Q.delete(e.taskId),X.delete(e.taskId),ee.delete(e.taskId),te.delete(e.taskId),ae.delete(e.taskId),re.delete(e.taskId),m)try{m.destroy()}catch{}Y--,0===Y&&ie&&U.setToken(ie),await ce()}}else if("CancelTask"===e.type){const t=te.get(e.taskId);t?(t.abort(),x("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):Z.get(e.taskId)&&await pe(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)te.has(e.taskId)?(ae.add(e.taskId),te.get(e.taskId)?.abort(),x("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):Z.get(e.taskId)&&await pe(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)te.has(e.taskId)?(re.add(e.taskId),te.get(e.taskId)?.abort()):Z.get(e.taskId)&&await pe(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(ne.has(e.taskId)){const t=X.get(e.taskId)??"anonymous";await K(U,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(ne.has(e.taskId)){const t=X.get(e.taskId)??"anonymous";await K(U,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&x("info","RetryTask received (no-op)",{taskId:e.taskId})})(r,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{console.error("[AgentInstance] Unhandled error in message handler:",e)})}};U.addListener(ye),i(a,{instanceId:c,description:t.description,scaling:{expectedInstances:F,concurrency:B,maxPendingBacklog:G,maxRunningTimeSec:z},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:j??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??_?.api.baseUrl,agentAuth:M}).then(e=>{if(console.log(`[AgentInstance] Registered agent: ${a} (instance: ${c})`),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");de=e.controlChannel,e.pamToken&&U.setToken(e.pamToken),0!==F&&"function"==typeof U.setFilterExpression&&U.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),U.subscribe({channels:[de]}),ce().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);console.error(`[AgentInstance] Failed to register agent: ${a} — ${t}`),e instanceof T&&process.exit(1)}),x("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{V.destroy(),U.removeListener(ye),de&&U.unsubscribe({channels:[de]}),q&&U.destroy();for(const e of W.streamIds()){const t=W.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}W.clear(),J.clear(),Z.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return de},get pubnub(){return U},get subscribeKey(){return f[R].subscribeKey},taskClient:V,publishTerminal:pe,failStream:fe,cdmConfig:_}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
|
|
@@ -229,6 +229,8 @@ declare function detectDeviceOs(): string;
|
|
|
229
229
|
*/
|
|
230
230
|
export declare function connectAgent(agentName: string, options: ConnectAgentOptions): Promise<{
|
|
231
231
|
pamToken?: string;
|
|
232
|
+
agentId?: string;
|
|
233
|
+
controlChannel?: string;
|
|
232
234
|
}>;
|
|
233
235
|
/**
|
|
234
236
|
* Get a single agent by name.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:os";import{registryAllChannel as n,registrySkillChannel as e,registryVisibilityChannel as a,registryLogChannel as r,normalizeSkillSlug as
|
|
1
|
+
import t from"node:os";import{registryAllChannel as n,registrySkillChannel as e,registryVisibilityChannel as a,registryLogChannel as r,normalizeSkillSlug as o}from"./channel-manager.js";import{getEnv as s}from"../env.js";import{CURRENT_PROTOCOL_VERSION as i,SUPPORTED_PROTOCOL_VERSIONS as c,SDK_VERSION as u,PROTOCOL_VERSION_HEADER as l}from"./protocol-version.js";import{captureAffinity as g,injectAffinity as d}from"./write-affinity.js";function m(){try{return t.platform()}catch{return"unknown"}}async function f(t,n=3,e=500){let a;for(let r=0;r<n;r++)try{return await t()}catch(t){a=t;const o=String(t);if(!(o.includes("ENOTFOUND")||o.includes("ETIMEDOUT")||o.includes("ECONNRESET")||o.includes("NetworkIssues"))||r===n-1)throw t;const s=e*Math.pow(2,r)+100*Math.random();await new Promise(t=>setTimeout(t,s))}throw a}function h(t,n){let e=`${function(t){if(t)return t.replace(/\/+$/,"");throw new Error("[AgentRegistry] baseUrl is required: provide it via options or CDM config")}(n)}/api/v1/registry/agents`;if(t){const n=new URLSearchParams;for(const[e,a]of Object.entries(t))void 0!==a&&""!==a&&n.set(e,a);const a=n.toString();a&&(e+="?"+a)}return e}async function p(t,n){const e=new Headers(n?.headers);e.has(l)||e.set(l,i);const a={};d(a);for(const[t,n]of Object.entries(a))e.set(t,n);const r=await fetch(t,{...n,headers:e});if(g(r.headers),404===r.status)return null;if(!r.ok){const t=(await r.json().catch(()=>({}))).message??`HTTP ${r.status}`;throw new Error(`[AgentRegistry] Request failed: ${t}`)}return{data:await r.json(),status:r.status}}function y(t){return{agentName:t.agentName,displayName:t.name??t.agentName,description:t.description,skills:t.skills,scaling:t.scaling,card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:t.listing??"playground",createdAt:t.registeredAt}}export async function connectAgent(t,n){if(!/^[a-zA-Z0-9_]+$/.test(t))throw new Error("agentName must contain only alphanumeric characters and underscores (no hyphens)");h(void 0,n.baseUrl);const e=s("BLOCKS_CLI_VERSION"),a={agentName:t,instanceId:n.instanceId,listing:n.listing,expectedInstances:n.scaling?.expectedInstances,concurrency:n.scaling?.concurrency,maxPendingBacklog:n.scaling?.maxPendingBacklog,maxRunningTimeSec:n.scaling?.maxRunningTimeSec,deviceOs:m(),sdkLanguage:"Node",sdkVersion:u,protocolVersions:[...c],preferredProtocolVersion:i};let r;if(e&&(a.cliVersion=e),!n.agentAuth)throw new Error("[AgentRegistry] agentAuth is required. Set BLOCKS_API_KEY and pass agentAuth to connectAgent.");const o=await f(async()=>n.agentAuth.init(a));return r=o.pamToken??void 0,{pamToken:r,agentId:o.agentId,controlChannel:o.controlChannel}}export async function getAgent(t,n){const e=h({agentName:t},n?.baseUrl),a=await f(async()=>p(e));return a?y(a.data.agent):null}export async function fetchAgentRegistry(t){const n={include:"full"};t?.limit&&(n.limit=String(t.limit)),t?.cursor&&(n.cursor=t.cursor);const e=h(n,t?.baseUrl),a=await f(async()=>p(e));return a?{agents:a.data.agents.map(y),next:a.data.next??void 0,totalCount:a.data.totalCount}:{agents:[],totalCount:0}}export async function fetchAgentsBySkill(t,n){const e={include:"full",skill:t};n?.limit&&(e.limit=String(n.limit)),n?.cursor&&(e.cursor=n.cursor);const a=h(e,n?.baseUrl),r=await f(async()=>p(a));return r?{agents:r.data.agents.map(y),next:r.data.next??void 0,totalCount:r.data.totalCount}:{agents:[],totalCount:0}}export async function fetchAgentsByListing(t,n){const e={include:"full",listing:t};n?.limit&&(e.limit=String(n.limit)),n?.cursor&&(e.cursor=n.cursor);const a=h(e,n?.baseUrl),r=await f(async()=>p(a));return r?{agents:r.data.agents.map(y),next:r.data.next??void 0,totalCount:r.data.totalCount}:{agents:[],totalCount:0}}export async function removeAgent(t,n){const e=h({agentName:t},n?.baseUrl);if(n?.agentAuth){return 404!==(await f(()=>n.agentAuth.authenticatedFetch(e,{method:"DELETE"}))).status}const a=s("BLOCKS_API_KEY");return null!==await f(async()=>p(e,{method:"DELETE",headers:{...a?{Authorization:`Bearer ${a}`}:{}}}))}export{m as detectDeviceOs,n as registryAllChannel,e as registrySkillChannel,a as registryVisibilityChannel,r as registryLogChannel,o as normalizeSkillSlug};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getEnv as
|
|
1
|
+
import{getEnv as o}from"../env.js";export const DEFAULT_CDM_URL="https://config.blocks.ai/config.json";export async function fetchCdmConfig(n){const r=n??o("BLOCKS_CDM_URL")??DEFAULT_CDM_URL,t=await fetch(r);if(!t.ok)throw new Error(`CDM config fetch failed: ${t.status} ${t.statusText}`);const s=await t.json();if(!s.playground?.publishKey||!s.playground?.subscribeKey)throw new Error("CDM config missing playground keys");if(!s.network?.publishKey||!s.network?.subscribeKey)throw new Error("CDM config missing network keys");return s}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Channel topology (org-scoped with 3-level constraint):
|
|
5
5
|
* - u.{orgId}.{taskId} - Org-scoped task events (progress, artifacts, terminal, logs)
|
|
6
|
-
* - agent.{
|
|
6
|
+
* - agent.{agentId}.control - Control plane per agent ID
|
|
7
7
|
* - obs.{agentName}.log - Agent-level observability
|
|
8
8
|
*
|
|
9
9
|
* Registry channels (App Context membership indexes):
|
|
@@ -72,9 +72,9 @@ export declare class ChannelManager {
|
|
|
72
72
|
taskChannel(taskId: string, orgId: string): string;
|
|
73
73
|
/**
|
|
74
74
|
* Control channel for agent.
|
|
75
|
-
* Format: agent.{
|
|
75
|
+
* Format: agent.{agentId}.control (3 levels)
|
|
76
76
|
*/
|
|
77
|
-
controlChannel(
|
|
77
|
+
controlChannel(agentId: string): string;
|
|
78
78
|
/**
|
|
79
79
|
* Agent-level observability channel.
|
|
80
80
|
* Format: obs.{agentName}.log (3 levels)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const registryAllChannel=()=>"registry.all";export const registrySkillChannel=r=>`registry.skill.${normalizeSkillSlug(r)}`;export const registryVisibilityChannel=r=>r?"registry.public":"registry.private";export const registryLogChannel=()=>"registry.log";export const normalizeSkillSlug=r=>r.toLowerCase().replace(/[^a-z0-9._]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"");const r=["agent","obs","sys","u","anonymous","system","stream"];export function validateOwnerId(e){return!(!e||"string"!=typeof e)&&!r.includes(e.toLowerCase())}export class ChannelManager{constructor(r={}){if(!r.agentName)throw new Error("agentName is required for ChannelManager");this.agentName=r.agentName}getAgentName(){return this.agentName}taskChannel(r,e){if(!e)throw new Error("orgId required for task channel");if(!r)throw new Error("taskId required for task channel");return`u.${e}.${r}`}controlChannel(r){return`agent.${r
|
|
1
|
+
export const registryAllChannel=()=>"registry.all";export const registrySkillChannel=r=>`registry.skill.${normalizeSkillSlug(r)}`;export const registryVisibilityChannel=r=>r?"registry.public":"registry.private";export const registryLogChannel=()=>"registry.log";export const normalizeSkillSlug=r=>r.toLowerCase().replace(/[^a-z0-9._]/g,"_").replace(/_+/g,"_").replace(/^_|_$/g,"");const r=["agent","obs","sys","u","anonymous","system","stream"];export function validateOwnerId(e){return!(!e||"string"!=typeof e)&&!r.includes(e.toLowerCase())}export class ChannelManager{constructor(r={}){if(!r.agentName)throw new Error("agentName is required for ChannelManager");this.agentName=r.agentName}getAgentName(){return this.agentName}taskChannel(r,e){if(!e)throw new Error("orgId required for task channel");if(!r)throw new Error("taskId required for task channel");return`u.${e}.${r}`}controlChannel(r){return`agent.${r}.control`}obsChannel(r){return`obs.${r??this.agentName}.log`}userTaskPattern(r){if(!r)throw new Error("orgId required for user task pattern");return`u.${r}.*`}agentWildcard(r){return`agent.${r??this.agentName}.*`}streamChannel(r,e){if(!r)throw new Error("streamId required for stream channel");return`stream.${e??this.agentName}.${r}`}streamWildcard(r){return`stream.${r??this.agentName}.*`}}export const createChannelManager=r=>new ChannelManager({agentName:r});export const taskChannel=(r,e)=>{if(!e)throw new Error("orgId required for task channel");if(!r)throw new Error("taskId required for task channel");return`u.${e}.${r}`};export const userTaskPattern=r=>{if(!r)throw new Error("orgId required for user task pattern");return`u.${r}.*`};export const streamChannel=(r,e)=>{if(!r)throw new Error("agentName required for stream channel");if(!e)throw new Error("streamId required for stream channel");return`stream.${r}.${e}`};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"pubnub";import{callRpc as t}from"./rpc-client.js";import{taskChannel as s}from"./channel-manager.js";import{TaskSession as n}from"./task-session.js";import{ConsumerAuth as i}from"./consumer-auth.js";import{shouldInlineArtifact as r,buildArtifactRef as o}from"./artifacts.js";import{uploadFile as a}from"./file-upload.js";import{normalizeFileInput as c}from"./file-input.js";import{fetchCdmConfig as u}from"./cdm-config.js";import{captureAffinity as b,injectAffinity as d}from"./write-affinity.js";import{getEnv as l}from"../env.js";import{StreamRef as h}from"./stream-ref.js";import{invertDirection as f}from"../stream/index.js";import{asPubNubFetcher as p}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as m,PROTOCOL_VERSION_HEADER as y}from"./protocol-version.js";import{getAgent as k}from"./agent-registry.js";const g=new Set(["completed","failed","canceled"]);export class TaskClient{constructor(e){this._ownsPubNub=!1,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(t){const s=t??{},n=s.listing;if(!n)throw new Error("TaskClient.create() requires a listing option ('playground', 'private', or 'public') to select the CDM keyset.");const r=!!(s.apiKey||s.tokenEndpoint||s.tokenProvider);if([s.apiKey,s.tokenEndpoint,s.tokenProvider].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");const o=s.cdmUrl??l("BLOCKS_CDM_URL"),a=await u(o),c="playground"===n?a.playground:a.network,b=s.subscribeKey??l("BLOCKS_SUBSCRIBE_KEY")??c.subscribeKey,d=s.publishKey??l("BLOCKS_PUBLISH_KEY")??c.publishKey,h=s.baseUrl??l("BLOCKS_BACKEND_URL")??a.api.baseUrl;if(!b)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!h)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const f=()=>{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:b,publishKey:d||void 0,userId:t,enableEventEngine:!0})};if(r){const e=new i({apiKey:s.apiKey,tokenEndpoint:s.tokenEndpoint,tokenProvider:s.tokenProvider,baseUrl:h,onAuthError:s.onAuthError});await e.init();const t=new TaskClient({subscribeKey:b,publishKey:d,baseUrl:h,createSessionPubNub:f,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({subscribeKey:b,publishKey:d,baseUrl:h,createSessionPubNub:f})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await k(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(t){let s;if(this._createSessionPubNub)s=this._createSessionPubNub();else{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;s=new e({subscribeKey:this._subscribeKey,publishKey:this._publishKey||void 0,userId:t,enableEventEngine:!0})}return t&&s.setToken(t),s}async fetchConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[y]:m},n=this.config.authProvider?.getAuthHeader();return n&&(s.Authorization=n),d(s),fetch(t,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:"consumer"})})};let n=await s();if(b(n.headers),401===n.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(n=await s(),b(n.headers))}if(!n.ok){const e=await n.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${n.status}${e?` ${e}`:""}`)}return n.json()}async sendMessage(e){const s=e.ownerId||this.defaultOwnerId||"",i=e.taskKind,u=e.duration;if("pipe"===i){if(null==u||!Number.isInteger(u)||u<1||u>43200)throw new Error("Pipe tasks require a duration between 1 and 43200 minutes")}else if(null!=u)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let b;const d=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const s=c(t.file),n=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(r(s.size)){const e=await s.getBytes(),r=o({data:e,mimeType:i,fileName:n}),{file:a,fileName:c,text:u,...b}=t;d.push({...b,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:n,fileSize:s.size,mimeType:i,partId:t.partId,uploadSessionId:b},c=await a(r,o,s.uploadBody);c.uploadSessionId&&(b=c.uploadSessionId);const{file:u,fileName:l,...h}=t;d.push({partId:h.partId})}}else{const{file:e,fileName:s,...n}=t;d.push(n)}const l={agentName:e.agentName,requestParts:d};b&&(l.uploadSessionId=b),e.idempotencyKey&&(l.idempotencyKey=e.idempotencyKey),l.ownerId=s;const h={};i&&(h.taskKind=i),null!=u&&(h.duration=u),e.consumerPublicKey&&(h.consumerPublicKey=e.consumerPublicKey),Object.keys(h).length>0&&(l.extensions={blocks:h}),e.pushNotificationConfig&&(l.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(l.retryPolicy=e.retryPolicy);const f=await t(this.config,"SendMessage",l),p=f.extensions?.blocks,m=p?.readToken??null,y=p?.streamChannels?.status??void 0;if(!0===f.idempotent&&!!f.state&&g.has(f.state))return new n({taskId:f.taskId,ownerId:s,orgId:f.orgId??s,readToken:m,statusChannel:y,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:this._subscribeKey,publishKey:this._publishKey},rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,preClosed:!0,state:f.state});const k=this.createPerSessionPubNub(m);return new n({taskId:f.taskId,ownerId:s,orgId:f.orgId??s,readToken:m,statusChannel:y,agentName:e.agentName,pubnub:k,ownsSubscribeClient:!0,sdkOptions:{subscribeKey:this._subscribeKey,publishKey:this._publishKey},rpcConfig:this.config,idempotent:f.idempotent,queued:f.queued,pushConfigId:f.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async connect(e){const{taskId:t}=e;if(!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const s=await this.getTask(t);if(!s)throw new Error(`Task not found: ${t}`);const i=s.agentName??"",r=s.state??"",o=await this.fetchConsumerReadToken(t),{pamToken:a,channel:c}=o,u={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(a);try{const o=await b.time(),d=String(o.timetoken),l=p(b);let m,y=new Map,k=[],w="0";if(l?.fetchMessages){const e=await async function(e,t){const s=[];let n;for(;;){const i={channels:[t],count:100};n&&(i.start=n);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(s.push(...o),o.length<100)break;n=o[0].timetoken}return s.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),s}(l,c),s=function(e,t,s,n){const i=new Map,r=[];let o,a="0",c="0";for(const u of e){u.timetoken&&u.timetoken>a&&(a=u.timetoken);const e=u.message;if(e&&"object"==typeof e&&e.type){if("terminal"===e.type){const t=String(u.timetoken??"0");if(t>=c){const s=e.state;"completed"!==s&&"failed"!==s&&"canceled"!==s||(o=s,c=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=f(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const b=a.affinity;if("dedicated"!==b&&"shared"!==b){console.warn(`[TaskClient] history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const d={taskId:t,streamId:e,agentName:s,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:b,metadata:a.metadata,declaredStream:r};i.set(e,new h(d,n))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,highWaterMark:a,terminalState:o}}(e,t,i,u);y=s.streams,k=s.artifacts,w=s.highWaterMark,m=s.terminalState}const K=m&&g.has(m)?m:r;if(g.has(K))return new n({taskId:t,ownerId:s.owner??"",readToken:a,statusChannel:c,agentName:i,pubnub:b,ownsSubscribeClient:!0,sdkOptions:u,rpcConfig:this.config,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:K,skipSubscription:!0,preloadedStreams:y,preloadedArtifacts:k});const P="0"!==w?w:d,T=[];let S,I=!1;const C={message:e=>{if(e.channel!==c)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const s=String(e.timetoken??"0");I&&S?S(t,s):T.push({message:t,timetoken:s})}};b.addListener(C),b.subscribe({channels:[c],timetoken:P});const N=new n({taskId:t,ownerId:s.owner??"",readToken:a,statusChannel:c,agentName:i,pubnub:b,ownsSubscribeClient:!0,sdkOptions:u,rpcConfig:this.config,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:r,preloadedStreams:y,preloadedArtifacts:k,externalSubscription:{listener:C,channel:c,onReady:e=>{S=e}}});if(S){const e=S;for(const t of T)t.timetoken>P&&e(t.message,t.timetoken)}return T.length=0,I=!0,N}catch(e){throw b.destroy(),e}}async getTask(e){return(await t(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return t(this.config,"ListTasks",{...e})}async cancelTask(e){await t(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await t(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await t(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await t(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await t(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,n){return function(e,t,n,i){const r=s(t,n),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){w(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){w(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){w(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){w(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){w(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,n)}}function w(e,t,s,n){const i=e instanceof Error?e:new Error(String(e));if(n)try{n(i,{entryPoint:"subscribeToTask",callbackType:t,event:s})}catch{}else console.warn(`[subscribeToTask] callback error in ${t}:`,i.message)}
|
|
1
|
+
import e from"pubnub";import{callRpc as t}from"./rpc-client.js";import{taskChannel as n}from"./channel-manager.js";import{TaskSession as s}from"./task-session.js";import{ConsumerAuth as i}from"./consumer-auth.js";import{shouldInlineArtifact as r,buildArtifactRef as o}from"./artifacts.js";import{uploadFile as a}from"./file-upload.js";import{normalizeFileInput as c}from"./file-input.js";import{fetchCdmConfig as u}from"./cdm-config.js";import{captureAffinity as d,injectAffinity as b}from"./write-affinity.js";import{getEnv as l}from"../env.js";import{StreamRef as f}from"./stream-ref.js";import{invertDirection as h}from"../stream/index.js";import{asPubNubFetcher as p}from"./pubnub-types.js";import{CURRENT_PROTOCOL_VERSION as m,PROTOCOL_VERSION_HEADER as g}from"./protocol-version.js";import{getAgent as k}from"./agent-registry.js";const y=new Set(["completed","failed","canceled"]);function w(e,t,n,s){const i=new Map,r=[];let o,a="0",c="0";for(const u of e){u.timetoken&&u.timetoken>a&&(a=u.timetoken);const e=u.message;if(e&&"object"==typeof e&&e.type){if("terminal"===e.type){const t=String(u.timetoken??"0");if(t>=c){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(o=n,c=t)}}if("progress"===e.type&&"stream_started"===e.streamEvent&&e.streams){const r=e.declaredStream,o=e.streams;for(const[e,a]of Object.entries(o)){if(!a||"object"!=typeof a||i.has(e))continue;const o=a.direction,c=h(o),u=a.format;if(!u||"bytes"!==u&&"events"!==u)continue;const d=a.affinity;if("dedicated"!==d&&"shared"!==d){console.warn(`[TaskClient] history-preload: dropping stream "${e}" for task "${t}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const b={taskId:t,streamId:e,agentName:n,channel:a.channel,token:a.token,agentDirection:o,localDirection:c,format:u,affinity:d,metadata:a.metadata,declaredStream:r};i.set(e,new f(b,s))}}if("artifact"===e.type&&e.artifactRef){const t=e.artifactRef;t&&"object"==typeof t&&t.kind&&r.push(t)}}}return{streams:i,artifacts:r,highWaterMark:a,terminalState:o}}async function I(e,t){const n=[];let s;for(;;){const i={channels:[t],count:100};s&&(i.start=s);const r=await e.fetchMessages(i),o=r.channels?.[t]??[];if(0===o.length)break;if(n.push(...o),o.length<100)break;s=o[0].timetoken}return n.sort((e,t)=>e.timetoken<t.timetoken?-1:e.timetoken>t.timetoken?1:0),n}export class TaskClient{constructor(e){this._ownsPubNub=!1,this.config={subscribeKey:e.subscribeKey,authProvider:e.authProvider,baseUrl:e.baseUrl,agentAuth:e.agentAuth},this._pubnub=e.pubnub,this._createPubNub=e.createPubNub,this._createSessionPubNub=e.createSessionPubNub,this.defaultOwnerId=e.defaultOwnerId,this._subscribeKey=e.subscribeKey,this._publishKey=e.publishKey??"",this._ownsPubNub=!e.pubnub&&!!e.createPubNub}static async create(t){const n=t??{},s=n.listing;if(!s)throw new Error("TaskClient.create() requires a listing option ('playground', 'private', or 'public') to select the CDM keyset.");const r=!!(n.apiKey||n.tokenEndpoint||n.tokenProvider);if([n.apiKey,n.tokenEndpoint,n.tokenProvider].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");const o=n.cdmUrl??l("BLOCKS_CDM_URL"),a=await u(o),c="playground"===s?a.playground:a.network,d=n.subscribeKey??l("BLOCKS_SUBSCRIBE_KEY")??c.subscribeKey,b=n.publishKey??l("BLOCKS_PUBLISH_KEY")??c.publishKey,f=n.baseUrl??l("BLOCKS_BACKEND_URL")??a.api.baseUrl;if(!d)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!f)throw new Error("TaskClient.create() could not resolve baseUrl. Set baseUrl option, BLOCKS_BACKEND_URL env var, or ensure CDM config has api.baseUrl.");const h=()=>{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:d,publishKey:b||void 0,userId:t,enableEventEngine:!0})};if(r){const e=new i({apiKey:n.apiKey,tokenEndpoint:n.tokenEndpoint,tokenProvider:n.tokenProvider,baseUrl:f,onAuthError:n.onAuthError});await e.init();const t=new TaskClient({subscribeKey:d,publishKey:b,baseUrl:f,createSessionPubNub:h,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({subscribeKey:d,publishKey:b,baseUrl:f,createSessionPubNub:h})}getUserId(){return this._consumerAuth?.getUserId()??null}async getAgentCard(e){const t=await k(e,{baseUrl:this.config.baseUrl});return t?.card??null}updateKeys(e,t){this.config.subscribeKey=e,this._subscribeKey=e,this._publishKey=t??""}getPubNub(){if(!this._pubnub&&this._createPubNub&&(this._pubnub=this._createPubNub()),!this._pubnub)throw new Error("TaskClient requires a pubnub instance for subscribe. Pass pubnub or createPubNub in TaskClientOptions.");return this._pubnub}destroy(){this._pubnub&&this._ownsPubNub&&(this._pubnub.destroy(),this._pubnub=void 0),this._consumerAuth&&this._consumerAuth.destroy()}[Symbol.dispose](){this.destroy()}createPerSessionPubNub(t){let n;if(this._createSessionPubNub)n=this._createSessionPubNub();else{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;n=new e({subscribeKey:this._subscribeKey,publishKey:this._publishKey||void 0,userId:t,enableEventEngine:!0})}return t&&n.setToken(t),n}async fetchConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,n=async()=>{const n={"Content-Type":"application/json",[g]:m},s=this.config.authProvider?.getAuthHeader();return s&&(n.Authorization=s),b(n),fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,role:"consumer"})})};let s=await n();if(d(s.headers),401===s.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(s=await n(),d(s.headers))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){const i=e.ownerId||this.defaultOwnerId||"",u=e.taskKind,d=e.duration;if("pipe"===u){if(null==d||!Number.isInteger(d)||d<1||d>43200)throw new Error("Pipe tasks require a duration between 1 and 43200 minutes")}else if(null!=d)throw new Error("Request tasks must not include a duration. Duration is only valid for pipe tasks.");let b;const l=[];for(const t of e.requestParts)if(t.file){if(!t.partId)throw new Error("partId is required for file-bearing request parts");const n=c(t.file),s=t.fileName??"unnamed",i=t.contentType??"application/octet-stream";if(r(n.size)){const e=await n.getBytes(),r=o({data:e,mimeType:i,fileName:s}),{file:a,fileName:c,text:u,...d}=t;l.push({...d,artifactRef:r})}else{if(!this.config.baseUrl)throw new Error("File upload requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const r={baseUrl:this.config.baseUrl,authProvider:this.config.authProvider,agentAuth:this.config.agentAuth},o={role:"consumer-input",agentName:e.agentName,fileName:s,fileSize:n.size,mimeType:i,partId:t.partId,uploadSessionId:b},c=await a(r,o,n.uploadBody);c.uploadSessionId&&(b=c.uploadSessionId);const{file:u,fileName:d,...f}=t;l.push({partId:f.partId,...f.contentType?{contentType:f.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;l.push(s)}const f={agentName:e.agentName,requestParts:l};b&&(f.uploadSessionId=b),e.idempotencyKey&&(f.idempotencyKey=e.idempotencyKey),f.ownerId=i;const h={};u&&(h.taskKind=u),null!=d&&(h.duration=d),e.consumerPublicKey&&(h.consumerPublicKey=e.consumerPublicKey),Object.keys(h).length>0&&(f.extensions={blocks:h}),e.pushNotificationConfig&&(f.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(f.retryPolicy=e.retryPolicy);const m=await t(this.config,"SendMessage",f),g=m.extensions?.blocks,k=g?.readToken??null,S=g?.streamChannels?.status??void 0;if(!0===m.idempotent&&!!m.state&&y.has(m.state))return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:S,agentName:e.agentName,pubnub:null,ownsSubscribeClient:!1,sdkOptions:{subscribeKey:this._subscribeKey,publishKey:this._publishKey},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const C=this.createPerSessionPubNub(k),T={subscribeKey:this._subscribeKey,publishKey:this._publishKey},K=S??n(m.taskId,m.orgId??i);try{const t=await C.time(),n=String(t.timetoken),r=p(C);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=w(await I(r,K),m.taskId,e.agentName,T);a=t.streams,c=t.artifacts,u=t.highWaterMark,o=t.terminalState}if(o&&y.has(o))return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:K,agentName:e.agentName,pubnub:C,ownsSubscribeClient:!0,sdkOptions:T,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:o,skipSubscription:!0,preloadedStreams:a,preloadedArtifacts:c});const d="0"!==u?u:n,b=[];let l,f=!1;const h={message:e=>{if(e.channel!==K)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");f&&l?l(t,n):b.push({message:t,timetoken:n})}};C.addListener(h),C.subscribe({channels:[K],timetoken:d});const g=new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:K,agentName:e.agentName,pubnub:C,ownsSubscribeClient:!0,sdkOptions:T,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:h,channel:K,onReady:e=>{l=e}}});if(l){const e=l;for(const t of b)t.timetoken>d&&e(t.message,t.timetoken)}return b.length=0,f=!0,g}catch{return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:K,agentName:e.agentName,pubnub:C,ownsSubscribeClient:!0,sdkOptions:T,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}}async connect(e){const{taskId:t}=e;if(!this.config.authProvider?.getAuthHeader())throw new Error("connect() requires an authenticated TaskClient. Use apiKey, tokenEndpoint, or tokenProvider. AgentAuth is not supported for consumer task connections.");const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const i=n.agentName??"",r=n.state??"",o=await this.fetchConsumerReadToken(t),{pamToken:a,channel:c}=o,u={subscribeKey:this._subscribeKey,publishKey:this._publishKey},d=this.createPerSessionPubNub(a);try{const o=await d.time(),b=String(o.timetoken),l=p(d);let f,h=new Map,m=[],g="0";if(l?.fetchMessages){const e=w(await I(l,c),t,i,u);h=e.streams,m=e.artifacts,g=e.highWaterMark,f=e.terminalState}const k=f&&y.has(f)?f:r;if(y.has(k))return new s({taskId:t,ownerId:n.owner??"",readToken:a,statusChannel:c,agentName:i,pubnub:d,ownsSubscribeClient:!0,sdkOptions:u,rpcConfig:this.config,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:k,skipSubscription:!0,preloadedStreams:h,preloadedArtifacts:m});const S="0"!==g?g:b,C=[];let T,K=!1;const P={message:e=>{if(e.channel!==c)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");K&&T?T(t,n):C.push({message:t,timetoken:n})}};d.addListener(P),d.subscribe({channels:[c],timetoken:S});const N=new s({taskId:t,ownerId:n.owner??"",readToken:a,statusChannel:c,agentName:i,pubnub:d,ownsSubscribeClient:!0,sdkOptions:u,rpcConfig:this.config,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,state:r,preloadedStreams:h,preloadedArtifacts:m,externalSubscription:{listener:P,channel:c,onReady:e=>{T=e}}});if(T){const e=T;for(const t of C)t.timetoken>S&&e(t.message,t.timetoken)}return C.length=0,K=!0,N}catch(e){throw d.destroy(),e}}async getTask(e){return(await t(this.config,"GetTask",{taskId:e})).task}async listTasks(e){return t(this.config,"ListTasks",{...e})}async cancelTask(e){await t(this.config,"CancelTask",{taskId:e})}async pauseTask(e){await t(this.config,"PauseTask",{taskId:e})}async resumeTask(e){await t(this.config,"ResumeTask",{taskId:e})}async retryTask(e){await t(this.config,"RetryTask",{taskId:e})}async terminateTask(e){await t(this.config,"TerminateTask",{taskId:e})}subscribeToTask(e,t,s){return function(e,t,s,i){const r=n(t,s),o={message:e=>{if(e.channel!==r)return;const t=e.message;if(t&&"object"==typeof t&&t.type){if(i.onEvent)try{i.onEvent(t)}catch(e){S(e,"onEvent",t,i.onError)}switch(t.type){case"progress":if(i.onProgress)try{i.onProgress(t)}catch(e){S(e,"onProgress",t,i.onError)}break;case"artifact":if(i.onArtifact)try{i.onArtifact(t)}catch(e){S(e,"onArtifact",t,i.onError)}break;case"terminal":if(i.onTerminal)try{i.onTerminal(t)}catch(e){S(e,"onTerminal",t,i.onError)}break;case"system":if(i.onSystem)try{i.onSystem(t)}catch(e){S(e,"onSystem",t,i.onError)}}}}};return e.addListener(o),e.subscribe({channels:[r],timetoken:1e3}),{unsubscribe(){e.removeListener(o),e.unsubscribe({channels:[r]})}}}(this.getPubNub(),e,t,s)}}function S(e,t,n,s){const i=e instanceof Error?e:new Error(String(e));if(s)try{s(i,{entryPoint:"subscribeToTask",callbackType:t,event:n})}catch{}else console.warn(`[subscribeToTask] callback error in ${t}:`,i.message)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"pubnub";import{invertDirection as e}from"../stream/index.js";import{StreamRef as s}from"./stream-ref.js";import{taskChannel as r}from"./channel-manager.js";import{callRpc as i}from"./rpc-client.js";import{downloadArtifact as a}from"./artifacts.js";export const TERMINAL_STATES=new Set(["completed","failed","canceled"]);export class TaskSession{constructor(t){if(this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[],this.streams=new Map,this.artifacts=[],this.streamWaiters=[],this.terminalWaiters=[],this.closed=!1,this.listener=null,this.terminalReceived=!1,this.drainTimer=null,this.openStreamClients=new Set,this.seenTimetokens=new Set,this.taskId=t.taskId,this.ownerId=t.ownerId,this.orgId=t.orgId??t.ownerId,this.readToken=t.readToken,this.agentName=t.agentName,this.pubnub=t.pubnub,this.ownsSubscribeClient=t.ownsSubscribeClient??!1,this.sdkOptions=t.sdkOptions,this.rpcConfig=t.rpcConfig??null,this.statusChannel=t.statusChannel??r(t.taskId,this.orgId),this.idempotent=t.idempotent,this.queued=t.queued,this.pushConfigId=t.pushConfigId,this.autoDrain=t.autoDrain??!0,this.drainWindowMs=t.drainWindowMs??3e4,this.state=t.state,this.skipSubscriptionMode=t.skipSubscription??!1,this.subscribeKey=t.sdkOptions.subscribeKey,this.publishKey=t.sdkOptions.publishKey??"",t.preloadedStreams)for(const[e,r]of t.preloadedStreams){const t=new s(r.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preClosed?this.closed=!0:t.skipSubscription||(t.externalSubscription?(this.listener=t.externalSubscription.listener,t.externalSubscription.onReady(this.handleEvent.bind(this))):this.setupSubscription())}setupSubscription(){const t=this.pubnub,e=this.statusChannel;this.listener={message:t=>{if(t.channel!==e)return;const s=t.message;if(!s||"object"!=typeof s||!s.type)return;const r=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,r)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const r=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(r,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else console.warn(`[TaskSession] callback error in ${e}:`,r.message)}handleEvent(t,e){if(!this.closed){if(e){if(this.seenTimetokens.has(e))return;if(this.seenTimetokens.add(e),this.seenTimetokens.size>TaskSession.SEEN_TIMETOKENS_MAX){const t=this.seenTimetokens.values().next().value;void 0!==t&&this.seenTimetokens.delete(t)}}for(const e of this.eventCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onEvent",t)}switch(t.type){case"progress":for(const e of this.progressCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onProgress",t)}"stream_started"===t.streamEvent&&t.streams&&this.handleStreamStarted(t);break;case"artifact":this.accumulateArtifact(t);for(const e of this.artifactCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onArtifact",t)}break;case"terminal":this.state=t.state;for(const e of this.terminalCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onTerminal",t)}for(const e of this.terminalWaiters)e.resolve(t);this.terminalWaiters=[],this.autoDrain?this.startAutoDrain():this.close()}}}trackStreamClient(t){this.openStreamClients.add(t),t.onInboundDone(()=>{this.openStreamClients.delete(t),t.isActive&&t.end().catch(()=>{}),this.terminalReceived&&0===this.openStreamClients.size&&(this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null),this.close())})}accumulateArtifact(t){const e=t.artifactRef;e&&"object"==typeof e&&e.kind&&this.artifacts.push(e)}handleStreamStarted(t){const r=t.streams;if(!r||"object"!=typeof r)return;const i=t.declaredStream;for(const[t,a]of Object.entries(r)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const r=a.direction,n=e(r),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const l=a.affinity;if("dedicated"!==l&&"shared"!==l){console.warn(`[TaskSession] live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const c={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:r,localDirection:n,format:o,affinity:l,metadata:a.metadata,declaredStream:i},h=new s(c,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,h);for(const t of this.streamCallbacks)try{t(h)}catch(t){this.routeCallbackError(t,"onStream",h)}this.resolveWaiters(h)}}startAutoDrain(){this.closed||(this.terminalReceived=!0,0!==this.openStreamClients.size?this.drainTimer=setTimeout(()=>{this.drainTimer=null;for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.close()},this.drainWindowMs):this.close())}findStreamByIdOrDeclared(t){const e=this.streams.get(t);if(e)return e;for(const e of this.streams.values())if(e.descriptor.declaredStream===t)return e}resolveWaiters(t){const e=[];for(const s of this.streamWaiters){let r=!1;if(void 0!==s.streamId)r=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{r=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else r=!0;r?s.resolve(t):e.push(s)}this.streamWaiters=e}onProgress(t){return this.progressCallbacks.push(t),()=>{this.progressCallbacks=this.progressCallbacks.filter(e=>e!==t)}}onArtifact(t){return this.artifactCallbacks.push(t),()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){return this.terminalCallbacks.push(t),()=>{this.terminalCallbacks=this.terminalCallbacks.filter(e=>e!==t)}}onEvent(t){return this.eventCallbacks.push(t),()=>{this.eventCallbacks=this.eventCallbacks.filter(e=>e!==t)}}onError(t){return this.errorCallbacks.push(t),()=>{this.errorCallbacks=this.errorCallbacks.filter(e=>e!==t)}}listArtifacts(){return[...this.artifacts]}async downloadArtifact(e){if(this.pubnub)return a(e,this.pubnub);const s=`blocks-dl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,r=new t({subscribeKey:this.subscribeKey,publishKey:this.publishKey||void 0,userId:s});this.readToken&&r.setToken(this.readToken);try{return await a(e,r)}finally{r.destroy()}}onStream(t){this.streamCallbacks.push(t);for(const e of this.streams.values())try{t(e)}catch(t){this.routeCallbackError(t,"onStream",e)}return()=>{this.streamCallbacks=this.streamCallbacks.filter(e=>e!==t)}}listStreams(){return[...this.streams.values()]}openAllStreams(t){const e=[];for(const s of this.streams.values()){const r=s.descriptor.localDirection;if("inbound"===r||"bidirectional"===r)try{e.push(s.open(t))}catch{}}return e}waitForStream(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));if(this.skipSubscriptionMode){if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive."))}if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,streamId:t})})}waitForStreamWhere(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));for(const e of this.streams.values())try{if(t(e))return Promise.resolve(e)}catch(t){this.routeCallbackError(t,"streamPredicate",e)}return this.skipSubscriptionMode?Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive.")):new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,predicate:t})})}async waitForTerminal(t){if(this.state&&TERMINAL_STATES.has(this.state))return{type:"terminal",taskId:this.taskId,state:this.state};if(this.closed)throw new Error("TaskSession closed");return new Promise((e,s)=>{const r={resolve:e,reject:s};if(this.terminalWaiters.push(r),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(r);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),i=r.resolve,a=r.reject;r.resolve=t=>{clearTimeout(e),i(t)},r.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:r,resolve:i,basename:a}=await import("node:path"),n=i(t);e(n,{recursive:!0});const o=[],l=this.listArtifacts();for(let t=0;t<l.length;t++){const e=await this.downloadArtifact(l[t]),c=e.fileName??`artifact-${t}`,h=i(r(n,a(c)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${c}" resolves outside target directory`);s(h,e.data),o.push(h)}return o}async cancel(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.cancelTask() directly");await i(this.rpcConfig,"CancelTask",{taskId:this.taskId})}async terminate(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.terminateTask() directly");await i(this.rpcConfig,"TerminateTask",{taskId:this.taskId})}close(){if(this.closed)return;this.closed=!0,this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null);for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.openStreamClients.clear();const t=new Error("TaskSession closed");for(const e of this.streamWaiters)e.reject(t);this.streamWaiters=[];for(const e of this.terminalWaiters)e.reject(t);this.terminalWaiters=[],this.pubnub&&(this.listener&&(this.pubnub.removeListener(this.listener),this.listener=null),this.pubnub.unsubscribe({channels:[this.statusChannel]}),this.ownsSubscribeClient&&this.pubnub.destroy()),this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[]}async asyncClose(){if(this.closed)return;const t=[...this.openStreamClients].filter(t=>t.isActive).map(t=>t.end().catch(()=>{}));await Promise.all(t),this.openStreamClients.clear(),this.close()}async[Symbol.asyncDispose](){await this.asyncClose()}get isClosed(){return this.closed}}TaskSession.SEEN_TIMETOKENS_MAX=200;
|
|
1
|
+
import t from"pubnub";import{invertDirection as e}from"../stream/index.js";import{StreamRef as s}from"./stream-ref.js";import{taskChannel as i}from"./channel-manager.js";import{callRpc as r}from"./rpc-client.js";import{downloadArtifact as a}from"./artifacts.js";export const TERMINAL_STATES=new Set(["completed","failed","canceled"]);export class TaskSession{constructor(t){if(this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[],this.streams=new Map,this.artifacts=[],this.streamWaiters=[],this.terminalWaiters=[],this.closed=!1,this.listener=null,this.terminalReceived=!1,this.drainTimer=null,this.openStreamClients=new Set,this.seenTimetokens=new Set,this.taskId=t.taskId,this.ownerId=t.ownerId,this.orgId=t.orgId??t.ownerId,this.readToken=t.readToken,this.agentName=t.agentName,this.pubnub=t.pubnub,this.ownsSubscribeClient=t.ownsSubscribeClient??!1,this.sdkOptions=t.sdkOptions,this.rpcConfig=t.rpcConfig??null,this.statusChannel=t.statusChannel??i(t.taskId,this.orgId),this.idempotent=t.idempotent,this.queued=t.queued,this.pushConfigId=t.pushConfigId,this.autoDrain=t.autoDrain??!0,this.drainWindowMs=t.drainWindowMs??3e4,this.state=t.state,this.skipSubscriptionMode=t.skipSubscription??!1,this.subscribeKey=t.sdkOptions.subscribeKey,this.publishKey=t.sdkOptions.publishKey??"",t.preloadedStreams)for(const[e,i]of t.preloadedStreams){const t=new s(i.descriptor,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(e,t)}t.preloadedArtifacts&&this.artifacts.push(...t.preloadedArtifacts),t.preClosed?this.closed=!0:t.skipSubscription||(t.externalSubscription?(this.listener=t.externalSubscription.listener,t.externalSubscription.onReady(this.handleEvent.bind(this))):this.setupSubscription())}setupSubscription(){const t=this.pubnub,e=this.statusChannel;this.listener={message:t=>{if(t.channel!==e)return;const s=t.message;if(!s||"object"!=typeof s||!s.type)return;const i=null!=t.timetoken?String(t.timetoken):void 0;this.handleEvent(s,i)}},t.addListener(this.listener),t.subscribe({channels:[e],timetoken:1e3})}routeCallbackError(t,e,s){const i=t instanceof Error?t:new Error(String(t));if(this.errorCallbacks.length>0)for(const t of this.errorCallbacks)try{t(i,{entryPoint:"taskSession",callbackType:e,event:s})}catch{}else console.warn(`[TaskSession] callback error in ${e}:`,i.message)}handleEvent(t,e){if(!this.closed){if(e){if(this.seenTimetokens.has(e))return;if(this.seenTimetokens.add(e),this.seenTimetokens.size>TaskSession.SEEN_TIMETOKENS_MAX){const t=this.seenTimetokens.values().next().value;void 0!==t&&this.seenTimetokens.delete(t)}}for(const e of this.eventCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onEvent",t)}switch(t.type){case"progress":for(const e of this.progressCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onProgress",t)}"stream_started"===t.streamEvent&&t.streams&&this.handleStreamStarted(t);break;case"artifact":this.accumulateArtifact(t);for(const e of this.artifactCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onArtifact",t)}break;case"terminal":this.state=t.state;for(const e of this.terminalCallbacks)try{e(t)}catch(e){this.routeCallbackError(e,"onTerminal",t)}for(const e of this.terminalWaiters)e.resolve(t);this.terminalWaiters=[],this.autoDrain?this.startAutoDrain():this.close()}}}trackStreamClient(t){this.openStreamClients.add(t),t.onInboundDone(()=>{this.openStreamClients.delete(t),t.isActive&&t.end().catch(()=>{}),this.terminalReceived&&0===this.openStreamClients.size&&(this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null),this.close())})}accumulateArtifact(t){const e=t.artifactRef;e&&"object"==typeof e&&e.kind&&this.artifacts.push(e)}handleStreamStarted(t){const i=t.streams;if(!i||"object"!=typeof i)return;const r=t.declaredStream;for(const[t,a]of Object.entries(i)){if(!a||"object"!=typeof a)continue;if(this.streams.has(t))continue;const i=a.direction,n=e(i),o=a.format;if(!o||"bytes"!==o&&"events"!==o)continue;const l=a.affinity;if("dedicated"!==l&&"shared"!==l){console.warn(`[TaskSession] live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const c={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:i,localDirection:n,format:o,affinity:l,metadata:a.metadata,declaredStream:r},h=new s(c,this.sdkOptions,{onOpen:t=>this.trackStreamClient(t),sessionState:()=>this.state});this.streams.set(t,h);for(const t of this.streamCallbacks)try{t(h)}catch(t){this.routeCallbackError(t,"onStream",h)}this.resolveWaiters(h)}}startAutoDrain(){this.closed||(this.terminalReceived=!0,0!==this.openStreamClients.size?this.drainTimer=setTimeout(()=>{this.drainTimer=null;for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.close()},this.drainWindowMs):this.close())}findStreamByIdOrDeclared(t){const e=this.streams.get(t);if(e)return e;for(const e of this.streams.values())if(e.descriptor.declaredStream===t)return e}resolveWaiters(t){const e=[];for(const s of this.streamWaiters){let i=!1;if(void 0!==s.streamId)i=t.descriptor.declaredStream===s.streamId||t.descriptor.streamId===s.streamId;else if(s.predicate)try{i=s.predicate(t)}catch(e){this.routeCallbackError(e,"streamPredicate",t)}else i=!0;i?s.resolve(t):e.push(s)}this.streamWaiters=e}onProgress(t){return this.progressCallbacks.push(t),()=>{this.progressCallbacks=this.progressCallbacks.filter(e=>e!==t)}}onArtifact(t){return this.artifactCallbacks.push(t),()=>{this.artifactCallbacks=this.artifactCallbacks.filter(e=>e!==t)}}onTerminal(t){if(this.terminalCallbacks.push(t),this.state&&TERMINAL_STATES.has(this.state)){const e={type:"terminal",taskId:this.taskId,state:this.state};try{t(e)}catch(t){this.routeCallbackError(t,"onTerminal",e)}}return()=>{this.terminalCallbacks=this.terminalCallbacks.filter(e=>e!==t)}}onEvent(t){return this.eventCallbacks.push(t),()=>{this.eventCallbacks=this.eventCallbacks.filter(e=>e!==t)}}onError(t){return this.errorCallbacks.push(t),()=>{this.errorCallbacks=this.errorCallbacks.filter(e=>e!==t)}}listArtifacts(){return[...this.artifacts]}async downloadArtifact(e){if(this.pubnub)return a(e,this.pubnub);const s=`blocks-dl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`,i=new t({subscribeKey:this.subscribeKey,publishKey:this.publishKey||void 0,userId:s});this.readToken&&i.setToken(this.readToken);try{return await a(e,i)}finally{i.destroy()}}onStream(t){this.streamCallbacks.push(t);for(const e of this.streams.values())try{t(e)}catch(t){this.routeCallbackError(t,"onStream",e)}return()=>{this.streamCallbacks=this.streamCallbacks.filter(e=>e!==t)}}listStreams(){return[...this.streams.values()]}openAllStreams(t){const e=[];for(const s of this.streams.values()){const i=s.descriptor.localDirection;if("inbound"===i||"bidirectional"===i)try{e.push(s.open(t))}catch{}}return e}waitForStream(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));if(this.skipSubscriptionMode){if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive."))}if(void 0!==t){const e=this.findStreamByIdOrDeclared(t);if(e)return Promise.resolve(e)}else{if(1===this.streams.size)return Promise.resolve(this.streams.values().next().value);if(this.streams.size>1)return Promise.reject(new Error(`Multiple streams exist (${this.streams.size}). Use waitForStream(streamId) or waitForStreamWhere(predicate) to select one.`))}return new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,streamId:t})})}waitForStreamWhere(t){if(this.closed)return Promise.reject(new Error("TaskSession is closed"));for(const e of this.streams.values())try{if(t(e))return Promise.resolve(e)}catch(t){this.routeCallbackError(t,"streamPredicate",e)}return this.skipSubscriptionMode?Promise.reject(new Error("No matching stream found. This is a terminal task session with no live subscription -- no future stream announcements will arrive.")):new Promise((e,s)=>{this.streamWaiters.push({resolve:e,reject:s,predicate:t})})}async waitForTerminal(t){if(this.state&&TERMINAL_STATES.has(this.state))return{type:"terminal",taskId:this.taskId,state:this.state};if(this.closed)throw new Error("TaskSession closed");return new Promise((e,s)=>{const i={resolve:e,reject:s};if(this.terminalWaiters.push(i),void 0!==t){const e=setTimeout(()=>{const e=this.terminalWaiters.indexOf(i);-1!==e&&this.terminalWaiters.splice(e,1),s(new Error(`waitForTerminal timed out after ${t}ms`))},t),r=i.resolve,a=i.reject;i.resolve=t=>{clearTimeout(e),r(t)},i.reject=t=>{clearTimeout(e),a(t)}}})}async saveArtifacts(t){const{mkdirSync:e,writeFileSync:s}=await import("node:fs"),{join:i,resolve:r,basename:a}=await import("node:path"),n=r(t);e(n,{recursive:!0});const o=[],l=this.listArtifacts();for(let t=0;t<l.length;t++){const e=await this.downloadArtifact(l[t]),c=e.fileName??`artifact-${t}`,h=r(i(n,a(c)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${c}" resolves outside target directory`);s(h,e.data),o.push(h)}return o}async cancel(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.cancelTask() directly");await r(this.rpcConfig,"CancelTask",{taskId:this.taskId})}async terminate(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.terminateTask() directly");await r(this.rpcConfig,"TerminateTask",{taskId:this.taskId})}close(){if(this.closed)return;this.closed=!0,this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null);for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.openStreamClients.clear();const t=new Error("TaskSession closed");for(const e of this.streamWaiters)e.reject(t);this.streamWaiters=[];for(const e of this.terminalWaiters)e.reject(t);this.terminalWaiters=[],this.pubnub&&(this.listener&&(this.pubnub.removeListener(this.listener),this.listener=null),this.pubnub.unsubscribe({channels:[this.statusChannel]}),this.ownsSubscribeClient&&this.pubnub.destroy()),this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[]}async asyncClose(){if(this.closed)return;const t=[...this.openStreamClients].filter(t=>t.isActive).map(t=>t.end().catch(()=>{}));await Promise.all(t),this.openStreamClients.clear(),this.close()}async[Symbol.asyncDispose](){await this.asyncClose()}get isClosed(){return this.closed}}TaskSession.SEEN_TIMETOKENS_MAX=200;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocks-network/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Blocks Network SDK — build and run A2A agents on
|
|
5
|
+
"description": "Blocks Network SDK — build and run A2A agents on PubNub",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"browser": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
},
|
|
59
59
|
"repository": {
|
|
60
60
|
"type": "git",
|
|
61
|
-
"url": "git+https://github.com/blocksnetwork
|
|
61
|
+
"url": "git+https://github.com/pubnub/blocksnetwork.git",
|
|
62
62
|
"directory": "blocks-sdk/sdks/node"
|
|
63
63
|
},
|
|
64
|
-
"homepage": "https://github.com/blocksnetwork/blocks-sdk/
|
|
64
|
+
"homepage": "https://github.com/pubnub/blocksnetwork/tree/main/blocks-sdk/sdks/node#readme",
|
|
65
65
|
"bugs": {
|
|
66
|
-
"url": "https://github.com/blocksnetwork/
|
|
66
|
+
"url": "https://github.com/pubnub/blocksnetwork/issues"
|
|
67
67
|
},
|
|
68
|
-
"license": "
|
|
68
|
+
"license": "PubNub",
|
|
69
69
|
"overrides": {
|
|
70
70
|
"pubnub": {
|
|
71
71
|
"react-native-url-polyfill": "npm:empty-npm-package@1.0.0"
|