@standardagents/builder 0.27.5 → 0.27.7

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/dist/runtime.d.ts CHANGED
@@ -1003,7 +1003,9 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
1003
1003
  private currentExecutionOwnerId;
1004
1004
  /** Persist + cache the execution-owner record (null releases ownership). */
1005
1005
  private setExecutionOwner;
1006
- /** Whether the given client id has a live bridge socket (excluding `except`). */
1006
+ /** Whether the given client id has an open bridge socket (excluding `except`).
1007
+ * Idle bridge heartbeats are handled by Cloudflare without waking this object,
1008
+ * so timestamp liveness is intentionally enforced only for an in-flight call. */
1007
1009
  private hasLiveBridgeSocketForClient;
1008
1010
  /** Tell every other bridge socket that execution ownership moved. */
1009
1011
  private broadcastOwnerChanged;
@@ -1031,11 +1033,10 @@ declare class DurableThread<Env extends ThreadEnv = ThreadEnv> extends DurableOb
1031
1033
  private markBridgeDead;
1032
1034
  /**
1033
1035
  * Return the bridge socket a forwarded call should be dispatched to, pruning
1034
- * dead ones. A half-open client (idle NAT/proxy drop) keeps a
1035
- * `readyState === OPEN` socket that never fires close/error, so we also treat
1036
- * a socket we haven't heard from within {@link BRIDGE_LIVENESS_TIMEOUT_MS} as
1037
- * dead. The CLI heartbeats every few seconds, so this only trips on a truly
1038
- * gone client — and it means we won't dispatch a forwarded call into the void.
1036
+ * closed ones. Idle clients use Cloudflare's hibernation auto-response, so
1037
+ * their frames deliberately do not update `bridgeLastSeen`. Once a call is
1038
+ * dispatched, the client switches to application-visible pings and the
1039
+ * in-flight liveness timer above detects a half-open connection.
1039
1040
  *
1040
1041
  * With an execution owner recorded, only the owner's socket (or, as a
1041
1042
  * back-compat fallback, an unidentified legacy socket) is eligible; with no
@@ -3604,6 +3605,28 @@ declare class ProviderRegistryImpl {
3604
3605
  private providerOverrideFn;
3605
3606
  private providerOverrideRevision;
3606
3607
  setProviderOverride(fn: ProviderOverrideFn | null): void;
3608
+ /**
3609
+ * The env a provider resolves against for a given execution, scoped to the
3610
+ * THREAD rather than just the instance: thread env overrides win, then the
3611
+ * thread OWNER's user env, then instance/Worker env. This is what makes
3612
+ * per-user BYOK providers work — a credential a user installs in their own
3613
+ * account env (e.g. an OAuth token a companion service rotates) is honored
3614
+ * for that user's threads no matter which authorized actor plays the turn.
3615
+ *
3616
+ * Best-effort by design: a missing user id (super-admin/local threads) or
3617
+ * an older AgentBuilder DO without user-env storage simply contributes
3618
+ * nothing, leaving instance-env behavior untouched.
3619
+ */
3620
+ buildThreadScopedEnv(state: {
3621
+ env: Env;
3622
+ thread: {
3623
+ metadata?: {
3624
+ user_id?: string | null;
3625
+ env?: Record<string, unknown> | null;
3626
+ tenvs?: Record<string, unknown> | null;
3627
+ };
3628
+ };
3629
+ }): Promise<Env>;
3607
3630
  /**
3608
3631
  * Get provider for a model.
3609
3632
  *
package/dist/runtime.js CHANGED
@@ -890,6 +890,42 @@ var init_ProviderRegistry = __esm({
890
890
  this.providerOverrideFn = fn;
891
891
  this.providerOverrideRevision += 1;
892
892
  }
893
+ /**
894
+ * The env a provider resolves against for a given execution, scoped to the
895
+ * THREAD rather than just the instance: thread env overrides win, then the
896
+ * thread OWNER's user env, then instance/Worker env. This is what makes
897
+ * per-user BYOK providers work — a credential a user installs in their own
898
+ * account env (e.g. an OAuth token a companion service rotates) is honored
899
+ * for that user's threads no matter which authorized actor plays the turn.
900
+ *
901
+ * Best-effort by design: a missing user id (super-admin/local threads) or
902
+ * an older AgentBuilder DO without user-env storage simply contributes
903
+ * nothing, leaving instance-env behavior untouched.
904
+ */
905
+ async buildThreadScopedEnv(state) {
906
+ const threadEnv = state.thread.metadata?.env ?? state.thread.metadata?.tenvs ?? null;
907
+ const userId = state.thread.metadata?.user_id ?? null;
908
+ let userEnv = null;
909
+ if (userId) {
910
+ try {
911
+ const agentBuilderId = state.env.AGENT_BUILDER.idFromName("singleton");
912
+ const agentBuilder = state.env.AGENT_BUILDER.get(agentBuilderId);
913
+ if (typeof agentBuilder.getUserEnvEntries === "function") {
914
+ const entries = await agentBuilder.getUserEnvEntries(userId);
915
+ if (Array.isArray(entries) && entries.length > 0) {
916
+ userEnv = {};
917
+ for (const entry of entries) {
918
+ if (entry?.name) userEnv[entry.name] = entry.value;
919
+ }
920
+ }
921
+ }
922
+ } catch (error) {
923
+ console.warn("buildThreadScopedEnv: user env unavailable:", error instanceof Error ? error.message : String(error));
924
+ }
925
+ }
926
+ if (!userEnv && !threadEnv) return state.env;
927
+ return { ...state.env, ...userEnv ?? {}, ...threadEnv ?? {} };
928
+ }
893
929
  /**
894
930
  * Get provider for a model.
895
931
  *
@@ -1801,7 +1837,8 @@ var init_LLMRequest = __esm({
1801
1837
  */
