@blocks-network/sdk 0.1.53 → 0.1.58

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/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Blocks Network Software Development Kit License Agreement
2
+ Copyright © 2026 Blocks Network Inc. All rights reserved.
3
+
4
+ Subject to the terms and conditions of the license, you are hereby granted
5
+ a non-exclusive, worldwide, royalty-free license to (a) copy and modify
6
+ the software in source code or binary form for use with the software services
7
+ and interfaces provided by Blocks Network, and (b) redistribute unmodified copies
8
+ of the software to third parties. The software may not be incorporated in
9
+ or used to provide any product or service competitive with the products
10
+ and services of Blocks Network.
11
+
12
+ The above copyright notice and this license shall be included
13
+ in or with all copies or substantial portions of the software.
14
+
15
+ This license does not grant you permission to use the trade names, trademarks,
16
+ service marks, or product names of Blocks Network, except as required for reasonable
17
+ and customary use in describing the origin of the software and reproducing
18
+ the content of this license.
19
+
20
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF
21
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
23
+ EVENT SHALL BLOCKS NETWORK OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE
24
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
25
+ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -61,7 +61,7 @@ npx blocks-run
61
61
  ```
62
62
 
63
63
  Both approaches load environment variables from `.env` in the current
64
- working directory. Run `blocks publish` first to populate `BLOCKS_API_KEY`.
64
+ working directory. Run `blocks login --write-env` first to populate `BLOCKS_API_KEY`.
65
65
 
66
66
  ## agent-card.json
67
67
 
@@ -96,8 +96,22 @@ subscribing PubNub clients (control client, per-task client, and per-stream
96
96
  client). Event Engine replaces the legacy subscribe manager with a
97
97
  deterministic state machine for subscribe, reconnect, and retry. The SDK
98
98
  does not set `autoNetworkDetection` or `restore` (both are browser-only
99
- settings). No explicit `retryConfiguration` is added; the PubNub JS SDK
100
- applies a default exponential retry policy when Event Engine is enabled.
99
+ settings).
100
+
101
+ **Control client** (long-lived, drives agent online presence) is
102
+ constructed with `subscribeRetryUnbounded: true`, which configures an
103
+ `ExponentialRetryPolicy` with `maximumRetry: 43_200` (~30 days at the
104
+ 60s cap). This prevents the Event Engine from parking in
105
+ `RECEIVE_FAILED`/`HEARTBEAT_FAILED` after the default 6-attempt budget
106
+ is exhausted, so the agent automatically resumes within seconds of a
107
+ network outage ending. Transport-level retry activity is forwarded to
108
+ the SDK log via the `onRetry` hook, surfacing as
109
+ `event: 'pubnub_transport_retry'` warn lines so the agent's log stays
110
+ active during outages instead of going silent.
111
+
112
+ **Per-task and per-stream clients** keep PubNub's default 6-attempt
113
+ retry budget. They are short-lived; a stuck task should fail cleanly
114
+ rather than loop forever.
101
115
 
102
116
  ## API
103
117
 
@@ -110,9 +124,9 @@ the primary runtime API.
110
124
 
111
125
  The SDK reads `BLOCKS_API_KEY` from the environment and uses it to
112
126
  authenticate with the backend. Set this in your `.env` file. The
113
- Go CLI's `blocks publish` command generates the API key and writes
114
- it to `.env` automatically. `blocks login` can also do this when
115
- invoked with `--write-env` (or by answering "y" to its prompt).
127
+ Go CLI's `blocks login` command writes the API key to `.env` when
128
+ invoked with `--write-env` (or by answering "y" to its interactive
129
+ prompt).
116
130
 
117
131
  PAM tokens for PubNub channel access are managed by the SDK at runtime
118
132
  (granted at registration, refreshed per-task on the control channel).
@@ -193,6 +193,25 @@ export interface ControlMessage {
193
193
  export declare const _LOG_LEVEL_ORDER: Record<string, number>;
194
194
  /** Resolve the effective LOG_LEVEL from env (default: "info"). */
195
195
  export declare function _resolveLogLevel(): number;
196
+ /**
197
+ * True when the diag entry is "parked" — past the staleness threshold
198
+ * AND not currently in the connected state. A long-silent
199
+ * PNConnectedCategory entry is healthy by definition: at LOG_LEVEL=info
200
+ * the SDK suppresses successful-heartbeat status events
201
+ * (announceSuccessfulHeartbeats=false), so lastStatusAt does not
202
+ * advance on a healthy idle client. Without the category gate, the
203
+ * 10s diag timer would log "STALE clients present" every tick after
204
+ * 60s of normal idleness — noisy false positive.
205
+ *
206
+ * Exported under the `_` convention for unit tests; not part of the
207
+ * public SDK surface.
208
+ */
209
+ export declare function _isDiagEntryStale(args: {
210
+ lastStatusAt: number | null;
211
+ lastCategory: string | null;
212
+ now: number;
213
+ thresholdMs: number;
214
+ }): boolean;
196
215
  /**
197
216
  * Resolve the effective `maxRunningTimeSec` for an agent instance from its
198
217
  * two possible sources: the constructor option (`opts.maxRunningTimeSec`)
@@ -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 g,runOnActivate as y}from"./stream-context.js";import{TaskClient as w}from"./task-client.js";import{ConsumerAuth as b}from"./consumer-auth.js";import{fetchCdmConfig as v}from"./cdm-config.js";import{AgentAuth as T,AgentAuthFatalError as S}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as E,SUPPORTED_PROTOCOL_VERSIONS as $,isProtocolVersionSupported as K}from"./protocol-version.js";const x=async(e,t,a,r,s,i=E)=>{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 C=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 x(e,r,s,n,{...u,artifactRef:t({data:o,mimeType:i,fileName:d})});else{if(!l)throw new Error("Artifact size exceeds the inline threshold but no baseUrl is configured. Set baseUrl in AgentInstanceOptions to enable the pre-signed URL upload flow.");await f(l,{role:"provider-output",taskId:r,fileName:d??`${r}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function _(e,t){const a=`setup.${t.orgId}.${t.taskId}`,r={type:"stream_setup",taskId:t.taskId,orgId:t.orgId,agentName:t.agentName,streamId:t.streamId,channel:t.channel,direction:t.direction,format:t.format,taskKind:t.taskKind,durationMinutes:t.durationMinutes,affinity:t.affinity,declaredStream:t.declaredStream??"_default",protocolVersion:E};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,R=null,M="playground",N=M;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else R=await v(t.cdmUrl),f={playground:R.playground,network:R.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 L;const j=t.baseUrl??R?.api.baseUrl;let U,q;if(P&&j&&(L=new T(P,j),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),R){const e=t.baseUrl??R.api.baseUrl,r=await d(a,{baseUrl:e,apiKey:P});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.`);U=r.billingMode,q=r.listing,M="paid"===U?"network":"playground",console.log(`[AgentInstance] Registry billingMode: ${U} — using ${M} environment`),N=M}const D=U??"free";let O,V=!1;if(t.pubnub)O=t.pubnub,V=!1,l&&O.setToken(l);else{const e=f[N];O=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20}),V=!0,l&&O.setToken(l)}let B=f[N].subscribeKey,F=f[N].publishKey;const G=j?new b({apiKey:P,baseUrl:j}):void 0,z=new w({billingMode:D,subscribeKey:B,publishKey:F,authProvider:G,baseUrl:j,createPubNub:()=>s({publishKey:F,subscribeKey:B,userId:`${c}-taskclient`})}),H=t.concurrency??u.concurrency,Y=t.expectedInstances??u.expectedInstances,W=t.maxPendingBacklog,Z=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),J=Date.now();let Q=0;const X=new k,ee=new I,te=new Map,ae=new Map,re=new Map,se=new Map,ne=new Map,oe=new Set,ie=new Set,de=new Map,ce=new Set,le=new Map;let ue,me;t.logChannel;const fe=async()=>{if("function"==typeof O.setState&&me)try{await O.setState({channels:[me],state:{instanceId:c,activeTasks:Q,activeStreams:X.activeStreamCount,concurrency:H,startedAt:J,preferredProtocolVersion:E,protocolVersions:[...$]}})}catch{}},pe=(e,t)=>{const a=te.get(e);a&&(a.delete(t),0===a.size&&te.delete(e))},ke=async e=>{(e=>{for(const[t,a]of te)a.has(e)&&(a.delete(e),0===a.size&&te.delete(t))})(e);const t=X.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},Ie=async(e,t)=>{pe(e,t);const a=X.get(e);if(0===X.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{}}fe().catch(()=>{})},he=async(e,t)=>{const a=X.forceRemove(e);if((e=>{te.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=ee.get(e);if(a){try{const r=f[a.environment??M]??f[M],n=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c});n.setToken(a.writeToken),await x(n,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),n.destroy()}catch{}ee.remove(e)}}await fe()}},ge=async(e,t)=>{const a=ee.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const r=f[a.environment??M]??f[M],n=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c});n.setToken(a.writeToken);try{await x(n,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{n.destroy()}await ke(e),ee.remove(e),await fe()},ye=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(()=>{oe.add(s.taskId),c.abort()},e);de.set(s.taskId,t)}}await x(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),le.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(),x(n,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},T={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{se.set(s.taskId,e);const t=Date.now();if(t-I>=1e3)w=null,I=t,x(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 oe.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:z,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=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:L}:void 0;await C(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=(le.get(s.taskId)??0)+1;le.set(s.taskId,e),S=`${s.taskId}-${e}`}const E=o.streamChannel(S);if("shared"===T){const e=te.get(S)?.get(s.taskId);if(e)return e}const{entry:$,isNew:K,isNewForTask:x}=X.acquire(S,s.taskId,{direction:p,format:I,external:w,affinity:T});ee.addStream(s.taskId,S);const A=e=>{if("shared"!==T)return;let t=te.get(S);t||(t=new Map,te.set(S,t)),t.set(s.taskId,e)};if(!x){$.setupPromise&&await $.setupPromise;const e=te.get(S)?.get(s.taskId);if(e)return e;if($.streamClient){const e=h(S,$.streamClient,s.taskId,{releaseStream:Ie});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($.setupPromise&&await $.setupPromise,"shared"===T&&await(async()=>{await _(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=h(S,$.streamClient,s.taskId,{releaseStream:Ie});return A(e),e}if(w)return g(S,E,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${S}" exists but has no client`)}catch(e){pe(S,s.taskId);try{await X.release(S,s.taskId)}catch{}throw e}let C,R;$.setupPromise=new Promise((e,t)=>{C=e,R=t}),$.setupPromise.catch(()=>{});const M=w?"token_request":"embedded";let N;try{N=await _(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),pe(S,s.taskId);try{await X.release(S,s.taskId)}catch{}throw e}if(w){C(),$.setupPromise=null;const e=N.token??"";return g(S,E,e,async e=>{await _(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),pe(S,s.taskId);try{await X.release(S,s.taskId)}catch{}throw e}const L=new e({subscribeKey:B,publishKey:F,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=h(S,L,s.taskId,{releaseStream:Ie});if(A(j),C(),$.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,he)),j}};let S;try{S=t.handler?await t.handler(s,T):void 0}finally{le.delete(s.taskId),b&&(clearTimeout(b),b=null),v()}const E=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:L}: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 C(n,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,E)}const $=c.signal.aborted;if(m){const e=oe.has(s.taskId),t=ie.has(s.taskId);if($||e||t){await ke(s.taskId);const a=$&&!e?"canceled":"completed";await x(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),ee.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=ie.has(s.taskId);await ke(s.taskId);const t=$?"canceled":"completed";await x(n,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),ee.remove(s.taskId),A("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},we=(()=>{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{O.destroy()}catch{}}}})(),be={status:e=>{const t=e,a=String(t.category??""),r=String(t.operation??"");"PNConnectedCategory"===a?console.log(`[AgentInstance] PubNub connected to ${me}`):t.error&&console.error(`[AgentInstance] PubNub status: ${a} (op=${r}, code=${t.statusCode})`),we(a)},message:e=>{const r=e.message;if(!r)return;if("SwitchEnvironment"===r.type){const e=r,t=e.environment;return void(t&&t!==N&&t in f&&((e,t)=>{if(A("info",`SwitchEnvironment requested: ${N} -> ${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=O,r=V;try{a.removeListener(be),me&&a.unsubscribe({channels:[me]})}catch{}const n=f[e];if(O=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,presenceTimeout:20}),V=!0,ue=void 0,O.setToken(t),0!==Y&&"function"==typeof O.setFilterExpression&&O.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),O.addListener(be),me&&O.subscribe({channels:[me]}),r)try{a.destroy()}catch{}N=e,B=n.subscribeKey,F=n.publishKey,z.updateKeys(n.subscribeKey,n.publishKey),A("info",`Switched to ${e} environment`),fe().catch(()=>{})})(t,e.pamToken))}(async(e,r)=>{if("StartTask"===e.type){const n=N,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(ce.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&&!K(e.protocolVersion))return d?void A("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await x(O,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 x(O,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=Q+X.activeStreamCount;if(H>0&&l>=H)return d?void A("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${H})`,{taskId:e.taskId,instanceId:c}):(await x(O,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:Q}));ae.set(e.taskId,o),re.set(e.taskId,i),ce.add(e.taskId),Q++,e.controlToken&&(ue=e.controlToken);const u=new AbortController;ne.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),ee.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:n}),await fe();const{writeToken:d,controlToken:l,...p}=e;t.onStartTask?await t.onStartTask(e,m):await ye(p,m,o,i,u)}catch(r){const s=r?.message??"Agent instance error",n=m??O;try{await ke(e.taskId)}catch{}await x(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}),ee.remove(e.taskId)}finally{const t=de.get(e.taskId);if(t&&(clearTimeout(t),de.delete(e.taskId)),ce.delete(e.taskId),ae.delete(e.taskId),re.delete(e.taskId),se.delete(e.taskId),ne.delete(e.taskId),oe.delete(e.taskId),ie.delete(e.taskId),m)try{m.destroy()}catch{}Q--,0===Q&&ue&&O.setToken(ue),await fe()}}else if("CancelTask"===e.type){const t=ne.get(e.taskId);t?(t.abort(),A("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):ee.get(e.taskId)&&await ge(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)ne.has(e.taskId)?(oe.add(e.taskId),ne.get(e.taskId)?.abort(),A("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):ee.get(e.taskId)&&await ge(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)ne.has(e.taskId)?(ie.add(e.taskId),ne.get(e.taskId)?.abort()):ee.get(e.taskId)&&await ge(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(ce.has(e.taskId)){const t=re.get(e.taskId)??"anonymous";await x(O,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(ce.has(e.taskId)){const t=re.get(e.taskId)??"anonymous";await x(O,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)})}};O.addListener(be),i(a,{instanceId:c,billingMode:D,description:t.description,scaling:{expectedInstances:Y,concurrency:H,maxPendingBacklog:W,maxRunningTimeSec:Z},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:q??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??R?.api.baseUrl,agentAuth:L}).then(e=>{if(console.log(`[AgentInstance] Registered agent: ${a} (instance: ${c})`),!e.controlChannel)throw new Error("Connect response missing controlChannel — server may be outdated");me=e.controlChannel,e.pamToken&&O.setToken(e.pamToken),0!==Y&&"function"==typeof O.setFilterExpression&&O.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),O.subscribe({channels:[me]}),fe().catch(()=>{})}).catch(e=>{const t=e instanceof Error?e.message:String(e);console.error(`[AgentInstance] Failed to register agent: ${a} — ${t}`),e instanceof S&&process.exit(1)}),A("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{G?.destroy(),z.destroy(),O.removeListener(be),me&&O.unsubscribe({channels:[me]}),V&&O.destroy();for(const e of X.streamIds()){const t=X.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}X.clear(),te.clear(),ee.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return me},get pubnub(){return O},get subscribeKey(){return f[N].subscribeKey},taskClient:z,publishTerminal:ge,failStream:he,cdmConfig:R}};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 n}from"./artifacts.js";import{createPubNubClient as s}from"./pubnub-client.js";import{createChannelManager as r}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 g}from"./stream-registry.js";import{CredentialCache as k}from"./credential-cache.js";import{createStreamObject as y,createExternalStreamObject as h,runOnActivate as b}from"./stream-context.js";import{TaskClient as I}from"./task-client.js";import{ConsumerAuth as w}from"./consumer-auth.js";import{fetchCdmConfig as v}from"./cdm-config.js";import{AgentAuth as S,AgentAuthFatalError as T}from"./agent-auth.js";import{CURRENT_PROTOCOL_VERSION as C,SUPPORTED_PROTOCOL_VERSIONS as E,isProtocolVersionSupported as A}from"./protocol-version.js";const $=async(e,t,a,n,s,i=C)=>{const d=r(n);try{await e.publish({channel:d.taskChannel(t,a),message:o({...s,protocolVersion:i}),storeInHistory:!0,sendByPost:!0,meta:o({agentName:n,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}export function _isDiagEntryStale(e){const{lastStatusAt:t,lastCategory:a,now:n,thresholdMs:s}=e;return null!==t&&("PNConnectedCategory"!==a&&n-t>s)}const _=(e,t,a)=>{const n=_resolveLogLevel();if((_LOG_LEVEL_ORDER[e]??_LOG_LEVEL_ORDER.info)>n)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 K=async(e,n,s,r,o,i,d,c,l)=>{const u={type:"artifact",taskId:n};if(c&&(u.outputId=c),a(o.length))await $(e,n,s,r,{...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:n,fileName:d??`${n}-artifact`,fileSize:o.length,mimeType:i,outputId:c},o)}};async function x(e,t){const a=`setup.${t.orgId}.${t.taskId}`,n={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:C};t.phase&&(n.phase=t.phase),t.metadata&&(n.metadata=t.metadata);try{throw await e.publish({channel:a,message:o(n),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=r(a),c=t.instanceId??`AG-${a}-${crypto.randomUUID()}`,l=t.token;let f,R=null,M="playground",L=M;if(t.pubnub){const e=t.pubnub._configuration?.keySet,a={publishKey:e?.publishKey??"",subscribeKey:e?.subscribeKey??""};f={playground:a,network:a}}else R=await v(t.cdmUrl),f={playground:R.playground,network:R.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 login --write-env' to set up credentials.");let P;const j=t.baseUrl??R?.api.baseUrl;let U,D;if(N&&j&&(P=new S(N,j),console.log("[AgentInstance] AgentAuth created — will authenticate via connect")),R){const e=t.baseUrl??R.api.baseUrl,n=await d(a,{baseUrl:e,apiKey:N});if(!n)throw new Error(`[AgentInstance] Agent "${a}" not found in registry. Register the agent (e.g. via \`blocks publish\`) before starting an instance.`);if(!n.billingMode)throw new Error(`[AgentInstance] Agent "${a}" registry entry is missing billingMode. Re-register the agent so the registry persists an explicit billingMode.`);U=n.billingMode,D=n.listing,M="paid"===U?"network":"playground",console.log(`[AgentInstance] Registry billingMode: ${U} — using ${M} environment`),L=M}const O=U??"free",q=[];let V=null;const B=(e,t)=>{const a={label:e,pn:t,listener:void 0,lastStatusAt:null,lastConnectedAt:null,lastMessageAt:null,lastCategory:null,lastOperation:null,lastStatusCode:null};a.listener=(e=>({status:t=>{const a=t,n=String(a.category??""),s=String(a.operation??""),r="number"==typeof a.statusCode?a.statusCode:null,o=Date.now(),i=e.lastCategory;e.lastStatusAt=o,e.lastCategory=n,e.lastOperation=s,e.lastStatusCode=r,"PNConnectedCategory"===n&&(e.lastConnectedAt=o),_("debug","pubnub status event",{event:"pubnub_status",client:e.label,category:n,operation:s,statusCode:r,error:Boolean(a.error),affectedChannels:Array.isArray(a.affectedChannels)?a.affectedChannels.length:void 0,instanceId:c}),n&&n!==i&&_("info",`pubnub status transition [${e.label}]: ${i??"(none)"} -> ${n}`,{event:"pubnub_status_transition",client:e.label,from:i,to:n,operation:s,statusCode:r,instanceId:c})},message:()=>{e.lastMessageAt=Date.now()}}))(a),q.push(a),t.addListener(a.listener)},F=e=>{const t=q.findIndex(t=>t.pn===e);if(t<0)return;const a=q[t];try{e.removeListener(a.listener)}catch{}q.splice(t,1)};let G,z=!1;if(t.pubnub)G=t.pubnub,z=!1,l&&G.setToken(l);else{const e=f[L];G=s({publishKey:e.publishKey,subscribeKey:e.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:_resolveLogLevel()>=_LOG_LEVEL_ORDER.debug,subscribeRetryUnbounded:!0,onRetry:e=>{_("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),z=!0,l&&G.setToken(l)}let H=f[L].subscribeKey,Y=f[L].publishKey;const W=j?new w({apiKey:N,baseUrl:j}):void 0,Z=new I({billingMode:O,subscribeKey:H,publishKey:Y,authProvider:W,baseUrl:j,createPubNub:()=>{const e=s({publishKey:Y,subscribeKey:H,userId:`${c}-taskclient`,subscribeRetryUnbounded:!1});return B("consumer-task",e),e}}),J=t.concurrency??u.concurrency,Q=t.expectedInstances??u.expectedInstances,X=t.maxPendingBacklog,ee=resolveMaxRunningTimeSec(t.maxRunningTimeSec,t.card?.runtime?.maxRunningTimeSec),te=Date.now();let ae=0;const ne=new g,se=new k,re=new Map,oe=new Map,ie=new Map,de=new Map,ce=new Map,le=new Set,ue=new Set,me=new Map,fe=new Set,pe=new Map;let ge,ke;t.logChannel;const ye=async()=>{if("function"==typeof G.setState&&ke)try{await G.setState({channels:[ke],state:{instanceId:c,activeTasks:ae,activeStreams:ne.activeStreamCount,concurrency:J,startedAt:te,preferredProtocolVersion:C,protocolVersions:[...E]}})}catch{}},he=(e,t)=>{const a=re.get(e);a&&(a.delete(t),0===a.size&&re.delete(e))},be=async e=>{(e=>{for(const[t,a]of re)a.has(e)&&(a.delete(e),0===a.size&&re.delete(t))})(e);const t=ne.releaseAllForTask(e);for(const e of t)if(e.streamClient)try{await e.streamClient.end()}catch{}},Ie=async(e,t)=>{he(e,t);const a=ne.get(e);if(0===ne.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{}}ye().catch(()=>{})},we=async(e,t)=>{const a=ne.forceRemove(e);if((e=>{re.delete(e)})(e),a){if(a.streamClient)try{await a.streamClient.end()}catch{}for(const e of a.taskIds){const a=se.get(e);if(a){try{const n=f[a.environment??M]??f[M],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken),await $(r,e,a.orgId,a.agentName,{type:"terminal",taskId:e,state:"failed",error:t}),r.destroy()}catch{}se.remove(e)}}await ye()}},ve=async(e,t)=>{const a=se.get(e);if(!a)throw new Error(`No cached credentials for task ${e}`);const n=f[a.environment??M]??f[M],r=s({publishKey:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1});r.setToken(a.writeToken);try{await $(r,e,a.orgId,a.agentName,{...t,type:"terminal",taskId:e})}finally{r.destroy()}await be(e),se.remove(e),await ye()},Se=async(s,r,i,d,c)=>{const u=a,m="pipe"===s.taskKind,f=m?"pipe":"request",g=computeStreamDurationMinutes(s.duration,m,ee);if(m&&"number"==typeof s.durationExpiresAtMs){const e=Math.max(0,s.durationExpiresAtMs-Date.now());if(e>0){const t=setTimeout(()=>{le.add(s.taskId),c.abort()},e);me.set(s.taskId,t)}}await $(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,progress:0,state:"running"}),pe.set(s.taskId,0);let k=0,I=null,w=null;const v=()=>{if(null===I)return;const e=I;I=null,k=Date.now(),$(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{})},S={taskId:s.taskId,requestParts:s.requestParts??[],reportStatus:e=>{de.set(s.taskId,e);const t=Date.now();if(t-k>=1e3)I=null,k=t,$(r,s.taskId,d,u,{type:"progress",taskId:s.taskId,message:e}).catch(()=>{});else if(I=e,!w){w=setTimeout(()=>{w=null,v()},1e3-(t-k))}},get isCancelled(){return c.signal.aborted},get isExpired(){return le.has(s.taskId)},hasStream:!!s.hasStream,cancelSignal:c.signal,taskClient:Z,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(n(t));if(!t.channel||!t.fileId||!t.fileName)throw new Error("File artifactRef missing channel, fileId, or fileName");if(!r.downloadFile)throw new Error("PubNub client does not support downloadFile");const a=await r.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,n=t?.mimeType??"application/octet-stream",o=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:P}:void 0;await K(r,s.taskId,d,u,a,n,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 n=t.card.streams;if(!n||0===Object.keys(n).length)throw new Error("Agent card has no streams block. Streaming requires declared streams in the card.");const i=Object.keys(n);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=n[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,k=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 I=a?.external??!1,w=a?.metadata,v=a?.onActivate,S=l.affinity??"dedicated";if("shared"===S&&I)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"===S)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(I)throw new Error("Request tasks cannot use external streams")}let T;if("shared"===S)T=c;else{const e=(pe.get(s.taskId)??0)+1;pe.set(s.taskId,e),T=`${s.taskId}-${e}`}const C=o.streamChannel(T);if("shared"===S){const e=re.get(T)?.get(s.taskId);if(e)return e}const{entry:E,isNew:A,isNewForTask:$}=ne.acquire(T,s.taskId,{direction:p,format:k,external:I,affinity:S});se.addStream(s.taskId,T);const _=e=>{if("shared"!==S)return;let t=re.get(T);t||(t=new Map,re.set(T,t)),t.set(s.taskId,e)};if(!$){E.setupPromise&&await E.setupPromise;const e=re.get(T)?.get(s.taskId);if(e)return e;if(E.streamClient){const e=y(T,E.streamClient,s.taskId,{releaseStream:Ie});return _(e),e}throw new Error(`Stream "${T}" 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(!A)try{if(E.setupPromise&&await E.setupPromise,"shared"===S&&await(async()=>{await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:"activate",metadata:w,declaredStream:c})})(),E.streamClient){const e=y(T,E.streamClient,s.taskId,{releaseStream:Ie});return _(e),e}if(I)return h(T,C,"",async()=>{throw new Error("External stream already activated")});throw new Error(`Stream "${T}" exists but has no client`)}catch(e){he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}let K,R;E.setupPromise=new Promise((e,t)=>{K=e,R=t}),E.setupPromise.catch(()=>{});const M=I?"token_request":"embedded";let L;try{L=await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:M,metadata:w,declaredStream:c})}catch(e){R(e),he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}if(I){K(),E.setupPromise=null;const e=L.token??"";return h(T,C,e,async e=>{await x(r,{taskId:s.taskId,orgId:d,agentName:u,streamId:T,channel:C,direction:p,format:k,taskKind:f,durationMinutes:g,affinity:S,phase:"activate",metadata:e?.metadata??w,declaredStream:c})})}const N=L.token;if(!N){const e=new Error("No T7a token received from stream setup handshake");R(e),he(T,s.taskId);try{await ne.release(T,s.taskId)}catch{}throw e}const P=new e({subscribeKey:H,publishKey:Y,token:N,agentName:u,streamId:T,channel:C,format:k,direction:p,affinity:S,gating:m,bundleSizeBytes:a?.bundleSizeBytes,maxLatencyMs:a?.maxLatencyMs});E.streamClient=P;const j=y(T,P,s.taskId,{releaseStream:Ie});if(_(j),K(),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=b(T,j,v,we)),j}};let T;try{T=t.handler?await t.handler(s,S):void 0}finally{pe.delete(s.taskId),w&&(clearTimeout(w),w=null),v()}const C=j?{baseUrl:j,authProvider:l?new p(l):void 0,agentAuth:P}:void 0;if(T?.artifacts&&T.artifacts.length>0)for(const e of T.artifacts){const t="string"==typeof e.data?Buffer.from(e.data,"utf-8"):e.data;await K(r,s.taskId,d,u,t,e.mimeType,e.fileName,e.outputId,C)}const E=c.signal.aborted;if(m){const e=le.has(s.taskId),t=ue.has(s.taskId);if(E||e||t){await be(s.taskId);const a=E&&!e?"canceled":"completed";await $(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:a,...e?{completionReason:"duration_expired"}:{},...t?{reason:"terminated"}:{}}),se.remove(s.taskId),_("info",`Task ${s.taskId} ${a}`,{taskId:s.taskId,agentName:u,owner:i})}else _("info",`Task ${s.taskId} handler returned (pipe, no auto-terminal)`,{taskId:s.taskId,agentName:u,owner:i})}else{const e=ue.has(s.taskId);await be(s.taskId);const t=E?"canceled":"completed";await $(r,s.taskId,d,u,{type:"terminal",taskId:s.taskId,state:t,...e?{reason:"terminated"}:{}}),se.remove(s.taskId),_("info",`Task ${s.taskId} ${t}`,{taskId:s.taskId,agentName:u,owner:i})}},Te=(()=>{let e=!1;return(t,a,n)=>{if(_("debug","pamDeniedHandler invoked",{event:"pam_handler_invoked",category:t,operation:a,statusCode:n,alreadyHandled:e,instanceId:c}),"PNAccessDeniedCategory"===t&&!e){e=!0,_("error","PAM access denied — destroying control client (agent will go silent)",{event:"pam_denied_destroy",category:t,operation:a,statusCode:n,instanceId:c,controlChannel:ke}),console.error(`[AgentInstance] PAM token expired or revoked — agent ${c} is no longer receiving tasks. Re-register the agent to resume.`);try{G.destroy()}catch{}}}})(),Ce={status:e=>{const t=e,a=String(t.category??""),n=String(t.operation??""),s="number"==typeof t.statusCode?t.statusCode:null;"PNConnectedCategory"===a&&console.log(`[AgentInstance] PubNub connected to ${ke}`),Te(a,n,s)},message:e=>{const n=e.message;if(!n)return;if("SwitchEnvironment"===n.type){const e=n,t=e.environment;return void(t&&t!==L&&t in f&&((e,t)=>{if(_("info",`SwitchEnvironment requested: ${L} -> ${e}`,{pamToken:t?"present":"absent"}),!t)return void _("error","SwitchEnvironment rejected: pamToken is required but was absent. The backend must include a pamToken in SwitchEnvironment messages.");const a=G,n=z;try{a.removeListener(Ce),ke&&a.unsubscribe({channels:[ke]})}catch{}F(a);const r=f[e];if(G=s({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:c,presenceTimeout:20,announceSuccessfulHeartbeats:_resolveLogLevel()>=_LOG_LEVEL_ORDER.debug,subscribeRetryUnbounded:!0,onRetry:e=>{_("warn","pubnub transport retrying",{event:"pubnub_transport_retry",message:e,instanceId:c})}}),z=!0,ge=void 0,G.setToken(t),0!==Q&&"function"==typeof G.setFilterExpression&&G.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),G.addListener(Ce),B("control",G),ke&&G.subscribe({channels:[ke]}),n)try{a.destroy()}catch{}L=e,H=r.subscribeKey,Y=r.publishKey,Z.updateKeys(r.subscribeKey,r.publishKey),_("info",`Switched to ${e} environment`),ye().catch(()=>{})})(t,e.pamToken))}(async(e,n)=>{if("StartTask"===e.type){const r=L,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(fe.has(e.taskId))return void _("warn",`Ignoring duplicate StartTask for in-flight task ${e.taskId}`,{taskId:e.taskId});const d=Boolean(n?.broadcast);if(e.protocolVersion&&!A(e.protocolVersion))return d?void _("debug",`Ignoring broadcast StartTask ${e.taskId} with unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId}):(await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"unsupported_protocol_version"},e.protocolVersion),void _("warn",`Task ${e.taskId} rejected: unsupported protocolVersion ${e.protocolVersion}`,{taskId:e.taskId,instanceId:c}));if("pipe"===(e.taskKind||"request")){const t=e.duration,n=e.durationExpiresAtMs;if(null==t||!Number.isInteger(t)||t<1||t>43200||"number"!=typeof n||n<=0)return _("error",`Task ${e.taskId} rejected: pipe StartTask with invalid duration (duration=${t}, durationExpiresAtMs=${n})`,{taskId:e.taskId,instanceId:c}),void await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"invalid_start_task"},e.protocolVersion)}const l=ae+ne.activeStreamCount;if(J>0&&l>=J)return d?void _("debug",`Task ${e.taskId} skipped (broadcast, at capacity ${l}/${J})`,{taskId:e.taskId,instanceId:c}):(await $(G,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:"agent_at_capacity"}),void _("warn",`Task ${e.taskId} rejected: agent at capacity`,{taskId:e.taskId,instanceId:c,activeTasks:ae}));oe.set(e.taskId,o),ie.set(e.taskId,i),fe.add(e.taskId),ae++,e.controlToken&&(ge=e.controlToken);const u=new AbortController;ce.set(e.taskId,u);let m=null;try{const n=f[r];if(!n.publishKey||!n.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:n.publishKey,subscribeKey:n.subscribeKey,userId:c,subscribeRetryUnbounded:!1}),e.writeToken&&m.setToken(e.writeToken),B(`task-sub:${e.taskId}`,m),se.set(e.taskId,{ownerId:o,orgId:i,writeToken:e.writeToken??"",agentName:a,environment:r}),await ye();const{writeToken:d,controlToken:l,...p}=e;t.onStartTask?await t.onStartTask(e,m):await Se(p,m,o,i,u)}catch(n){const s=n?.message??"Agent instance error",r=m??G;try{await be(e.taskId)}catch{}await $(r,e.taskId,i,a,{type:"terminal",taskId:e.taskId,state:"failed",error:s}),t.onError&&await t.onError(e.taskId,n),_("error",s,{taskId:e.taskId}),se.remove(e.taskId)}finally{const t=me.get(e.taskId);if(t&&(clearTimeout(t),me.delete(e.taskId)),fe.delete(e.taskId),oe.delete(e.taskId),ie.delete(e.taskId),de.delete(e.taskId),ce.delete(e.taskId),le.delete(e.taskId),ue.delete(e.taskId),m){F(m);try{m.destroy()}catch{}}ae--,0===ae&&ge&&G.setToken(ge),await ye()}}else if("CancelTask"===e.type){const t=ce.get(e.taskId);t?(t.abort(),_("info",`Task ${e.taskId} cancel requested (cooperative)`,{taskId:e.taskId,instanceId:c})):se.get(e.taskId)&&await ve(e.taskId,{state:"canceled"})}else if("ExpireTask"===e.type)ce.has(e.taskId)?(le.add(e.taskId),ce.get(e.taskId)?.abort(),_("info",`Task ${e.taskId} expired`,{taskId:e.taskId,instanceId:c})):se.get(e.taskId)&&await ve(e.taskId,{state:"completed",completionReason:"duration_expired"});else if("TerminateTask"===e.type)ce.has(e.taskId)?(ue.add(e.taskId),ce.get(e.taskId)?.abort()):se.get(e.taskId)&&await ve(e.taskId,{state:"canceled",reason:"terminated"});else if("PauseTask"===e.type){if(fe.has(e.taskId)){const t=ie.get(e.taskId)??"anonymous";await $(G,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"paused"})}}else if("ResumeTask"===e.type){if(fe.has(e.taskId)){const t=ie.get(e.taskId)??"anonymous";await $(G,e.taskId,t,a,{type:"system",taskId:e.taskId,status:"resumed"})}}else"RetryTask"===e.type&&_("info","RetryTask received (no-op)",{taskId:e.taskId})})(n,e.userMetadata&&"object"==typeof e.userMetadata?e.userMetadata:void 0).catch(e=>{console.error("[AgentInstance] Unhandled error in message handler:",e)})}};G.addListener(Ce),B("control",G),null===V&&(V=setInterval(()=>{const e=Date.now();let t=!1;const a=q.map(a=>{const n=null!==a.lastStatusAt?e-a.lastStatusAt:null,s=_isDiagEntryStale({lastStatusAt:a.lastStatusAt,lastCategory:a.lastCategory,now:e,thresholdMs:6e4});s&&(t=!0);const r="function"==typeof a.pn.getSubscribedChannels?a.pn.getSubscribedChannels().length:null;return{client:a.label,lastCategory:a.lastCategory,lastOperation:a.lastOperation,lastStatusCode:a.lastStatusCode,msSinceStatus:n,msSinceConnected:null!==a.lastConnectedAt?e-a.lastConnectedAt:null,msSinceMessage:null!==a.lastMessageAt?e-a.lastMessageAt:null,subscribedChannels:r,stale:s}}),n={event:"pubnub_alive_snapshot",instanceId:c,clients:a};t?_("info","pubnub alive snapshot — STALE clients present",n):_("debug","pubnub alive snapshot",n)},1e4),"function"==typeof V.unref&&V.unref()),_("info","pubnub diagnostics armed",{event:"pubnub_diagnostics_armed",snapshotIntervalMs:1e4,staleThresholdMs:6e4,instanceId:c}),i(a,{instanceId:c,billingMode:O,description:t.description,scaling:{expectedInstances:Q,concurrency:J,maxPendingBacklog:X,maxRunningTimeSec:ee},card:t.card,cardRef:t.cardRef,cardSummary:t.cardSummary,listing:D??t.listing,actor:`agent-instance:${c}`,baseUrl:t.baseUrl??R?.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");ke=e.controlChannel,e.pamToken&&G.setToken(e.pamToken),0!==Q&&"function"==typeof G.setFilterExpression&&G.setFilterExpression(`meta.instance == '${c}' || meta.broadcast == "true"`),G.subscribe({channels:[ke]}),ye().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)}),_("info",`Agent instance ${c} started (agent name: ${a})`,{agentName:a,event:"agent_instance_started",instanceId:c});return{stop:()=>{try{null!==V&&(clearInterval(V),V=null);for(const e of q)try{e.pn.removeListener(e.listener)}catch{}q.length=0,W?.destroy(),Z.destroy(),G.removeListener(Ce),ke&&G.unsubscribe({channels:[ke]}),z&&G.destroy();for(const e of ne.streamIds()){const t=ne.get(e);if(t?.streamClient)try{t.streamClient.end()}catch{}}ne.clear(),re.clear(),se.clear()}catch{}},agentName:a,instanceId:c,get controlChannel(){return ke},get pubnub(){return G},get subscribeKey(){return f[L].subscribeKey},taskClient:Z,publishTerminal:ve,failStream:we,cdmConfig:R}};export const artifactUtils={buildArtifactRef:t,shouldInlineArtifact:a};
@@ -4,5 +4,41 @@ export interface PubNubClientConfig {
4
4
  publishKey: string;
5
5
  userId?: string;
6
6
  presenceTimeout?: number;
7
+ /**
8
+ * Phase 1 connectivity diagnostic: when true, PubNub emits a status event
9
+ * on every successful heartbeat, so the SDK status listener can prove
10
+ * "heartbeats stopped" during a network outage. Disabled by default
11
+ * because heartbeats fire every ~10 s per client and would otherwise
12
+ * spam the log.
13
+ */
14
+ announceSuccessfulHeartbeats?: boolean;
15
+ /**
16
+ * BLOCKS-129 silent-park fix. When true, the client uses an
17
+ * ExponentialRetryPolicy with maximumRetry expanded to ~30 days
18
+ * (43_200 attempts at the 60s cap). PubNub's Event Engine otherwise
19
+ * exhausts the default 6-attempt budget after ~3.5 min and parks in
20
+ * RECEIVE_FAILED/HEARTBEAT_FAILED, going silent until reconnect()
21
+ * is called manually. The PubNub JS SDK docs confirm maximumRetry has
22
+ * no upper bound; the constructor's validate() does enforce 6, so we
23
+ * mutate maximumRetry on the policy object after construction.
24
+ *
25
+ * Default: true (parity with the Python SDK). Per-task / ephemeral /
26
+ * per-stream call sites MUST pass `subscribeRetryUnbounded: false`
27
+ * explicitly so they keep the fail-fast retry budget — a stuck task
28
+ * is preferable to a stuck loop. See dev_docs/SDK_CONTRACT.md
29
+ * §Cross-SDK retry-budget defaults for the contract.
30
+ */
31
+ subscribeRetryUnbounded?: boolean;
32
+ /**
33
+ * BLOCKS-129 Phase 2B: surfaces the SDK's transport-layer retry
34
+ * activity during a network outage. PubNub's middleware logs
35
+ * `'HTTP request retry #N in Nms.'` at warn level whenever the
36
+ * transport schedules a retry; with subscribeRetryUnbounded on these
37
+ * fire continuously through an outage but never bubble up to the
38
+ * Event Engine listener. Wiring this callback turns those warnings
39
+ * into a visible signal so a human watching the log can tell the
40
+ * difference between "agent retrying" and "agent dead".
41
+ */
42
+ onRetry?: (message: string) => void;
7
43
  }
8
44
  export declare const createPubNubClient: (config: PubNubClientConfig) => PubNub;
@@ -1 +1 @@
1
- import e from"pubnub";export const createPubNubClient=r=>{if(!r.subscribeKey||!r.publishKey)throw new Error("PUBNUB keys not configured");return new e({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:r.userId??"blocks-agent",enableEventEngine:!0,...void 0!==r.presenceTimeout?{presenceTimeout:r.presenceTimeout}:{}})};
1
+ import e from"pubnub";const n=()=>{const n=e.LogLevel;return"number"==typeof n?.Warn?n.Warn:3};export const createPubNubClient=r=>{if(!r.subscribeKey||!r.publishKey)throw new Error("PUBNUB keys not configured");const t=r.subscribeRetryUnbounded??!0?(()=>{const n=e.ExponentialRetryPolicy;if("function"!=typeof n)return null;const r=n({minimumDelay:2,maximumDelay:60,maximumRetry:6});try{r.maximumRetry=43200,r.validate=()=>{}}catch{return null}return 43200!==r.maximumRetry?null:r})():null,u=r.onRetry?(s=r.onRetry,{trace:()=>{},debug:()=>{},info:()=>{},error:()=>{},warn:e=>{"PubNubMiddleware"===e.location&&"text"===e.messageType&&"string"==typeof e.message&&e.message.includes("HTTP request retry")&&s(e.message)}}):null;var s;return new e({publishKey:r.publishKey,subscribeKey:r.subscribeKey,userId:r.userId??"blocks-agent",enableEventEngine:!0,...void 0!==r.presenceTimeout?{presenceTimeout:r.presenceTimeout}:{},...!0===r.announceSuccessfulHeartbeats?{announceSuccessfulHeartbeats:!0}:{},...null!==t?{retryConfiguration:t}:{},...null!==u?{logLevel:n(),loggers:[u]}:{}})};
@@ -316,6 +316,12 @@ export declare class TaskClient {
316
316
  * `StreamUnavailableError` per the merged t7c baseline.
317
317
  */
318
318
  drainWindowMs?: number;
319
+ /**
320
+ * Role to request when minting the read token. Defaults to 'consumer'
321
+ * (task submitter). Set to 'provider' when the caller is the agent
322
+ * owner viewing a received task.
323
+ */
324
+ role?: 'consumer' | 'provider';
319
325
  }): Promise<TaskSession>;
320
326
  /**
321
327
  * Shared session assembly used by both branches of `connect()`:
@@ -1 +1 @@
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 h}from"../env.js";import{StreamRef as b}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 g}from"./protocol-version.js";import{getAgent as k}from"./agent-registry.js";const y=new Set(["completed","failed","canceled"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}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=f(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 b(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 T(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._anonFingerprint=e.anonFingerprint??null,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),o=!!n.anonFingerprint;if([n.apiKey,n.tokenEndpoint,n.tokenProvider,n.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(o&&"free"!==s)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const a=n.cdmUrl??h("BLOCKS_CDM_URL"),c=await u(a),d="paid"===s?c.network:c.playground,l=n.subscribeKey??h("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,b=n.publishKey??h("BLOCKS_PUBLISH_KEY")??d.publishKey,f=n.baseUrl??h("BLOCKS_BACKEND_URL")??c.api.baseUrl;if(!l)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 p=()=>{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;return new e({subscribeKey:l,publishKey:b||void 0,userId:t,enableEventEngine:!0})};if(r){const e=new i({apiKey:n.apiKey,tokenEndpoint:n.tokenEndpoint,tokenProvider:n.tokenProvider,baseUrl:f,onAuthError:n.onAuthError});await e.init();const t=new TaskClient({billingMode:s,subscribeKey:l,publishKey:b,baseUrl:f,createSessionPubNub:p,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:s,subscribeKey:l,publishKey:b,baseUrl:f,createSessionPubNub:p,anonFingerprint:n.anonFingerprint})}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 fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[g]:m};l(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(d(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();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 h=[];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;h.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,...b}=t;h.push({partId:b.partId,...b.contentType?{contentType:b.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;h.push(s)}const b={agentName:e.agentName,billingMode:this._billingMode,requestParts:h};l&&(b.uploadSessionId=l),e.idempotencyKey&&(b.idempotencyKey=e.idempotencyKey),b.ownerId=i;const f={};u&&(f.taskKind=u),null!=d&&(f.duration=d),e.consumerPublicKey&&(f.consumerPublicKey=e.consumerPublicKey),Object.keys(f).length>0&&(b.extensions={blocks:f}),e.pushNotificationConfig&&(b.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(b.retryPolicy=e.retryPolicy);const m=await t(this.config,"SendMessage",b),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 I=this.createPerSessionPubNub(k),C={subscribeKey:this._subscribeKey,publishKey:this._publishKey},P=S??n(m.taskId,m.orgId??i);try{const t=await I.time(),n=String(t.timetoken),r=p(I);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=w(await T(r,P),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:P,agentName:e.agentName,pubnub:I,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 h,b=!1;const f={message:e=>{if(e.channel!==P)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");b&&h?h(t,n):l.push({message:t,timetoken:n})}};I.addListener(f),I.subscribe({channels:[P],timetoken:d});const g=new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:P,agentName:e.agentName,pubnub:I,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:f,channel:P,onReady:e=>{h=e}}});if(h){const e=h;for(const t of l)t.timetoken>d&&e(t.message,t.timetoken)}return l.length=0,b=!0,g}catch{return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:P,agentName:e.agentName,pubnub:I,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._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}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 s=n.agentName??"",i=n.state??"",r=await this.fetchConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:i,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},h=this.createPerSessionPubNub(u);try{const e=await h.time(),r=String(e.timetoken),b=p(h);let f,m=new Map,g=[],k="0";if(b?.fetchMessages){const e=w(await T(b,d),c,n,l);m=e.streams,g=e.artifacts,k=e.highWaterMark,f=e.terminalState}const S=f&&y.has(f)?f:i;if(y.has(S))return new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:h,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:S,skipSubscription:!0,preloadedStreams:m,preloadedArtifacts:g});const I="0"!==k?k:r,C=[];let P,K=!1;const N={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");K&&P?P(t,n):C.push({message:t,timetoken:n})}};h.addListener(N),h.subscribe({channels:[d],timetoken:I});const _=new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:h,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:i,preloadedStreams:m,preloadedArtifacts:g,externalSubscription:{listener:N,channel:d,onReady:e=>{P=e}}});if(P){const e=P;for(const t of C)t.timetoken>I&&e(t.message,t.timetoken)}return C.length=0,K=!0,_}catch(e){throw h.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
+ 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 h}from"./stream-ref.js";import{invertDirection as p}from"../stream/index.js";import{asPubNubFetcher as f}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"]);export class AnonTaskAccessDeniedError extends Error{constructor(e="anon-task-read-token denied: HTTP 403 Forbidden"){super(e),this.name="AnonTaskAccessDeniedError"}}function w(e,t,n,s){const i=new Map,r=[],o=[];let a,c="0",u="0";for(const d of e){d.timetoken&&d.timetoken>c&&(c=d.timetoken);const e=d.message;if(e&&"object"==typeof e&&e.type){if(o.push(e),"terminal"===e.type){const t=String(d.timetoken??"0");if(t>=u){const n=e.state;"completed"!==n&&"failed"!==n&&"canceled"!==n||(a=n,u=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=p(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 h(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,events:o,highWaterMark:c,terminalState:a}}async function T(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._anonFingerprint=e.anonFingerprint??null,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),o=!!n.anonFingerprint;if([n.apiKey,n.tokenEndpoint,n.tokenProvider,n.anonFingerprint].filter(Boolean).length>1)throw new Error("Only one token provider mode may be specified");if(o&&"free"!==s)throw new Error("TaskClient.create() with anonFingerprint requires billingMode: 'free'");const a=n.cdmUrl??b("BLOCKS_CDM_URL"),c=await u(a),d="paid"===s?c.network:c.playground,l=n.subscribeKey??b("BLOCKS_SUBSCRIBE_KEY")??d.subscribeKey,h=n.publishKey??b("BLOCKS_PUBLISH_KEY")??d.publishKey,p=n.baseUrl??b("BLOCKS_BACKEND_URL")??c.api.baseUrl;if(!l)throw new Error("TaskClient.create() could not resolve subscribeKey from options, env, or CDM");if(!p)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:l,publishKey:h||void 0,userId:t,enableEventEngine:!0})};if(r){const e=new i({apiKey:n.apiKey,tokenEndpoint:n.tokenEndpoint,tokenProvider:n.tokenProvider,baseUrl:p,onAuthError:n.onAuthError});await e.init();const t=new TaskClient({billingMode:s,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:f,defaultOwnerId:e.getUserId()??void 0});return t.config.authProvider=e,t._consumerAuth=e,t}return new TaskClient({billingMode:s,subscribeKey:l,publishKey:h,baseUrl:p,createSessionPubNub:f,anonFingerprint:n.anonFingerprint})}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,n,s){let i;const r=n??this._subscribeKey,o=s??this._publishKey;if(this._createSessionPubNub&&r===this._subscribeKey)i=this._createSessionPubNub();else{const t=`blocks-task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`;i=new e({subscribeKey:r,publishKey:o||void 0,userId:t,enableEventEngine:!0})}return t&&i.setToken(t),i}async fetchConsumerReadToken(e,t="consumer"){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");const n=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/task-read-token`,s=async()=>{const s={"Content-Type":"application/json",[g]:m},i=this.config.authProvider?.getAuthHeader();return i&&(s.Authorization=i),l(s),fetch(n,{method:"POST",headers:s,body:JSON.stringify({taskId:e,role:t})})};let i=await s();if(d(i.headers),401===i.status&&this.config.authProvider){await this.config.authProvider.onAuthFailure()&&(i=await s(),d(i.headers))}if(!i.ok){const e=await i.text().catch(()=>"");throw new Error(`task-read-token failed: HTTP ${i.status}${e?` ${e}`:""}`)}return i.json()}async fetchAnonConsumerReadToken(e){if(!this.config.baseUrl)throw new Error("connect() requires a backend baseUrl. Set baseUrl in TaskClientOptions.");if(!this._anonFingerprint)throw new Error("fetchAnonConsumerReadToken called without anonFingerprint");const t=`${this.config.baseUrl.replace(/\/+$/,"")}/api/v1/auth/anon-task-read-token`,n={"Content-Type":"application/json",[g]:m};l(n);const s=await fetch(t,{method:"POST",headers:n,body:JSON.stringify({taskId:e,fingerprint:this._anonFingerprint})});if(d(s.headers),403===s.status){const e=await s.text().catch(()=>"");throw new AnonTaskAccessDeniedError("anon-task-read-token denied: HTTP 403"+(e?` ${e}`:""))}if(!s.ok){const e=await s.text().catch(()=>"");throw new Error(`anon-task-read-token failed: HTTP ${s.status}${e?` ${e}`:""}`)}return s.json()}async sendMessage(e){if(this._anonFingerprint)throw new Error("anon-mode TaskClient does not support sendMessage()");this.config.authProvider?.ensureReady&&await this.config.authProvider.ensureReady();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,...h}=t;b.push({partId:h.partId,...h.contentType?{contentType:h.contentType}:{}})}}else{const{file:e,fileName:n,...s}=t;b.push(s)}const h={agentName:e.agentName,billingMode:this._billingMode,requestParts:b};l&&(h.uploadSessionId=l),e.idempotencyKey&&(h.idempotencyKey=e.idempotencyKey),h.ownerId=i;const p={};u&&(p.taskKind=u),null!=d&&(p.duration=d),e.consumerPublicKey&&(p.consumerPublicKey=e.consumerPublicKey),Object.keys(p).length>0&&(h.extensions={blocks:p}),e.pushNotificationConfig&&(h.pushNotificationConfig=e.pushNotificationConfig),e.retryPolicy&&(h.retryPolicy=e.retryPolicy);const m=await t(this.config,"SendMessage",h),g=m.extensions?.blocks,k=g?.readToken??null,S=g?.streamChannels?.status??void 0,I=g?.subscribeKey??this._subscribeKey,C=g?.publishKey??this._publishKey;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:I,publishKey:C},rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,preClosed:!0,state:m.state});const P=this.createPerSessionPubNub(k,I,C),K={subscribeKey:I,publishKey:C},N=S??n(m.taskId,m.orgId??i);try{const t=await P.time(),n=String(t.timetoken),r=f(P);let o,a=new Map,c=[],u="0";if(r?.fetchMessages){const t=w(await T(r,N),m.taskId,e.agentName,K);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:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,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,h=!1;const p={message:e=>{if(e.channel!==N)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");h&&b?b(t,n):l.push({message:t,timetoken:n})}};P.addListener(p),P.subscribe({channels:[N],timetoken:d});const g=new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,rpcConfig:this.config,idempotent:m.idempotent,queued:m.queued,pushConfigId:m.pushConfigId,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs,preloadedStreams:a,preloadedArtifacts:c,externalSubscription:{listener:p,channel:N,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,h=!0,g}catch{return new s({taskId:m.taskId,ownerId:i,orgId:m.orgId??i,readToken:k,statusChannel:N,agentName:e.agentName,pubnub:P,ownsSubscribeClient:!0,sdkOptions:K,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._anonFingerprint){const n=await this.getTask(t);if(!n)throw new Error(`Task not found: ${t}`);const s=n.agentName??"",i=n.state??"",r=await this.fetchAnonConsumerReadToken(t);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}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 s=n.agentName??"",i=n.state??"",r=await this.fetchConsumerReadToken(t,e.role);return this._openSessionFromToken({task:n,agentName:s,taskState:i,tokenResult:r,autoDrain:e.autoDrain,drainWindowMs:e.drainWindowMs})}async _openSessionFromToken(e){const{task:t,agentName:n,taskState:i,tokenResult:r,autoDrain:o,drainWindowMs:a}=e,c=t.taskId,{pamToken:u,channel:d}=r,l={subscribeKey:this._subscribeKey,publishKey:this._publishKey},b=this.createPerSessionPubNub(u);try{const e=await b.time(),r=String(e.timetoken),h=f(b);let p,m=new Map,g=[],k=[],S="0";if(h?.fetchMessages){const e=w(await T(h,d),c,n,l);m=e.streams,g=e.artifacts,k=e.events,S=e.highWaterMark,p=e.terminalState}const I=p&&y.has(p)?p:i;if(y.has(I))return new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:I,skipSubscription:!0,preloadedStreams:m,preloadedArtifacts:g,preloadedEvents:k});const C="0"!==S?S:r,P=[];let K,N=!1;const E={message:e=>{if(e.channel!==d)return;const t=e.message;if(!t||"object"!=typeof t||!t.type)return;const n=String(e.timetoken??"0");N&&K?K(t,n):P.push({message:t,timetoken:n})}};b.addListener(E),b.subscribe({channels:[d],timetoken:C});const _=new s({taskId:c,ownerId:t.owner??"",readToken:u,statusChannel:d,agentName:n,pubnub:b,ownsSubscribeClient:!0,sdkOptions:l,rpcConfig:this.config,autoDrain:o,drainWindowMs:a,state:i,preloadedStreams:m,preloadedArtifacts:g,preloadedEvents:k,externalSubscription:{listener:E,channel:d,onReady:e=>{K=e}}});if(K){const e=K;for(const t of P)t.timetoken>C&&(_._appendHistoryEvent(t.message,t.timetoken),e(t.message,t.timetoken))}return P.length=0,N=!0,_}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,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)}
@@ -66,6 +66,8 @@ export declare class TaskSession {
66
66
  private errorCallbacks;
67
67
  private streams;
68
68
  private artifacts;
69
+ private historyEvents;
70
+ private historyTimetokens;
69
71
  private streamWaiters;
70
72
  private terminalWaiters;
71
73
  private closed;
@@ -131,6 +133,8 @@ export declare class TaskSession {
131
133
  preloadedStreams?: Map<string, StreamRef>;
132
134
  /** Pre-populated artifact refs from history (connect()). */
133
135
  preloadedArtifacts?: ArtifactRef[];
136
+ /** Pre-populated task events from connect() history. */
137
+ preloadedEvents?: TaskEvent[];
134
138
  /** External subscription managed by connect() for active tasks. */
135
139
  externalSubscription?: {
136
140
  listener: unknown;
@@ -165,6 +169,10 @@ export declare class TaskSession {
165
169
  onError(cb: (error: Error, context: CallbackErrorContext) => void): Unsubscribe;
166
170
  /** Return all artifact refs seen so far (from history and live events). */
167
171
  listArtifacts(): ArtifactRef[];
172
+ /** Return all history events from connect() in arrival order. */
173
+ listEvents(): TaskEvent[];
174
+ /** @internal Called by connect() to append buffer-drain events to the history snapshot. */
175
+ _appendHistoryEvent(event: TaskEvent, timetoken?: string): void;
168
176
  /**
169
177
  * Download an artifact from an ArtifactRef.
170
178
  * Delegates to the standalone downloadArtifact() helper.
@@ -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 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;
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.historyEvents=[],this.historyTimetokens=new Set,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.preloadedEvents&&this.historyEvents.push(...t.preloadedEvents),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 c=a.affinity;if("dedicated"!==c&&"shared"!==c){console.warn(`[TaskSession] live stream_started: dropping stream "${t}" for task "${this.taskId}" — invalid or missing affinity (got ${JSON.stringify(a.affinity)})`);continue}const l={taskId:this.taskId,streamId:t,agentName:this.agentName,channel:a.channel,token:a.token,agentDirection:i,localDirection:n,format:o,affinity:c,metadata:a.metadata,declaredStream:r},h=new s(l,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){this.artifactCallbacks.push(t);for(const e of[...this.artifacts]){const s={type:"artifact",taskId:this.taskId,artifactRef:e};try{t(s)}catch(t){this.routeCallbackError(t,"onArtifact",s)}}return()=>{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]}listEvents(){return[...this.historyEvents]}_appendHistoryEvent(t,e){if(e){if(this.historyTimetokens.has(e))return;this.historyTimetokens.add(e)}this.historyEvents.push(t)}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=[],c=this.listArtifacts();for(let t=0;t<c.length;t++){const e=await this.downloadArtifact(c[t]),l=e.fileName??`artifact-${t}`,h=r(i(n,a(l)||`artifact-${t}`));if(!h.startsWith(n+"/")&&h!==n)throw new Error(`Artifact filename "${l}" resolves outside target directory`);s(h,e.data),o.push(h)}return o}async cancel(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.cancelTask() directly");await r(this.rpcConfig,"CancelTask",{taskId:this.taskId})}async terminate(){if(!this.rpcConfig)throw new Error("TaskSession was not created with RPC config; use TaskClient.terminateTask() directly");await r(this.rpcConfig,"TerminateTask",{taskId:this.taskId})}close(){if(this.closed)return;this.closed=!0,this.drainTimer&&(clearTimeout(this.drainTimer),this.drainTimer=null);for(const t of this.openStreamClients)t.isActive&&t.end().catch(()=>{});this.openStreamClients.clear();const t=new Error("TaskSession closed");for(const e of this.streamWaiters)e.reject(t);this.streamWaiters=[];for(const e of this.terminalWaiters)e.reject(t);this.terminalWaiters=[],this.pubnub&&(this.listener&&(this.pubnub.removeListener(this.listener),this.listener=null),this.pubnub.unsubscribe({channels:[this.statusChannel]}),this.ownsSubscribeClient&&this.pubnub.destroy()),this.progressCallbacks=[],this.artifactCallbacks=[],this.terminalCallbacks=[],this.eventCallbacks=[],this.streamCallbacks=[],this.errorCallbacks=[]}async asyncClose(){if(this.closed)return;const t=[...this.openStreamClients].filter(t=>t.isActive).map(t=>t.end().catch(()=>{}));await Promise.all(t),this.openStreamClients.clear(),this.close()}async[Symbol.asyncDispose](){await this.asyncClose()}get isClosed(){return this.closed}}TaskSession.SEEN_TIMETOKENS_MAX=200;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocks-network/sdk",
3
- "version": "0.1.53",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "description": "Blocks Network SDK — build and run A2A agents on PubNub",
6
6
  "exports": {
@@ -27,7 +27,8 @@
27
27
  "files": [
28
28
  "dist/",
29
29
  "schemas/",
30
- "README.md"
30
+ "README.md",
31
+ "LICENSE"
31
32
  ],
32
33
  "engines": {
33
34
  "node": ">=20.0.0"
@@ -52,9 +53,9 @@
52
53
  "eslint": "^8.57.1",
53
54
  "jsdom": "^29.0.2",
54
55
  "terser": "^5.46.1",
55
- "typescript": "^5.4.5",
56
- "vite": "^5.4.21",
57
- "vitest": "^1.6.0"
56
+ "typescript": "^5.9.3",
57
+ "vite": "^7.3.3",
58
+ "vitest": "^3.2.4"
58
59
  },
59
60
  "repository": {
60
61
  "type": "git",
@@ -65,7 +66,7 @@
65
66
  "bugs": {
66
67
  "url": "https://github.com/pubnub/blocksnetwork/issues"
67
68
  },
68
- "license": "PubNub",
69
+ "license": "SEE LICENSE IN LICENSE",
69
70
  "overrides": {
70
71
  "pubnub": {
71
72
  "react-native-url-polyfill": "npm:empty-npm-package@1.0.0"