@blocks-network/sdk 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +632 -0
  2. package/dist/cli/run.d.ts +39 -0
  3. package/dist/cli/run.js +2 -0
  4. package/dist/config-loader.d.ts +10 -0
  5. package/dist/config-loader.js +1 -0
  6. package/dist/defaults.d.ts +7 -0
  7. package/dist/defaults.js +1 -0
  8. package/dist/env.d.ts +5 -0
  9. package/dist/env.js +1 -0
  10. package/dist/index.d.ts +28 -0
  11. package/dist/index.js +1 -0
  12. package/dist/runtime/agent-auth.d.ts +94 -0
  13. package/dist/runtime/agent-auth.js +1 -0
  14. package/dist/runtime/agent-instance.d.ts +233 -0
  15. package/dist/runtime/agent-instance.js +1 -0
  16. package/dist/runtime/agent-registry.d.ts +270 -0
  17. package/dist/runtime/agent-registry.js +1 -0
  18. package/dist/runtime/artifacts.d.ts +65 -0
  19. package/dist/runtime/artifacts.js +1 -0
  20. package/dist/runtime/auth-provider.d.ts +39 -0
  21. package/dist/runtime/auth-provider.js +1 -0
  22. package/dist/runtime/cdm-config.d.ts +16 -0
  23. package/dist/runtime/cdm-config.js +1 -0
  24. package/dist/runtime/channel-manager.d.ts +125 -0
  25. package/dist/runtime/channel-manager.js +1 -0
  26. package/dist/runtime/consumer-auth.d.ts +112 -0
  27. package/dist/runtime/consumer-auth.js +1 -0
  28. package/dist/runtime/credential-cache.d.ts +40 -0
  29. package/dist/runtime/credential-cache.js +1 -0
  30. package/dist/runtime/file-input.d.ts +37 -0
  31. package/dist/runtime/file-input.js +1 -0
  32. package/dist/runtime/file-upload.d.ts +112 -0
  33. package/dist/runtime/file-upload.js +1 -0
  34. package/dist/runtime/part-helpers.d.ts +52 -0
  35. package/dist/runtime/part-helpers.js +1 -0
  36. package/dist/runtime/protocol-version.d.ts +18 -0
  37. package/dist/runtime/protocol-version.js +1 -0
  38. package/dist/runtime/pubnub-client.d.ts +8 -0
  39. package/dist/runtime/pubnub-client.js +1 -0
  40. package/dist/runtime/pubnub-types.d.ts +119 -0
  41. package/dist/runtime/pubnub-types.js +1 -0
  42. package/dist/runtime/rpc-client.d.ts +45 -0
  43. package/dist/runtime/rpc-client.js +1 -0
  44. package/dist/runtime/stream-context.d.ts +69 -0
  45. package/dist/runtime/stream-context.js +1 -0
  46. package/dist/runtime/stream-ref.d.ts +74 -0
  47. package/dist/runtime/stream-ref.js +1 -0
  48. package/dist/runtime/stream-registry.d.ts +133 -0
  49. package/dist/runtime/stream-registry.js +1 -0
  50. package/dist/runtime/stream-setup-helper.d.ts +87 -0
  51. package/dist/runtime/stream-setup-helper.js +1 -0
  52. package/dist/runtime/task-client.d.ts +259 -0
  53. package/dist/runtime/task-client.js +1 -0
  54. package/dist/runtime/task-session.d.ts +224 -0
  55. package/dist/runtime/task-session.js +1 -0
  56. package/dist/runtime/write-affinity.d.ts +22 -0
  57. package/dist/runtime/write-affinity.js +1 -0
  58. package/dist/runtime/write-affinity.test.d.ts +1 -0
  59. package/dist/runtime/write-affinity.test.js +1 -0
  60. package/dist/stream/bytes.d.ts +14 -0
  61. package/dist/stream/bytes.js +1 -0
  62. package/dist/stream/descriptor.d.ts +47 -0
  63. package/dist/stream/descriptor.js +1 -0
  64. package/dist/stream/index.d.ts +11 -0
  65. package/dist/stream/index.js +1 -0
  66. package/dist/stream/stream-bundle.d.ts +82 -0
  67. package/dist/stream/stream-bundle.js +1 -0
  68. package/dist/stream/stream-client.d.ts +192 -0
  69. package/dist/stream/stream-client.js +1 -0
  70. package/dist/stream/types.d.ts +75 -0
  71. package/dist/stream/types.js +1 -0
  72. package/dist/stream/validate.d.ts +15 -0
  73. package/dist/stream/validate.js +1 -0
  74. package/package.json +74 -0
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Shared Stream Registry
3
+ *
4
+ * Instance-level map of active embedded and externally-coordinated streams.
5
+ * Named streams are ref-counted across tasks by a `taskIds: Set<string>`
6
+ * set — one entry per ref-holding task. Unnamed streams have a single
7
+ * taskId and are scoped to their task.
8
+ *
9
+ * Compatibility checks on named stream reuse:
10
+ * - direction must match
11
+ * - format must match
12
+ * - external flag must match
13
+ *
14
+ * First creator wins for: onActivate, transport tuning options, affinity.
15
+ * Duplicate onActivate callbacks for existing streams are silently ignored.
16
+ *
17
+ * `acquire()` is idempotent within a task: a second call with the same
18
+ * `(streamId, taskId)` returns `{ isNew: false, isNewForTask: false }`
19
+ * and does not grow the set. See SHARED_STREAM_LIFECYCLE_IMPL §Fix (e).
20
+ */
21
+ import type { StreamClient } from '../stream/index.js';
22
+ import type { StreamAffinity } from '../stream/descriptor.js';
23
+ export interface StreamRegistryEntry {
24
+ streamId: string;
25
+ direction: 'outbound' | 'inbound' | 'bidirectional';
26
+ format: 'bytes' | 'events';
27
+ external: boolean;
28
+ /** Per-entry task tracking — the set of tasks that currently hold a ref. */
29
+ taskIds: Set<string>;
30
+ /** Affinity captured at first acquire (constant for the life of the entry). */
31
+ affinity: StreamAffinity;
32
+ streamClient: StreamClient | null;
33
+ activated: boolean;
34
+ /** The running onActivate promise, if any. */
35
+ activatePromise: Promise<void> | null;
36
+ /**
37
+ * First-acquirer setup promise. Installed synchronously on the entry
38
+ * before the first acquirer awaits `performStreamSetup`, resolved
39
+ * once `streamClient` is installed (or rejected if setup fails).
40
+ * Concurrent second acquirers on the same shared entry MUST await
41
+ * this promise before consulting `streamClient`; otherwise a race
42
+ * between first-acquirer setup and second-acquirer attach either
43
+ * throws "Stream exists but has no client" (Node) or silently creates
44
+ * a duplicate writer (Python). Null when setup is not in flight.
45
+ */
46
+ setupPromise: Promise<void> | null;
47
+ /**
48
+ * Derived reference count. Kept as a getter so existing callers /
49
+ * tests that inspect `entry.refCount` continue to work after the
50
+ * shape change (see SHARED_STREAM_LIFECYCLE_IMPL §Risk "Registry
51
+ * shape change ripples").
52
+ */
53
+ readonly refCount: number;
54
+ }
55
+ /** Result of an `acquire` call: distinguishes fresh entry / fresh-for-task / idempotent. */
56
+ export interface AcquireResult {
57
+ entry: StreamRegistryEntry;
58
+ /** First time this entry was created (no prior ref-holders). */
59
+ isNew: boolean;
60
+ /**
61
+ * First time THIS task attached to the entry. True on new entries,
62
+ * true on existing entries when the taskId was not already tracked,
63
+ * false when the same task re-acquires idempotently.
64
+ */
65
+ isNewForTask: boolean;
66
+ }
67
+ interface AcquireOpts {
68
+ direction: 'outbound' | 'inbound' | 'bidirectional';
69
+ format: 'bytes' | 'events';
70
+ external: boolean;
71
+ affinity?: StreamAffinity;
72
+ }
73
+ export declare class StreamRegistry {
74
+ private readonly entries;
75
+ /**
76
+ * Get or create a registry entry for a stream.
77
+ *
78
+ * Three-case matrix (per SHARED_STREAM_LIFECYCLE_IMPL §Fix (e)):
79
+ * 1. Entry doesn't exist -> create, taskIds = {taskId}, isNew: true, isNewForTask: true
80
+ * 2. Entry exists + taskId already tracked -> idempotent no-op, isNew: false, isNewForTask: false
81
+ * 3. Entry exists + taskId is new -> add to set, isNew: false, isNewForTask: true
82
+ */
83
+ acquire(streamId: string, taskId: string, opts: AcquireOpts): AcquireResult;
84
+ /** Get a registry entry by stream ID. */
85
+ get(streamId: string): StreamRegistryEntry | undefined;
86
+ /**
87
+ * Release a task's reference to a stream.
88
+ *
89
+ * Pure bookkeeping: removes ``taskId`` from the entry's set and, on
90
+ * last-ref, removes the entry from the registry map. Does NOT tear
91
+ * down the underlying ``streamClient`` — callers snapshot the entry
92
+ * before this call (via ``get()``) and invoke ``streamClient.end()``
93
+ * themselves. This keeps the registry side-effect-free and lets
94
+ * agent-level teardown (logging, presence updates) live in one
95
+ * place. Matches the Python SDK's ``release()`` shape.
96
+ *
97
+ * Returns the remaining refCount (0 means the entry was removed).
98
+ */
99
+ release(streamId: string, taskId: string): number;
100
+ /**
101
+ * Force-remove a stream entry (for failStream).
102
+ *
103
+ * Deletes the entry from the registry and returns it. `taskIds` on
104
+ * the returned entry is LEFT INTACT so `failStream` can iterate the
105
+ * set of tasks to publish terminal failure to. The returned entry is
106
+ * disowned — `refCount` as exposed by the getter reflects the
107
+ * current size of the retained task set (the getter reads
108
+ * `taskIds.size` live, so a caller that mutates the set after
109
+ * removal sees the mutation), but no one can `release` it.
110
+ *
111
+ * DO NOT clear `taskIds` here "for hygiene": `failStream` reads the
112
+ * set to fan out `state: 'failed'` terminals, and Python's prior
113
+ * implementation silently broke that fan-out by clearing the set
114
+ * before returning. The single reader lives at
115
+ * `agent-instance.ts#failStreamImpl` — verify its loop still works
116
+ * before changing this behavior. See QUESTIONS.md R6
117
+ * (shared_stream_lifecycle).
118
+ */
119
+ forceRemove(streamId: string): StreamRegistryEntry | undefined;
120
+ /**
121
+ * Release all streams for a given task.
122
+ * Returns entries whose refCount reached 0 (removed from registry).
123
+ * Callers should end() the streamClient on each returned entry.
124
+ */
125
+ releaseAllForTask(taskId: string): StreamRegistryEntry[];
126
+ /** Count of active embedded stream processing contexts. */
127
+ get activeStreamCount(): number;
128
+ /** All stream IDs in the registry. */
129
+ streamIds(): string[];
130
+ /** Clear the entire registry. */
131
+ clear(): void;
132
+ }
133
+ export {};
@@ -0,0 +1 @@
1
+ export class StreamRegistry{constructor(){this.entries=new Map}acquire(e,t,r){const i=this.entries.get(e);if(i){if(i.external!==r.external)throw new Error(`Stream "${e}" incompatible: cannot mix embedded and external`);if(i.direction!==r.direction)throw new Error(`Stream "${e}" incompatible: direction mismatch (existing: ${i.direction}, requested: ${r.direction})`);if(i.format!==r.format)throw new Error(`Stream "${e}" incompatible: format mismatch (existing: ${i.format}, requested: ${r.format})`);const s=r.affinity??"dedicated";if(i.affinity!==s)throw new Error(`Stream "${e}" incompatible: affinity mismatch (existing: ${i.affinity}, requested: ${s})`);return i.taskIds.has(t)?{entry:i,isNew:!1,isNewForTask:!1}:(i.taskIds.add(t),{entry:i,isNew:!1,isNewForTask:!0})}const s=function(e,t,r){const i=new Set([t]);return{streamId:e,direction:r.direction,format:r.format,external:r.external,taskIds:i,affinity:r.affinity??"dedicated",streamClient:null,activated:!1,activatePromise:null,setupPromise:null,get refCount(){return i.size}}}(e,t,r);return this.entries.set(e,s),{entry:s,isNew:!0,isNewForTask:!0}}get(e){return this.entries.get(e)}release(e,t){const r=this.entries.get(e);return r?(r.taskIds.delete(t),0===r.taskIds.size?(this.entries.delete(e),0):r.taskIds.size):0}forceRemove(e){const t=this.entries.get(e);return t&&this.entries.delete(e),t}releaseAllForTask(e){const t=[];for(const[r,i]of this.entries)i.taskIds.has(e)&&(i.taskIds.delete(e),0===i.taskIds.size&&(this.entries.delete(r),t.push(i)));return t}get activeStreamCount(){let e=0;for(const t of this.entries.values())t.external||e++;return e}streamIds(){return[...this.entries.keys()]}clear(){this.entries.clear()}}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Stream Setup Helper - T7a abort-payload parsing
3
+ *
4
+ * Internal helper for consuming the streamSetup Function's response.
5
+ * The streamSetup Function returns T7a via request.abort(customPayload),
6
+ * which the PubNub SDK surfaces as a 403 error. This helper extracts
7
+ * the T7a token from the error body after verifying the expected markers.
8
+ *
9
+ * This is a protocol-consumption helper only. It does not implement the
10
+ * full stream setup handshake (that belongs to Phase 3 SDK runtime).
11
+ *
12
+ * The stream setup protocol is documented in the SDK contract and event flow docs.
13
+ */
14
+ /**
15
+ * Parsed stream setup response extracted from the abort payload.
16
+ */
17
+ export interface StreamSetupResult {
18
+ taskId: string;
19
+ streamId: string;
20
+ channel: string;
21
+ direction: 'outbound' | 'inbound' | 'bidirectional';
22
+ phase: 'embedded' | 'token_request' | 'activate';
23
+ token?: string;
24
+ tokenTtlMinutes: number;
25
+ }
26
+ /**
27
+ * Structured error returned by the streamSetup Function for validation
28
+ * failures. The Function returns { ok: false, error: { code, message } }
29
+ * via request.abort(), which is also surfaced as a 403 error. This type
30
+ * lets callers distinguish a server-side validation rejection from an
31
+ * opaque PubNub 403.
32
+ */
33
+ export interface StreamSetupError {
34
+ code: string;
35
+ message: string;
36
+ }
37
+ /**
38
+ * Attempt to extract a structured error from a PubNub publish error.
39
+ *
40
+ * When the streamSetup Function rejects a request (e.g., missing
41
+ * durationMinutes, invalid direction), it returns
42
+ * { ok: false, error: { code, message } } via request.abort().
43
+ * This is surfaced as a 403 by the PubNub SDK, just like the success
44
+ * path. This function extracts the structured error from the 403 body.
45
+ *
46
+ * Returns null if the error is not a structured setup error (i.e., it is
47
+ * either a real 403 or a success abort payload).
48
+ *
49
+ * @param error - The error object thrown by pubnub.publish()
50
+ * @returns The parsed StreamSetupError, or null
51
+ */
52
+ export declare function parseStreamSetupError(error: unknown): StreamSetupError | null;
53
+ /**
54
+ * Attempt to parse a T7a stream setup response from a PubNub publish error.
55
+ *
56
+ * The streamSetup Function calls request.abort(customPayload), which causes
57
+ * the PubNub SDK to reject the publish with a PubNubError. The error
58
+ * object contains the custom payload at:
59
+ * error.status.errorData.message (already parsed by the Node SDK)
60
+ *
61
+ * This function checks the marker fields (ok: true, streamSetupResponse)
62
+ * and extracts the result. Returns null if the error is not a valid
63
+ * stream setup response (i.e., it is a real 403 error).
64
+ *
65
+ * @param error - The error object thrown by pubnub.publish()
66
+ * @returns The parsed StreamSetupResult, or null if not a valid setup response
67
+ */
68
+ export declare function parseStreamSetupResponse(error: unknown): StreamSetupResult | null;
69
+ /**
70
+ * Extract a structured error from a raw abort payload object.
71
+ * The streamSetup Function returns { ok: false, error: { code, message } }
72
+ * for validation failures (missing fields, invalid direction, invalid
73
+ * durationMinutes, etc.). This function checks for that shape and
74
+ * returns the error details, or null if the payload is not a structured error.
75
+ *
76
+ * @param payload - The parsed abort payload
77
+ * @returns The parsed StreamSetupError, or null if not an error payload
78
+ */
79
+ export declare function extractErrorFromPayload(payload: unknown): StreamSetupError | null;
80
+ /**
81
+ * Extract StreamSetupResult from a raw abort payload object.
82
+ * Validates the marker fields and required properties.
83
+ *
84
+ * @param payload - The parsed abort payload
85
+ * @returns The parsed StreamSetupResult, or null if invalid
86
+ */
87
+ export declare function extractFromPayload(payload: unknown): StreamSetupResult | null;
@@ -0,0 +1 @@
1
+ const t=["outbound","inbound","bidirectional"],e=["embedded","token_request","activate"];export function parseStreamSetupError(t){if(!t||"object"!=typeof t)return null;const e=t.status;if(!e)return null;if(403!==(e.statusCode??e.category))return null;const n=e.errorData;if(!n)return null;const r=n.message;return r&&"object"==typeof r?extractErrorFromPayload(r):null}export function parseStreamSetupResponse(t){if(!t||"object"!=typeof t)return null;const e=t.status;if(!e)return null;if(403!==(e.statusCode??e.category))return null;const n=e.errorData;if(!n)return null;const r=n.message;return r&&"object"==typeof r?extractFromPayload(r):null}export function extractErrorFromPayload(t){if(!t||"object"!=typeof t)return null;const e=t;if(!1!==e.ok)return null;const n=e.error;if(!n||"object"!=typeof n)return null;const r=n,o=r.code,u=r.message;return"string"==typeof o&&o&&"string"==typeof u&&u?{code:o,message:u}:null}export function extractFromPayload(n){if(!n||"object"!=typeof n)return null;const r=n;if(!0!==r.ok)return null;const o=r.streamSetupResponse;if(!o||"object"!=typeof o)return null;const u=o,l=u.taskId,s=u.streamId,i=u.channel,a=u.direction,c=u.phase,f=u.tokenTtlMinutes;if("string"!=typeof l||!l)return null;if("string"!=typeof s||!s)return null;if("string"!=typeof i||!i)return null;if("string"!=typeof a||!t.includes(a))return null;if("string"!=typeof c||!e.includes(c))return null;if("number"!=typeof f||f<=0)return null;const p={taskId:l,streamId:s,channel:i,direction:a,phase:c,tokenTtlMinutes:f},y=u.token;return"string"==typeof y&&y.length>0&&(p.token=y),p}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * TaskClient -- send tasks to other agents via the PubNub Functions RPC gateway.
3
+ *
4
+ * Provides:
5
+ * - `sendMessage()` -- calls JSON-RPC "SendMessage" and returns a TaskSession with eager subscription
6
+ * - `connect()` -- connect to an existing task and return a pre-populated TaskSession
7
+ * - `create()` -- static factory to build a TaskClient from env vars or CDM config
8
+ * - Task lifecycle methods: getTask, listTasks, cancelTask, pauseTask, resumeTask, retryTask, terminateTask
9
+ * - `subscribeToTask()` -- low-level subscribe to real-time task events via PubNub channels
10
+ */
11
+ import PubNub from 'pubnub';
12
+ import { TaskSession, type CallbackErrorContext } from './task-session.js';
13
+ import type { AgentAuth } from './agent-auth.js';
14
+ import type { AuthProvider } from './auth-provider.js';
15
+ import { type TokenResult, type TokenEndpointConfig } from './consumer-auth.js';
16
+ import { type AgentCard } from './agent-registry.js';
17
+ export interface TaskClientOptions {
18
+ subscribeKey: string;
19
+ publishKey?: string;
20
+ /** Internal/advanced hook for supplying an auth provider directly. */
21
+ authProvider?: AuthProvider;
22
+ /** Shared PubNub instance for low-level subscribeToTask() operations. */
23
+ pubnub?: PubNub;
24
+ /** Shared factory for low-level subscribeToTask(). Creates once, caches. */
25
+ createPubNub?: () => PubNub;
26
+ /**
27
+ * Per-session factory for sendMessage() -> TaskSession eager subscriptions.
28
+ * Must return a fresh PubNub client per call so each session gets its own
29
+ * token-isolated instance. Not used by subscribeToTask().
30
+ */
31
+ createSessionPubNub?: () => PubNub;
32
+ defaultOwnerId?: string;
33
+ baseUrl?: string;
34
+ /** AgentAuth instance for API key-based authentication */
35
+ agentAuth?: AgentAuth;
36
+ }
37
+ /**
38
+ * A request part item. Each part may include a `partId` referencing a
39
+ * declared input in the agent's io.inputs[].id.
40
+ * Explicit optional fields match the agent-instance RequestPart for consistency.
41
+ *
42
+ * For file inputs: set `file` (raw data) and `fileName`. The SDK
43
+ * automatically inlines small files (<= 16 KB) or runs the pre-signed
44
+ * URL upload flow for large files.
45
+ */
46
+ export interface SendMessageRequestPart {
47
+ partId?: string;
48
+ text?: string;
49
+ contentType?: string;
50
+ /** Raw file data. Small files (<= 16 KB) are inlined as base64;
51
+ * large files use the pre-signed URL upload flow. Accepts
52
+ * `Uint8Array`, `ArrayBuffer`, `Blob`, or `File` -- browser
53
+ * consumers can pass a `File` from `<input type="file">` directly. */
54
+ file?: Uint8Array | ArrayBuffer | Blob | File;
55
+ /** Original file name. Required when `file` is provided. */
56
+ fileName?: string;
57
+ [key: string]: unknown;
58
+ }
59
+ export interface SendMessageParams {
60
+ agentName: string;
61
+ requestParts: SendMessageRequestPart[];
62
+ /** Optional idempotency key for duplicate detection. Scoped to the caller's identity. */
63
+ idempotencyKey?: string;
64
+ ownerId?: string;
65
+ /** Task kind. Defaults to request when omitted. */
66
+ taskKind?: 'request' | 'pipe';
67
+ /** Duration in minutes. Required for pipe tasks. */
68
+ duration?: number;
69
+ /** Consumer's public key for E2E encryption. Included in extensions.blocks. */
70
+ consumerPublicKey?: string;
71
+ pushNotificationConfig?: {
72
+ url: string;
73
+ filter?: string;
74
+ authStrategy?: string;
75
+ };
76
+ retryPolicy?: {
77
+ maxRetries?: number;
78
+ expiresAfterSec?: number;
79
+ };
80
+ /** Enable auto-drain on terminal (default: true). When true, TaskSession
81
+ * waits for open streams to drain via stream_end before closing. When
82
+ * false, terminal causes immediate close with no stream force-end. */
83
+ autoDrain?: boolean;
84
+ /**
85
+ * Duration in milliseconds the session waits for already-open streams
86
+ * to finish draining naturally after a terminal event. Defaults to
87
+ * 30000 ms (30 seconds). Ignored when `autoDrain` is false.
88
+ *
89
+ * Only applies to streams that were opened while the task was still
90
+ * active. Unopened streams on a terminal session throw
91
+ * `StreamUnavailableError` per the merged t7c baseline.
92
+ */
93
+ drainWindowMs?: number;
94
+ }
95
+ export interface TaskInfo {
96
+ taskId: string;
97
+ agentName?: string;
98
+ owner?: string;
99
+ state?: string;
100
+ createdTime?: string;
101
+ updatedTime?: string;
102
+ [key: string]: unknown;
103
+ }
104
+ export interface ListTasksParams {
105
+ ownerId?: string;
106
+ agentName?: string;
107
+ state?: string;
108
+ limit?: number;
109
+ cursor?: string;
110
+ }
111
+ export interface ListTasksResult {
112
+ tasks: TaskInfo[];
113
+ next?: string;
114
+ totalCount?: number;
115
+ }
116
+ export interface TaskEvent {
117
+ type: string;
118
+ taskId: string;
119
+ [key: string]: unknown;
120
+ }
121
+ export interface TaskEventCallbacks {
122
+ /** Progress events (type: "progress") */
123
+ onProgress?: (event: TaskEvent) => void;
124
+ /** Artifact events (type: "artifact") */
125
+ onArtifact?: (event: TaskEvent) => void;
126
+ /** Terminal events (type: "terminal") -- task completed, failed, or canceled */
127
+ onTerminal?: (event: TaskEvent) => void;
128
+ /** System events (type: "system") -- paused, resumed, etc. */
129
+ onSystem?: (event: TaskEvent) => void;
130
+ /** Catch-all for any event */
131
+ onEvent?: (event: TaskEvent) => void;
132
+ /** Error handler for callback exceptions (P1-3) */
133
+ onError?: (error: Error, context: CallbackErrorContext) => void;
134
+ }
135
+ export interface TaskSubscription {
136
+ unsubscribe(): void;
137
+ }
138
+ export declare class TaskClient {
139
+ private readonly config;
140
+ private _pubnub?;
141
+ private readonly _createPubNub?;
142
+ private readonly _createSessionPubNub?;
143
+ private readonly defaultOwnerId?;
144
+ private readonly _ownsPubNub;
145
+ private _subscribeKey;
146
+ private _publishKey;
147
+ private _consumerAuth?;
148
+ constructor(options: TaskClientOptions);
149
+ /**
150
+ * Create a TaskClient from environment variables or CDM config.
151
+ *
152
+ * Resolution order for each config value:
153
+ * - Explicit options (if provided)
154
+ * - Environment variables (BLOCKS_*)
155
+ * - CDM config (fetched from cdmUrl or BLOCKS_CDM_URL)
156
+ *
157
+ * `listing` is required -- it determines which CDM keyset to use:
158
+ * - 'playground' selects the CDM playground keyset
159
+ * - 'private' or 'public' selects the CDM network keyset
160
+ */
161
+ static create(options?: {
162
+ listing?: 'playground' | 'private' | 'public';
163
+ cdmUrl?: string;
164
+ subscribeKey?: string;
165
+ publishKey?: string;
166
+ baseUrl?: string;
167
+ apiKey?: string;
168
+ tokenEndpoint?: TokenEndpointConfig;
169
+ tokenProvider?: () => Promise<TokenResult>;
170
+ onAuthError?: (error: Error) => void;
171
+ }): Promise<TaskClient>;
172
+ /** Returns the authenticated user ID from ConsumerAuth, or null. */
173
+ getUserId(): string | null;
174
+ /**
175
+ * Look up an agent's card by name from the registry.
176
+ * Returns null if the agent is not found or has no card.
177
+ */
178
+ getAgentCard(agentName: string): Promise<AgentCard | null>;
179
+ /**
180
+ * Update keyset keys after an environment switch.
181
+ * Updates both the RPC config and the keys used for PubNub client creation.
182
+ */
183
+ updateKeys(subscribeKey: string, publishKey?: string): void;
184
+ /**
185
+ * Lazily resolve the PubNub instance for subscribe operations.
186
+ * If a direct `pubnub` was provided, use it. Otherwise, call the factory.
187
+ */
188
+ private getPubNub;
189
+ /**
190
+ * Clean up the PubNub instance if it was created by this TaskClient (via createPubNub factory).
191
+ * Externally-provided instances are left untouched.
192
+ * Stops ConsumerAuth refresh timer if active. Token remains readable
193
+ * so active sessions can still call cancel/terminate using the stale token.
194
+ */
195
+ destroy(): void;
196
+ [Symbol.dispose](): void;
197
+ /**
198
+ * Create a per-session PubNub subscribe client with the given T4 token.
199
+ * Each TaskSession gets its own client to prevent token stomping.
200
+ *
201
+ * Uses the dedicated `createSessionPubNub` factory when available.
202
+ * Falls back to an internal fresh-client construction otherwise.
203
+ * Never uses the shared `createPubNub` factory -- that is reserved
204
+ * for low-level `subscribeToTask()` operations.
205
+ */
206
+ private createPerSessionPubNub;
207
+ private fetchConsumerReadToken;
208
+ /**
209
+ * Send a message (task) to an agent via JSON-RPC "SendMessage".
210
+ * Returns a TaskSession that eagerly subscribes to the task channel.
211
+ *
212
+ * When request parts include `file` data:
213
+ * - Small files (<= 16 KB): inlined as base64 artifactRef on the part
214
+ * - Large files (> 16 KB): uploaded via pre-signed URL flow, then
215
+ * SendMessage includes the uploadSessionId to bind files to the task
216
+ */
217
+ sendMessage(params: SendMessageParams): Promise<TaskSession>;
218
+ /**
219
+ * Connect to an existing task. Returns a TaskSession pre-populated
220
+ * with stream refs, artifact refs, and task state from history.
221
+ *
222
+ * For active tasks: the session subscribes to the task channel and
223
+ * live events flow through callbacks from that point forward.
224
+ *
225
+ * For terminal tasks: the session is pre-populated but does not
226
+ * subscribe. The consumer reads listArtifacts(), listStreams(), and
227
+ * session.state, then calls close().
228
+ *
229
+ * Uses the task-read-token endpoint with role:'consumer' to acquire
230
+ * a fresh T4 read token. The caller does not need to persist or
231
+ * supply readToken, orgId, ownerId, or agentName.
232
+ */
233
+ connect(params: {
234
+ taskId: string;
235
+ autoDrain?: boolean;
236
+ /**
237
+ * Duration in milliseconds the session waits for already-open streams
238
+ * to finish draining naturally after a terminal event. Defaults to
239
+ * 30000 ms (30 seconds). Ignored when `autoDrain` is false.
240
+ *
241
+ * Only applies to streams that were opened while the task was still
242
+ * active. Unopened streams on a terminal session throw
243
+ * `StreamUnavailableError` per the merged t7c baseline.
244
+ */
245
+ drainWindowMs?: number;
246
+ }): Promise<TaskSession>;
247
+ getTask(taskId: string): Promise<TaskInfo>;
248
+ listTasks(params?: ListTasksParams): Promise<ListTasksResult>;
249
+ cancelTask(taskId: string): Promise<void>;
250
+ pauseTask(taskId: string): Promise<void>;
251
+ resumeTask(taskId: string): Promise<void>;
252
+ retryTask(taskId: string): Promise<void>;
253
+ terminateTask(taskId: string): Promise<void>;
254
+ /**
255
+ * Subscribe to real-time task events via PubNub.
256
+ * Requires pubnub instance in TaskClientOptions.
257
+ */
258
+ subscribeToTask(taskId: string, orgId: string, callbacks: TaskEventCallbacks): TaskSubscription;
259
+ }
@@ -0,0 +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)}