1802
1838
  static async callModel(modelId, context, state, logId) {
1803
1839
  const { ProviderRegistry: ProviderRegistry2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
1804
- const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(modelId, state.env, state.thread.instance);
1840
+ const providerEnv = await ProviderRegistry2.buildThreadScopedEnv(state);
1841
+ const { provider, modelName, modelDef, viaPlatform, platformProxyOrigin } = await ProviderRegistry2.getProvider(modelId, providerEnv, state.thread.instance);
1805
1842
  const streamAbort = new AbortController();
1806
1843
  const parentSignal = state.abortController?.signal;
1807
1844
  let onParentAbort;
@@ -16093,9 +16130,11 @@ var DurableThread = class _DurableThread extends DurableObject {
16093
16130
  codeExecutionBridges = /* @__PURE__ */ new Map();
16094
16131
  constructor(ctx, env) {
16095
16132
  super(ctx, env);
16096
- ctx.setWebSocketAutoResponse(
16097
- new WebSocketRequestResponsePair("stream_ping", "stream_pong")
16098
- );
16133
+ if (typeof WebSocketRequestResponsePair !== "undefined") {
16134
+ ctx.setWebSocketAutoResponse(
16135
+ new WebSocketRequestResponsePair("stream_ping", "stream_pong")
16136
+ );
16137
+ }
16099
16138
  this.alarmQueue = new AlarmQueue(ctx);
16100
16139
  this.alarmQueue.setExecutor(
16101
16140
  async (method, args, itemId) => {
@@ -19460,14 +19499,13 @@ ${resultContent}${attachmentSummary}`;
19460
19499
  async setExecutionOwner(owner) {
19461
19500
  await this.setValue(EXECUTION_OWNER_KEY, owner);
19462
19501
  }
19463
- /** Whether the given client id has a live bridge socket (excluding `except`). */
19502
+ /** Whether the given client id has an open bridge socket (excluding `except`).
19503
+ * Idle bridge heartbeats are handled by Cloudflare without waking this object,
19504
+ * so timestamp liveness is intentionally enforced only for an in-flight call. */
19464
19505
  hasLiveBridgeSocketForClient(clientId, except) {
19465
- const now = Date.now();
19466
19506
  for (const ws of this.bridgeSockets) {
19467
19507
  if (ws === except) continue;
19468
19508
  if (ws.readyState !== WebSocket.OPEN) continue;
19469
- const lastSeen = this.bridgeLastSeen.get(ws) ?? 0;
19470
- if (now - lastSeen > BRIDGE_LIVENESS_TIMEOUT_MS) continue;
19471
19509
  if ((this.bridgeClientInfo.get(ws)?.clientId ?? null) === clientId) return true;
19472
19510
  }
19473
19511
  return false;
@@ -19628,22 +19666,19 @@ ${resultContent}${attachmentSummary}`;
19628
19666
  }
19629
19667
  /**
19630
19668
  * Return the bridge socket a forwarded call should be dispatched to, pruning
19631
- * dead ones. A half-open client (idle NAT/proxy drop) keeps a
19632
- * `readyState === OPEN` socket that never fires close/error, so we also treat
19633
- * a socket we haven't heard from within {@link BRIDGE_LIVENESS_TIMEOUT_MS} as
19634
- * dead. The CLI heartbeats every few seconds, so this only trips on a truly
19635
- * gone client — and it means we won't dispatch a forwarded call into the void.
19669
+ * closed ones. Idle clients use Cloudflare's hibernation auto-response, so
19670
+ * their frames deliberately do not update `bridgeLastSeen`. Once a call is
19671
+ * dispatched, the client switches to application-visible pings and the
19672
+ * in-flight liveness timer above detects a half-open connection.
19636
19673
  *
19637
19674
  * With an execution owner recorded, only the owner's socket (or, as a
19638
19675
  * back-compat fallback, an unidentified legacy socket) is eligible; with no
19639
19676
  * owner, the first live socket wins as it always has.
19640
19677
  */
19641
19678
  getOpenBridgeSocket(ownerClientId) {
19642
- const now = Date.now();
19643
19679
  const live = [];
19644
19680
  for (const ws of Array.from(this.bridgeSockets)) {
19645
- const lastSeen = this.bridgeLastSeen.get(ws) ?? 0;
19646
- if (ws.readyState === WebSocket.OPEN && now - lastSeen <= BRIDGE_LIVENESS_TIMEOUT_MS) {
19681
+ if (ws.readyState === WebSocket.OPEN) {
19647
19682
  live.push({ socket: ws, clientId: this.bridgeClientInfo.get(ws)?.clientId ?? null });
19648
19683
  continue;
19649
19684
  }
@@ -22317,9 +22352,11 @@ var DurableAgentBuilder = class extends DurableObject {
22317
22352
  eventSockets = /* @__PURE__ */ new Set();
22318
22353
  constructor(ctx, env) {
22319
22354
  super(ctx, env);
22320
- ctx.setWebSocketAutoResponse(
22321
- new WebSocketRequestResponsePair("stream_ping", "stream_pong")
22322
- );
22355
+ if (typeof WebSocketRequestResponsePair !== "undefined") {
22356
+ ctx.setWebSocketAutoResponse(
22357
+ new WebSocketRequestResponsePair("stream_ping", "stream_pong")
22358
+ );
22359
+ }
22323
22360
  const sockets = ctx.getWebSockets();
22324
22361
  for (const ws of sockets) {
22325
22362
  const attachment = ws.deserializeAttachment();