@blocks-network/sdk 0.1.49 → 0.1.51
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 +26 -24
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/runtime/agent-auth.d.ts +6 -1
- package/dist/runtime/agent-instance.d.ts +1 -1
- package/dist/runtime/agent-instance.js +1 -1
- package/dist/runtime/agent-registry.d.ts +18 -4
- 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/file-upload.js +1 -1
- package/dist/runtime/rpc-client.d.ts +30 -0
- package/dist/runtime/rpc-client.js +1 -1
- package/dist/runtime/task-client.d.ts +19 -4
- package/dist/runtime/task-client.js +1 -1
- package/dist/runtime/task-session.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ import type { TaskContext } from '@blocks-network/sdk';
|
|
|
17
17
|
|
|
18
18
|
export default async function handler(task: any, ctx: TaskContext) {
|
|
19
19
|
const input = task.requestParts?.[0]?.text ?? '';
|
|
20
|
-
ctx.reportStatus(
|
|
20
|
+
ctx.reportStatus(`Processing: ${input}`);
|
|
21
21
|
return { artifacts: [{ data: `Echo: ${input}`, mimeType: 'text/plain' }] };
|
|
22
22
|
}
|
|
23
23
|
```
|
|
@@ -26,13 +26,14 @@ Create an `agent-card.json`:
|
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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"] },
|
|
36
37
|
"skills": [{ "id": "main", "name": "Main Skill" }],
|
|
37
38
|
"runtime": {
|
|
38
39
|
"handler": "./handler.ts"
|
|
@@ -68,13 +69,14 @@ The agent card follows the A2A specification with a `runtime` extension:
|
|
|
68
69
|
|
|
69
70
|
```json
|
|
70
71
|
{
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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"] },
|
|
78
80
|
"skills": [{ "id": "main", "name": "Main Skill" }],
|
|
79
81
|
"runtime": {
|
|
80
82
|
"handler": "./handler.ts",
|
|
@@ -134,10 +136,10 @@ Static async factory that creates a configured `TaskClient` from
|
|
|
134
136
|
environment variables or CDM config.
|
|
135
137
|
|
|
136
138
|
```typescript
|
|
137
|
-
const client = await TaskClient.create({
|
|
139
|
+
const client = await TaskClient.create({ billingMode: 'free' });
|
|
138
140
|
```
|
|
139
141
|
|
|
140
|
-
- `
|
|
142
|
+
- `billingMode` (required): `'free'` or `'paid'`. Selects the keyset — `'free'` → playground, `'paid'` → network. Must match the target agent's persisted `billingMode`, which is set explicitly by the agent owner at register/update time (the backend validates pricing against `billingMode` — `'free'` rejects positive prices, `'paid'` requires a positive `pricePerTask` or `pricePerMinute` — but does NOT derive `billingMode` from pricing). The backend rejects mismatches between the caller-supplied `billingMode` and the agent row with `BillingModeMismatch`.
|
|
141
143
|
- Resolution: explicit options > `BLOCKS_*` env vars > CDM config
|
|
142
144
|
- Auth: one of the token provider modes below
|
|
143
145
|
|
|
@@ -150,7 +152,7 @@ token acquisition and refresh.
|
|
|
150
152
|
|
|
151
153
|
```typescript
|
|
152
154
|
const client = await TaskClient.create({
|
|
153
|
-
|
|
155
|
+
billingMode: 'free',
|
|
154
156
|
apiKey: process.env.BLOCKS_API_KEY,
|
|
155
157
|
onAuthError: (err) => console.error('Auth failed:', err.message),
|
|
156
158
|
});
|
|
@@ -168,7 +170,7 @@ Simplest form — a bare URL string. The SDK sends `POST <url>` with
|
|
|
168
170
|
|
|
169
171
|
```typescript
|
|
170
172
|
const client = await TaskClient.create({
|
|
171
|
-
|
|
173
|
+
billingMode: 'free',
|
|
172
174
|
tokenEndpoint: '/api/blocks-token',
|
|
173
175
|
});
|
|
174
176
|
```
|
|
@@ -188,7 +190,7 @@ const tokenEndpoint: TokenEndpointConfig = {
|
|
|
188
190
|
};
|
|
189
191
|
|
|
190
192
|
const client = await TaskClient.create({
|
|
191
|
-
|
|
193
|
+
billingMode: 'free',
|
|
192
194
|
tokenEndpoint,
|
|
193
195
|
});
|
|
194
196
|
```
|
|
@@ -232,7 +234,7 @@ uniformly by this SDK.
|
|
|
232
234
|
|
|
233
235
|
```typescript
|
|
234
236
|
const client = await TaskClient.create({
|
|
235
|
-
|
|
237
|
+
billingMode: 'free',
|
|
236
238
|
tokenProvider: async () => {
|
|
237
239
|
const resp = await fetch('/api/my-auth');
|
|
238
240
|
const { token, expiresIn, userId } = await resp.json();
|
|
@@ -317,7 +319,7 @@ terminal `connect()`).
|
|
|
317
319
|
```typescript
|
|
318
320
|
import { TaskClient, textPart } from '@blocks-network/sdk';
|
|
319
321
|
|
|
320
|
-
const client = await TaskClient.create({
|
|
322
|
+
const client = await TaskClient.create({ billingMode: 'free', apiKey });
|
|
321
323
|
const session = await client.sendMessage({
|
|
322
324
|
agentName: 'acme_echo',
|
|
323
325
|
requestParts: [textPart('Hello')],
|
|
@@ -553,7 +555,7 @@ work on terminal-session refs without raising.
|
|
|
553
555
|
```typescript
|
|
554
556
|
// TypeScript 5.2+ using keyword
|
|
555
557
|
{
|
|
556
|
-
using client = await TaskClient.create({
|
|
558
|
+
using client = await TaskClient.create({ billingMode: 'free', apiKey });
|
|
557
559
|
// client.destroy() called automatically at scope exit
|
|
558
560
|
}
|
|
559
561
|
|
|
@@ -620,7 +622,7 @@ In browsers, use a customer-owned proxy endpoint or custom provider —
|
|
|
620
622
|
the SDK does not read from `process.env` in browser environments:
|
|
621
623
|
|
|
622
624
|
const client = await TaskClient.create({
|
|
623
|
-
|
|
625
|
+
billingMode: 'free',
|
|
624
626
|
tokenEndpoint: {
|
|
625
627
|
url: '/api/blocks-token',
|
|
626
628
|
credentials: 'include', // send the app's session cookie
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const BLOCKS_MAX_UPLOAD_BYTES: number;
|
|
|
12
12
|
export { loadBlocksConfig, type BlocksConfig } from './config-loader.js';
|
|
13
13
|
export { TaskClient } from './runtime/task-client.js';
|
|
14
14
|
export type { TaskClientOptions, SendMessageParams, SendMessageRequestPart, TaskInfo, ListTasksParams, ListTasksResult, TaskEvent as TaskClientEvent, TaskEventCallbacks, TaskSubscription, } from './runtime/task-client.js';
|
|
15
|
+
export { RpcError, BillingModeMismatchError } from './runtime/rpc-client.js';
|
|
15
16
|
export { TaskSession } from './runtime/task-session.js';
|
|
16
17
|
export type { TaskEvent, ProgressEvent, ArtifactEvent, TerminalEvent, Unsubscribe, CallbackErrorContext, } from './runtime/task-session.js';
|
|
17
18
|
export { textPart, filePart, filePartFromPath } from './runtime/part-helpers.js';
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{startAgentInstance}from"./runtime/agent-instance.js";export{createPubNubClient}from"./runtime/pubnub-client.js";export{createChannelManager,ChannelManager,streamChannel}from"./runtime/channel-manager.js";export{buildArtifactRef,shouldInlineArtifact,decodeInlineArtifact,downloadArtifact}from"./runtime/artifacts.js";export{connectAgent,fetchAgentRegistry,getAgent,removeAgent,fetchAgentsBySkill,fetchAgentsByListing}from"./runtime/agent-registry.js";export{DEFAULTS}from"./defaults.js";import{DEFAULTS as e}from"./defaults.js";export const BLOCKS_MAX_UPLOAD_BYTES=e.maxUploadBytes;export{loadBlocksConfig}from"./config-loader.js";export{TaskClient}from"./runtime/task-client.js";export{TaskSession}from"./runtime/task-session.js";export{textPart,filePart,filePartFromPath}from"./runtime/part-helpers.js";export{StreamRef,StreamUnavailableError}from"./runtime/stream-ref.js";export{AgentAuth,AgentAuthFatalError}from"./runtime/agent-auth.js";export{ConsumerAuth}from"./runtime/consumer-auth.js";export{StreamRegistry}from"./runtime/stream-registry.js";export{CredentialCache}from"./runtime/credential-cache.js";export{fetchCdmConfig,DEFAULT_CDM_URL}from"./runtime/cdm-config.js";
|
|
1
|
+
export{startAgentInstance}from"./runtime/agent-instance.js";export{createPubNubClient}from"./runtime/pubnub-client.js";export{createChannelManager,ChannelManager,streamChannel}from"./runtime/channel-manager.js";export{buildArtifactRef,shouldInlineArtifact,decodeInlineArtifact,downloadArtifact}from"./runtime/artifacts.js";export{connectAgent,fetchAgentRegistry,getAgent,removeAgent,fetchAgentsBySkill,fetchAgentsByListing}from"./runtime/agent-registry.js";export{DEFAULTS}from"./defaults.js";import{DEFAULTS as e}from"./defaults.js";export const BLOCKS_MAX_UPLOAD_BYTES=e.maxUploadBytes;export{loadBlocksConfig}from"./config-loader.js";export{TaskClient}from"./runtime/task-client.js";export{RpcError,BillingModeMismatchError}from"./runtime/rpc-client.js";export{TaskSession}from"./runtime/task-session.js";export{textPart,filePart,filePartFromPath}from"./runtime/part-helpers.js";export{StreamRef,StreamUnavailableError}from"./runtime/stream-ref.js";export{AgentAuth,AgentAuthFatalError}from"./runtime/agent-auth.js";export{ConsumerAuth}from"./runtime/consumer-auth.js";export{StreamRegistry}from"./runtime/stream-registry.js";export{CredentialCache}from"./runtime/credential-cache.js";export{fetchCdmConfig,DEFAULT_CDM_URL}from"./runtime/cdm-config.js";
|
|
@@ -13,11 +13,16 @@
|
|
|
13
13
|
/**
|
|
14
14
|
* Connect payload sent to POST /api/v1/auth/agent/connect.
|
|
15
15
|
* Contains instance-specific fields needed for runtime credential issuance.
|
|
16
|
+
*
|
|
17
|
+
* `billingMode` is required: the backend validates it against the persisted
|
|
18
|
+
* agent row and uses it to pick the PAM keyset. The SDK populates this
|
|
19
|
+
* value from the registry GET at boot — providers do NOT override it.
|
|
16
20
|
*/
|
|
17
21
|
export interface RegistrationPayload {
|
|
18
22
|
agentName: string;
|
|
19
23
|
instanceId: string;
|
|
20
|
-
|
|
24
|
+
billingMode: 'free' | 'paid';
|
|
25
|
+
listing?: 'private' | 'public';
|
|
21
26
|
expectedInstances?: number;
|
|
22
27
|
concurrency?: number;
|
|
23
28
|
maxPendingBacklog?: number;
|
|
@@ -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 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};
|
|
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 g,createExternalStreamObject as h,runOnActivate as y}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 E,isProtocolVersionSupported as $}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 A=(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 x=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,R="playground",M=R;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 N=m("BLOCKS_API_KEY");if(!N)throw new Error("BLOCKS_API_KEY is required. Run 'blocks publish' to set up credentials.");let P;const L=t.baseUrl??_?.api.baseUrl;let j,U;if(N&&L&&(P=new v(N,L),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),_){const e=t.baseUrl??_.api.baseUrl,r=await d(a,{baseUrl:e,apiKey:N});if(!r)throw new Error(`[AgentInstance] Agent "${a}" not found in registry. Register the agent (e.g. via \`blocks publish\`) before starting an instance.`);if(!r.billingMode)throw new Error(`[AgentInstance] Agent "${a}" registry entry is missing billingMode. Re-register the agent so the registry persists an explicit billingMode.`);j=r.billingMode,U=r.listing,R="paid"===j?"network":"playground",console.log(`[AgentInstance] Registry billingMode: ${j} — using ${R} environment`),M=R}const q=j??"free";let D,O=!1;if(t.pubnub)D=t.pubnub,O=!1,l&&D.setToken(l);else{const e=f[M];D=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20}),O=!0,l&&D.setToken(l)}let V=f[M].subscribeKey,B=f[M].publishKey;const F=new w({billingMode:q,subscribeKey:V,authProvider:l?new p(l):void 0,baseUrl:t.baseUrl??_?.api.baseUrl,agentAuth:P,createPubNub:()=>{const e=s({publishKey:B,subscribeKey:V,userId:`${c}-taskclient`});return l&&e.setToken(l),e}}),G=t.concurrency??u.concurrency,z=t.expectedInstances??u.expectedInstances,H=t.maxPendingBacklog,Y=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),W=Date.now();let Z=0;const J=new k,Q=new I,X=new Map,ee=new Map,te=new Map,ae=new Map,re=new Map,se=new Set,ne=new Set,oe=new Map,ie=new Set,de=new Map;let ce,le;t.logChannel;const ue=async()=>{if("function"==typeof D.setState&&le)try{await D.setState({channels:[le],state:{instanceId:c,activeTasks:Z,activeStreams:J.activeStreamCount,concurrency:G,startedAt:W,preferredProtocolVersion:S,protocolVersions:[...E]}})}catch{}},me=(e,t)=>{const a=X.get(e);a&&(a.delete(t),0===a.size&&X.delete(e))},fe=async e=>{(e=>{for(const[t,a]of X)a.has(e)&&(a.delete(e),0===a.size&&X.delete(t))})(e);const t=J.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},pe=async(e,t)=>{me(e,t);const a=J.get(e);if(0===J.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{}}ue().catch(()=>{})},ke=async(e,t)=>{const a=J.forceRemove(e);if((e=>{X.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=Q.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{}Q.remove(e)}}await ue()}},Ie=async(e,t)=>{const a=Q.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 fe(e),Q.remove(e),await ue()},ge=async(s,n,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",k=computeStreamDurationMinutes(s.duration,m,Y);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{se.add(s.taskId),c.abort()},e);oe.set(s.taskId,t)}}await K(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),de.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=>{ae.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 se.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:F,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:P}:void 0;await x(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=(de.get(s.taskId)??0)+1;de.set(s.taskId,e),S=`${s.taskId}-${e}`}const E=o.streamChannel(S);if("shared"===T){const e=X.get(S)?.get(s.taskId);if(e)return e}const{entry:$,isNew:K,isNewForTask:A}=J.acquire(S,s.taskId,{direction:p,format:I,external:w,affinity:T});Q.addStream(s.taskId,S);const x=e=>{if("shared"!==T)return;let t=X.get(S);t||(t=new Map,X.set(S,t)),t.set(s.taskId,e)};if(!A){$.setupPromise&&await $.setupPromise;const e=X.get(S)?.get(s.taskId);if(e)return e;if($.streamClient){const e=g(S,$.streamClient,s.taskId,{releaseStream:pe});return x(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($.setupPromise&&await $.setupPromise,"shared"===T&&await(async()=>{await C(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:E,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:"activate",metadata:b,declaredStream:c})})(),$.streamClient){const e=g(S,$.streamClient,s.taskId,{releaseStream:pe});return x(e),e}if(w)return h(S,E,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${S}" exists but has no client`)}catch(e){me(S,s.taskId);try{await J.release(S,s.taskId)}catch{}throw e}let _,R;$.setupPromise=new Promise((e,t)=>{_=e,R=t}),$.setupPromise.catch(()=>{});const M=w?"token_request":"embedded";let N;try{N=await C(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:E,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:M,metadata:b,declaredStream:c})}catch(e){R(e),me(S,s.taskId);try{await J.release(S,s.taskId)}catch{}throw e}if(w){_(),$.setupPromise=null;const e=N.token??"";return h(S,E,e,async e=>{await C(n,{taskId:s.taskId,orgId:d,agentName:u,streamId:S,channel:E,direction:p,format:I,taskKind:f,durationMinutes:k,affinity:T,phase:"activate",metadata:e?.metadata??b,declaredStream:c})})}const P=N.token;if(!P){const e=new Error("No T7a token received from stream setup handshake");R(e),me(S,s.taskId);try{await J.release(S,s.taskId)}catch{}throw e}const L=new e({subscribeKey:V,publishKey:B,token:P,agentName:u,streamId:S,channel:E,format:I,direction:p,affinity:T,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});$.streamClient=L;const j=g(S,L,s.taskId,{releaseStream:pe});if(x(j),_(),$.setupPromise=null,"outbound"===p||"bidirectional"===p){const e=a?.subscribeGraceMs??1e3;e>0&&await new Promise(t=>setTimeout(t,e))}return v&&($.activated=!0,$.activatePromise=y(S,j,v,ke)),j}};let S;try{S=t.handler?await t.handler(s,T):void 0}finally{de.delete(s.taskId),b&&(clearTimeout(b),b=null),v()}const E=L?{baseUrl:L,authProvider:l?new p(l):void 0,agentAuth:P}: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 x(n,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,E)}const $=c.signal.aborted;if(m){const e=se.has(s.taskId),t=ne.has(s.taskId);if($||e||t){await fe(s.taskId);const a=$&&!e?"canceled":"completed";await K(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),Q.remove(s.taskId),A("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else A("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=ne.has(s.taskId);await fe(s.taskId);const t=$?"canceled":"completed";await K(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),Q.remove(s.taskId),A("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},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{D.destroy()}catch{}}}})(),ye={status:e=>{const t=e,a=String(t.category??""),r=String(t.operation??"");"PNConnectedCategory"===a?console.log(`[AgentInstance] PubNub connected to ${le}`):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!==M&&t in f&&((e,t)=>{if(A("info",`SwitchEnvironment requested: ${M} -> ${e}`,{pamToken:t?"present":"absent"}),!t)return void A("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=D,r=O;try{a.removeListener(ye),le&&a.unsubscribe({channels:[le]})}catch{}const n=f[e];if(D=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,presenceTimeout:20}),O=!0,ce=void 0,D.setToken(t),0!==z&&"function"==typeof D.setFilterExpression&&D.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),D.addListener(ye),le&&D.subscribe({channels:[le]}),r)try{a.destroy()}catch{}M=e,V=n.subscribeKey,B=n.publishKey,F.updateKeys(n.subscribeKey,n.publishKey),A("info",`Switched to ${e} environment`),ue().catch(()=>{})})(t,e.pamToken))}(async(e,r)=>{if("StartTask"===e.type){const n=M,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(ie.has(e.taskId))return void A("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(r?.broadcast);if(e.protocolVersion&&!$(e.protocolVersion))return d?void A("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await K(D,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void A("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 A("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${r})`,{taskId:e.taskId,instanceId:c}),void await K(D,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=Z+J.activeStreamCount;if(G>0&&l>=G)return d?void A("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${G})`,{taskId:e.taskId,instanceId:c}):(await K(D,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void A("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:Z}));ee.set(e.taskId,o),te.set(e.taskId,i),ie.add(e.taskId),Z++,e.controlToken&&(ce=e.controlToken);const u=new AbortController;re.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),Q.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:n}),await ue(),t.onStartTask?await t.onStartTask(e,m):await ge(e,m,o,i,u)}catch(r){const s=r?.message??"Agent instance error",n=m??D;try{await fe(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),A("error",s,{taskId:e.taskId}),Q.remove(e.taskId)}finally{const t=oe.get(e.taskId);if(t&&(clearTimeout(t),oe.delete(e.taskId)),ie.delete(e.taskId),ee.delete(e.taskId),te.delete(e.taskId),ae.delete(e.taskId),re.delete(e.taskId),se.delete(e.taskId),ne.delete(e.taskId),m)try{m.destroy()}catch{}Z--,0===Z&&ce&&D.setToken(ce),await ue()}}else if("CancelTask"===e.type){const t=re.get(e.taskId);t?(t.abort(),A("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):Q.get(e.taskId)&&await Ie(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)re.has(e.taskId)?(se.add(e.taskId),re.get(e.taskId)?.abort(),A("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):Q.get(e.taskId)&&await Ie(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)re.has(e.taskId)?(ne.add(e.taskId),re.get(e.taskId)?.abort()):Q.get(e.taskId)&&await Ie(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(ie.has(e.taskId)){const t=te.get(e.taskId)??"anonymous";await K(D,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(ie.has(e.taskId)){const t=te.get(e.taskId)??"anonymous";await K(D,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&A("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)})}};D.addListener(ye),i(a,{instanceId:c,billingMode:q,description:t.description,scaling:{expectedInstances:z,concurrency:G,maxPendingBacklog:H,maxRunningTimeSec:Y},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:U??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??_?.api.baseUrl,agentAuth:P}).then(e=>{if(console.log(`[AgentInstance] Registered agent: ${a} (instance: ${c})`),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");le=e.controlChannel,e.pamToken&&D.setToken(e.pamToken),0!==z&&"function"==typeof D.setFilterExpression&&D.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),D.subscribe({channels:[le]}),ue().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)}),A("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{F.destroy(),D.removeListener(ye),le&&D.unsubscribe({channels:[le]}),O&&D.destroy();for(const e of J.streamIds()){const t=J.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}J.clear(),X.clear(),Q.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return le},get pubnub(){return D},get subscribeKey(){return f[M].subscribeKey},taskClient:F,publishTerminal:Ie,failStream:ke,cdmConfig:_}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
|
|
@@ -149,7 +149,9 @@ export interface AgentEntry {
|
|
|
149
149
|
/** Optional short summary for list views */
|
|
150
150
|
cardSummary?: string;
|
|
151
151
|
/** Listing visibility */
|
|
152
|
-
listing: '
|
|
152
|
+
listing: 'private' | 'public';
|
|
153
|
+
/** Server-derived billing tier. 'paid' when any pricing field > 0, else 'free'. */
|
|
154
|
+
billingMode?: 'free' | 'paid';
|
|
153
155
|
/** Creation timestamp (ISO string) */
|
|
154
156
|
createdAt?: string;
|
|
155
157
|
/** Last update timestamp (ISO string) */
|
|
@@ -161,6 +163,12 @@ export interface AgentEntry {
|
|
|
161
163
|
export interface ConnectAgentOptions {
|
|
162
164
|
/** @internal Agent instance identifier — auto-generated by startAgentInstance, not caller-supplied. */
|
|
163
165
|
instanceId: string;
|
|
166
|
+
/**
|
|
167
|
+
* @internal Billing mode — resolved by `startAgentInstance` from the registry
|
|
168
|
+
* GET at boot. NOT a caller-supplied override. To change billing mode,
|
|
169
|
+
* update the agent in the registry and restart the process.
|
|
170
|
+
*/
|
|
171
|
+
billingMode: 'free' | 'paid';
|
|
164
172
|
/** Human-readable description */
|
|
165
173
|
description?: string;
|
|
166
174
|
/** Skill objects advertised by this agent */
|
|
@@ -173,8 +181,8 @@ export interface ConnectAgentOptions {
|
|
|
173
181
|
cardRef?: string;
|
|
174
182
|
/** Optional short summary for list views */
|
|
175
183
|
cardSummary?: string;
|
|
176
|
-
/** Listing visibility
|
|
177
|
-
listing?: '
|
|
184
|
+
/** Listing visibility */
|
|
185
|
+
listing?: 'private' | 'public';
|
|
178
186
|
/** Actor identifier for audit log */
|
|
179
187
|
actor?: string;
|
|
180
188
|
/** Request ID for audit log correlation */
|
|
@@ -234,9 +242,15 @@ export declare function connectAgent(agentName: string, options: ConnectAgentOpt
|
|
|
234
242
|
}>;
|
|
235
243
|
/**
|
|
236
244
|
* Get a single agent by name.
|
|
245
|
+
*
|
|
246
|
+
* Pass `apiKey` to authenticate the lookup. Without it, the registry endpoint
|
|
247
|
+
* returns 404 for private agents (existence-privacy) and the SDK cannot
|
|
248
|
+
* resolve its own published record — leaving keyset routing stranded on the
|
|
249
|
+
* playground default while connect mints a PAM token for the network keyset.
|
|
237
250
|
*/
|
|
238
251
|
export declare function getAgent(agentName: string, options?: {
|
|
239
252
|
baseUrl?: string;
|
|
253
|
+
apiKey?: string;
|
|
240
254
|
}): Promise<AgentEntry | null>;
|
|
241
255
|
/**
|
|
242
256
|
* Fetch the agent registry (all agents).
|
|
@@ -257,7 +271,7 @@ export declare function fetchAgentsBySkill(skill: string, options?: {
|
|
|
257
271
|
/**
|
|
258
272
|
* Fetch agents filtered by listing type.
|
|
259
273
|
*/
|
|
260
|
-
export declare function fetchAgentsByListing(listing:
|
|
274
|
+
export declare function fetchAgentsByListing(listing: 'private' | 'public', options?: {
|
|
261
275
|
limit?: number;
|
|
262
276
|
cursor?: string;
|
|
263
277
|
baseUrl?: string;
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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 i}from"../env.js";import{CURRENT_PROTOCOL_VERSION as s,SUPPORTED_PROTOCOL_VERSIONS as c,SDK_VERSION as l,PROTOCOL_VERSION_HEADER as u}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 i=e*Math.pow(2,r)+100*Math.random();await new Promise(t=>setTimeout(t,i))}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(u)||e.set(u,s);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??"public",billingMode:t.billingMode,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=i("BLOCKS_CLI_VERSION"),a={agentName:t,instanceId:n.instanceId,billingMode:n.billingMode,listing:n.listing,expectedInstances:n.scaling?.expectedInstances,concurrency:n.scaling?.concurrency,maxPendingBacklog:n.scaling?.maxPendingBacklog,maxRunningTimeSec:n.scaling?.maxRunningTimeSec,deviceOs:m(),sdkLanguage:"Node",sdkVersion:l,protocolVersions:[...c],preferredProtocolVersion:s};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=n?.apiKey?{headers:{Authorization:`Bearer ${n.apiKey}`}}:void 0,r=await f(async()=>p(e,a));return r?y(r.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=i("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}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as o}from"./protocol-version.js";import{captureAffinity as a,injectAffinity as e}from"./write-affinity.js";async function r(r,n,i){const s=`${r.baseUrl.replace(/\/+$/,"")}${n}`,u=a=>{const n={"Content-Type":"application/json",[o]:t},s=r.authProvider?.getAuthHeader();return s&&(n.Authorization=s),a&&e(n),{method:"POST",headers:n,body:JSON.stringify(i)}};if(r.agentAuth)return r.agentAuth.authenticatedFetch(s,u(!1));const d=await fetch(s,u(!0));if(a(d.headers),401===d.status&&r.authProvider){if(await r.authProvider.onAuthFailure()){const t=await fetch(s,u(!0));return a(t.headers),t}}return d}export async function requestUpload(t,o){const a=await r(t,"/api/v1/files/request-upload",o);if(!a.ok){const t=await a.text().catch(()=>"");throw new Error(`request-upload failed: HTTP ${a.status} ${t}`)}return a.json()}export async function uploadToStorage(t,o,a,e,r){const n=new FormData;for(const t of o)n.append(t.key,t.value);const i=a instanceof Blob?a:new Blob([a],{type:r});n.append("file",i,e);const s=await fetch(t,{method:"POST",body:n});if(!s.ok){const t=await s.text().catch(()=>"");throw new Error(`S3 upload failed: HTTP ${s.status} ${t}`)}}export async function confirmUpload(t,o,a){const e=await r(t,"/api/v1/files/confirm-upload",{uploadId:o});if(!e.ok){const t=await e.text().catch(()=>"");throw new Error(`confirm-upload failed: HTTP ${e.status} ${t}`)}return e.json()}export async function uploadFile(t,o,a){const e=await requestUpload(t,o);if(await uploadToStorage(e.uploadUrl,e.formFields,a,o.fileName,o.mimeType),"provider-output"===o.role){return await confirmUpload(t,e.uploadId,!0)}const r=e;return await confirmUpload(t,e.uploadId),{uploadSessionId:r.uploadSessionId,uploadId:e.uploadId}}
|
|
1
|
+
import{CURRENT_PROTOCOL_VERSION as t,PROTOCOL_VERSION_HEADER as o}from"./protocol-version.js";import{captureAffinity as a,injectAffinity as e}from"./write-affinity.js";async function r(r,n,i){const s=`${r.baseUrl.replace(/\/+$/,"")}${n}`,u=a=>{const n={"Content-Type":"application/json",[o]:t},s=r.authProvider?.getAuthHeader();return s&&(n.Authorization=s),a&&e(n),{method:"POST",credentials:"omit",headers:n,body:JSON.stringify(i)}};if(r.agentAuth)return r.agentAuth.authenticatedFetch(s,u(!1));const d=await fetch(s,u(!0));if(a(d.headers),401===d.status&&r.authProvider){if(await r.authProvider.onAuthFailure()){const t=await fetch(s,u(!0));return a(t.headers),t}}return d}export async function requestUpload(t,o){const a=await r(t,"/api/v1/files/request-upload",o);if(!a.ok){const t=await a.text().catch(()=>"");throw new Error(`request-upload failed: HTTP ${a.status} ${t}`)}return a.json()}export async function uploadToStorage(t,o,a,e,r){const n=new FormData;for(const t of o)n.append(t.key,t.value);const i=a instanceof Blob?a:new Blob([a],{type:r});n.append("file",i,e);const s=await fetch(t,{method:"POST",body:n});if(!s.ok){const t=await s.text().catch(()=>"");throw new Error(`S3 upload failed: HTTP ${s.status} ${t}`)}}export async function confirmUpload(t,o,a){const e=await r(t,"/api/v1/files/confirm-upload",{uploadId:o});if(!e.ok){const t=await e.text().catch(()=>"");throw new Error(`confirm-upload failed: HTTP ${e.status} ${t}`)}return e.json()}export async function uploadFile(t,o,a){const e=await requestUpload(t,o);if(await uploadToStorage(e.uploadUrl,e.formFields,a,o.fileName,o.mimeType),"provider-output"===o.role){return await confirmUpload(t,e.uploadId,!0)}const r=e;return await confirmUpload(t,e.uploadId),{uploadSessionId:r.uploadSessionId,uploadId:e.uploadId}}
|
|
@@ -33,6 +33,36 @@ export declare class RpcError extends Error {
|
|
|
33
33
|
data?: unknown;
|
|
34
34
|
constructor(rpcMessage: string, code?: number, data?: unknown);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Typed error for backend `BillingModeMismatch` responses.
|
|
38
|
+
*
|
|
39
|
+
* Backend wire shape (per Phase 1 `bmc-data` IMPL_REPORT):
|
|
40
|
+
*
|
|
41
|
+
* {
|
|
42
|
+
* "jsonrpc": "2.0",
|
|
43
|
+
* "error": {
|
|
44
|
+
* "code": -32000,
|
|
45
|
+
* "message": "Billing mode mismatch: ...",
|
|
46
|
+
* "data": {
|
|
47
|
+
* "code": "BillingModeMismatch",
|
|
48
|
+
* "details": { "expected": "free" | "paid", "got": "free" | "paid" }
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* Extends `RpcError` for cross-language parity with the Python SDK's
|
|
54
|
+
* `BillingModeMismatchError` (which extends `RpcError` for the same reason).
|
|
55
|
+
*
|
|
56
|
+
* The SDK does NOT auto-retry or auto-correct on this error. The caller
|
|
57
|
+
* must update their `TaskClient` billing mode (typically by re-running
|
|
58
|
+
* `TaskClient.create({ billingMode: ... })`) to match the agent's
|
|
59
|
+
* persisted mode.
|
|
60
|
+
*/
|
|
61
|
+
export declare class BillingModeMismatchError extends RpcError {
|
|
62
|
+
expected: 'free' | 'paid';
|
|
63
|
+
got: 'free' | 'paid';
|
|
64
|
+
constructor(rpcMessage: string, expected: 'free' | 'paid', got: 'free' | 'paid', code?: number, data?: unknown);
|
|
65
|
+
}
|
|
36
66
|
/**
|
|
37
67
|
* Send a JSON-RPC 2.0 request to the Blocks backend RPC endpoint.
|
|
38
68
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CURRENT_PROTOCOL_VERSION as r,PROTOCOL_VERSION_HEADER as t}from"./protocol-version.js";import{captureAffinity as e,injectAffinity as o}from"./write-affinity.js";export async function withRetry(r,t=3,e=500){let o;for(let n=0;n<t;n++)try{return await r()}catch(r){o=r;const
|
|
1
|
+
import{CURRENT_PROTOCOL_VERSION as r,PROTOCOL_VERSION_HEADER as t}from"./protocol-version.js";import{captureAffinity as e,injectAffinity as o}from"./write-affinity.js";export async function withRetry(r,t=3,e=500){let o;for(let n=0;n<t;n++)try{return await r()}catch(r){o=r;const i=String(r);if(!(i.includes("ENOTFOUND")||i.includes("ETIMEDOUT")||i.includes("ECONNRESET")||i.includes("NetworkIssues"))||n===t-1)throw r;const s=e*Math.pow(2,n)+100*Math.random();await new Promise(r=>setTimeout(r,s))}throw o}export function rpcEndpoint(r,t){return t?`${t.replace(/\/+$/,"")}/api/v1/rpc`:`https://ps.pndsn.com/v1/blocks/sub-key/${r}/rpc`}export class RpcError extends Error{constructor(r,t,e){super(`[RPC] ${r}`),this.name="RpcError",this.rpcMessage=r,this.code=t,this.data=e}}export class BillingModeMismatchError extends RpcError{constructor(r,t,e,o,n){super(r,o,n),this.name="BillingModeMismatchError",this.expected=t,this.got=e}}export async function callRpc(n,i,s){const a=rpcEndpoint(n.subscribeKey,n.baseUrl),c=`rpc-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,d=e=>{const i={"Content-Type":"application/json",[t]:r},s=n.authProvider?.getAuthHeader();return s&&(i.Authorization=s),e&&o(i),i},u={jsonrpc:"2.0",id:c,method:i,params:s},p=async()=>{let r;if(n.agentAuth?r=await n.agentAuth.authenticatedFetch(a,{method:"POST",headers:d(!1),body:JSON.stringify(u)}):(r=await fetch(a,{method:"POST",headers:d(!0),body:JSON.stringify(u)}),e(r.headers)),!r.ok)throw new RpcError(`HTTP ${r.status}`,r.status);const t=await r.json();if(t.error){const r=t.error.data?.message??t.error.message??"Unknown RPC error",e=function(r,t,e){if(!e||"object"!=typeof e)return null;const o=e;if("BillingModeMismatch"!==o.code)return null;const n=o.details;if(!n||"object"!=typeof n)return null;const i=n;return"free"!==i.expected&&"paid"!==i.expected||"free"!==i.got&&"paid"!==i.got?null:new BillingModeMismatchError(r,i.expected,i.got,t,e)}(r,t.error.code,t.error.data);if(e)throw e;throw new RpcError(r,t.error.code,t.error.data)}return t.result};return withRetry(async()=>{try{return await p()}catch(r){if(r instanceof RpcError&&401===r.code&&n.authProvider&&!n.agentAuth){if(await n.authProvider.onAuthFailure())return p()}throw r}})}
|
|
@@ -15,6 +15,17 @@ import type { AuthProvider } from './auth-provider.js';
|
|
|
15
15
|
import { type TokenResult, type TokenEndpointConfig } from './consumer-auth.js';
|
|
16
16
|
import { type AgentCard } from './agent-registry.js';
|
|
17
17
|
export interface TaskClientOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Caller's billing mode. Required.
|
|
20
|
+
*
|
|
21
|
+
* Threaded into every SendMessage RPC params object so the backend can
|
|
22
|
+
* compare against the target agent's persisted `billingMode` and reject
|
|
23
|
+
* mismatches with `BillingModeMismatchError` carrying `expected`/`got`.
|
|
24
|
+
*
|
|
25
|
+
* `TaskClient.create({ billingMode })` populates this from its own
|
|
26
|
+
* required option. Direct constructor callers must supply it explicitly.
|
|
27
|
+
*/
|
|
28
|
+
billingMode: 'free' | 'paid';
|
|
18
29
|
subscribeKey: string;
|
|
19
30
|
publishKey?: string;
|
|
20
31
|
/** Internal/advanced hook for supplying an auth provider directly. */
|
|
@@ -145,6 +156,7 @@ export declare class TaskClient {
|
|
|
145
156
|
private _subscribeKey;
|
|
146
157
|
private _publishKey;
|
|
147
158
|
private _consumerAuth?;
|
|
159
|
+
private readonly _billingMode;
|
|
148
160
|
constructor(options: TaskClientOptions);
|
|
149
161
|
/**
|
|
150
162
|
* Create a TaskClient from environment variables or CDM config.
|
|
@@ -154,12 +166,15 @@ export declare class TaskClient {
|
|
|
154
166
|
* - Environment variables (BLOCKS_*)
|
|
155
167
|
* - CDM config (fetched from cdmUrl or BLOCKS_CDM_URL)
|
|
156
168
|
*
|
|
157
|
-
* `
|
|
158
|
-
* - '
|
|
159
|
-
* - '
|
|
169
|
+
* `billingMode` is required -- it determines which CDM keyset to use:
|
|
170
|
+
* - 'free' selects the CDM playground keyset
|
|
171
|
+
* - 'paid' selects the CDM network keyset
|
|
172
|
+
*
|
|
173
|
+
* Keyset names (`playground`, `network`) are unchanged; only the input
|
|
174
|
+
* surface moves from `listing` to `billingMode`.
|
|
160
175
|
*/
|
|
161
176
|
static create(options?: {
|
|
162
|
-
|
|
177
|
+
billingMode?: 'free' | 'paid';
|
|
163
178
|
cdmUrl?: string;
|
|
164
179
|
subscribeKey?: string;
|
|
165
180
|
publishKey?: string;
|
|
@@ -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,...h.contentType?{contentType:h.contentType}:{}})}}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 T="0"!==w?w:d,P=[];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):P.push({message:t,timetoken:s})}};b.addListener(C),b.subscribe({channels:[c],timetoken:T});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 P)t.timetoken>T&&e(t.message,t.timetoken)}return P.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 l}from"./write-affinity.js";import{getEnv as b}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 l={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(l,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){if(this._ownsPubNub=!1,"free"!==e.billingMode&&"paid"!==e.billingMode)throw new Error("TaskClient requires a billingMode option ('free' or 'paid')");this._billingMode=e.billingMode,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.billingMode;if("free"!==s&&"paid"!==s)throw new Error("TaskClient.create() requires a billingMode option ('free' or 'paid')");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??b("BLOCKS_CDM_URL"),a=await u(o),c="paid"===s?a.network:a.playground,d=n.subscribeKey??b("BLOCKS_SUBSCRIBE_KEY")??c.subscribeKey,l=n.publishKey??b("BLOCKS_PUBLISH_KEY")??c.publishKey,f=n.baseUrl??b("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:l||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({billingMode:s,subscribeKey:d,publishKey:l,baseUrl:f,createSessionPubNub:h,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:s,subscribeKey:d,publishKey:l,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),l(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 l;const b=[];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;b.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:l},c=await a(r,o,n.uploadBody);c.uploadSessionId&&(l=c.uploadSessionId);const{file:u,fileName:d,...f}=t;b.push({partId:f.partId,...f.contentType?{contentType:f.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;b.push(s)}const f={agentName:e.agentName,billingMode:this._billingMode,requestParts:b};l&&(f.uploadSessionId=l),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 T=this.createPerSessionPubNub(k),C={subscribeKey:this._subscribeKey,publishKey:this._publishKey},K=S??n(m.taskId,m.orgId??i);try{const t=await T.time(),n=String(t.timetoken),r=p(T);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=w(await I(r,K),m.taskId,e.agentName,C);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:T,ownsSubscribeClient:!0,sdkOptions:C,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,l=[];let b,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&&b?b(t,n):l.push({message:t,timetoken:n})}};T.addListener(h),T.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:T,ownsSubscribeClient:!0,sdkOptions:C,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=>{b=e}}});if(b){const e=b;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.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:T,ownsSubscribeClient:!0,sdkOptions:C,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(),l=String(o.timetoken),b=p(d);let f,h=new Map,m=[],g="0";if(b?.fetchMessages){const e=w(await I(b,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:l,T=[];let C,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&&C?C(t,n):T.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=>{C=e}}});if(C){const e=C;for(const t of T)t.timetoken>S&&e(t.message,t.timetoken)}return T.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;
|