@opengeni/worker-bundle 1.3.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/activities/agent-turn/errors.d.ts +1 -13
  2. package/dist/activities/agent-turn/sandbox-provision.d.ts +22 -0
  3. package/dist/activities/agent-turn/subscription-lease.d.ts +1 -0
  4. package/dist/activities/agent-turn/tool-policy.d.ts +10 -1
  5. package/dist/activities/agent-turn/turn-context.d.ts +6 -0
  6. package/dist/activities/agent-turn/web-search.d.ts +37 -0
  7. package/dist/activities/codemode-dispatcher.d.ts +8 -0
  8. package/dist/activities/sandbox-lease.d.ts +4 -0
  9. package/dist/{activities-control-PEXBWKQJ.js → activities-control-AKJZHDTY.js} +53 -13
  10. package/dist/activities-control-AKJZHDTY.js.map +1 -0
  11. package/dist/{activities-turn-W27A2SBO.js → activities-turn-AOXNE6BM.js} +5984 -5571
  12. package/dist/activities-turn-AOXNE6BM.js.map +1 -0
  13. package/dist/{chunk-XONQEVAD.js → chunk-DCQ7XTXW.js} +108 -3
  14. package/dist/chunk-DCQ7XTXW.js.map +1 -0
  15. package/dist/{chunk-QUX5LT2B.js → chunk-WVZQJBGD.js} +166 -45
  16. package/dist/chunk-WVZQJBGD.js.map +1 -0
  17. package/dist/index.js +2 -2
  18. package/dist/observability-metrics.d.ts +51 -13
  19. package/dist/sandbox-resume.d.ts +3 -0
  20. package/package.json +21 -21
  21. package/src/activities/agent-turn/agent-build.ts +4 -2
  22. package/src/activities/agent-turn/errors.ts +89 -10
  23. package/src/activities/agent-turn/finalization.ts +16 -0
  24. package/src/activities/agent-turn/governance-model.ts +8 -8
  25. package/src/activities/agent-turn/run.ts +12 -0
  26. package/src/activities/agent-turn/sandbox-establish.ts +87 -80
  27. package/src/activities/agent-turn/sandbox-provision.ts +51 -0
  28. package/src/activities/agent-turn/sandbox-runtime.ts +16 -0
  29. package/src/activities/agent-turn/stream-attempt.ts +36 -15
  30. package/src/activities/agent-turn/subscription-lease.ts +10 -1
  31. package/src/activities/agent-turn/tool-environment.ts +19 -0
  32. package/src/activities/agent-turn/tool-policy.ts +25 -0
  33. package/src/activities/agent-turn/turn-context.ts +8 -0
  34. package/src/activities/agent-turn/web-search.ts +381 -0
  35. package/src/activities/codemode-dispatcher.ts +57 -8
  36. package/src/activities/connector-attachments.ts +3 -1
  37. package/src/activities/sandbox-lease.ts +63 -10
  38. package/src/activity-services.ts +8 -0
  39. package/src/observability-metrics.ts +130 -17
  40. package/src/sandbox-resume.ts +24 -5
  41. package/src/sandbox-routing.ts +70 -24
  42. package/dist/activities-control-PEXBWKQJ.js.map +0 -1
  43. package/dist/activities-turn-W27A2SBO.js.map +0 -1
  44. package/dist/chunk-QUX5LT2B.js.map +0 -1
  45. package/dist/chunk-XONQEVAD.js.map +0 -1
@@ -66,6 +66,7 @@ import {
66
66
  classifySandboxLogicalProvisionFailure,
67
67
  isLazySandboxProvisionRetryable,
68
68
  createTurnSandboxProvisioner,
69
+ trackPhysicalSandboxResume,
69
70
  } from "./sandbox-provision";
70
71
  import {
71
72
  resolveActiveSandboxBackend,
@@ -645,40 +646,43 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
645
646
  const lazyHolderId = sandboxState.sandboxHolderId;
646
647
  const lazyGroupId = sandboxState.sandboxGroupId;
647
648
  sandboxState.resumeManagedGroupBox = () =>
648
- resumeBoxForTurn(
649
- {
650
- db,
651
- settings: runSettings,
652
- logicalFallbackSettings: logicalSandboxSettings,
653
- cancellationSignal: sandboxResumeSignal,
654
- sandboxMetrics: runtimeMetricsHooksForObservability(observability),
655
- observability,
656
- freshSandboxReadinessReplacementBudget,
657
- onSandboxLost: publishSandboxLost,
658
- objectStorage,
659
- bus,
660
- },
661
- {
662
- accountId: input.accountId,
663
- workspaceId: input.workspaceId,
664
- sandboxGroupId: lazyGroupId,
665
- sessionId: input.sessionId,
666
- backend: groupBoxBackend,
667
- os: groupBoxOs,
668
- environment: sandboxEnvironment,
669
- ...(groupBoxImage
670
- ? {
671
- image: groupBoxImage,
672
- imagePolicy: groupBoxImagePolicy,
673
- }
674
- : {}),
675
- // The lazy acquire must enforce the same frozen rig authority
676
- // as the eager acquire; otherwise a warm box for another rig
677
- // can bypass the shared-state conflict/rotation fence.
678
- ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
679
- },
680
- "turn",
681
- lazyHolderId,
649
+ trackPhysicalSandboxResume(
650
+ sandboxState.inFlightSandboxResumes,
651
+ resumeBoxForTurn(
652
+ {
653
+ db,
654
+ settings: runSettings,
655
+ logicalFallbackSettings: logicalSandboxSettings,
656
+ cancellationSignal: sandboxResumeSignal,
657
+ sandboxMetrics: runtimeMetricsHooksForObservability(observability),
658
+ observability,
659
+ freshSandboxReadinessReplacementBudget,
660
+ onSandboxLost: publishSandboxLost,
661
+ objectStorage,
662
+ bus,
663
+ },
664
+ {
665
+ accountId: input.accountId,
666
+ workspaceId: input.workspaceId,
667
+ sandboxGroupId: lazyGroupId,
668
+ sessionId: input.sessionId,
669
+ backend: groupBoxBackend,
670
+ os: groupBoxOs,
671
+ environment: sandboxEnvironment,
672
+ ...(groupBoxImage
673
+ ? {
674
+ image: groupBoxImage,
675
+ imagePolicy: groupBoxImagePolicy,
676
+ }
677
+ : {}),
678
+ // The lazy acquire must enforce the same frozen rig authority
679
+ // as the eager acquire; otherwise a warm box for another rig
680
+ // can bypass the shared-state conflict/rotation fence.
681
+ ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
682
+ },
683
+ "turn",
684
+ lazyHolderId,
685
+ ),
682
686
  );
683
687
  if (
684
688
  shouldPrefetchManagedSandbox({
@@ -725,52 +729,55 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
725
729
  );
726
730
  try {
727
731
  sandboxState.resolvedSandbox = await waitForTurnOperation(
728
- resumeBoxForTurn(
729
- {
730
- db,
731
- settings: runSettings,
732
- logicalFallbackSettings: logicalSandboxSettings,
733
- cancellationSignal: sandboxResumeSignal,
734
- sandboxMetrics: runtimeMetricsHooksForObservability(observability),
735
- observability,
736
- freshSandboxReadinessReplacementBudget,
737
- onSandboxLost: publishSandboxLost,
738
- objectStorage,
739
- bus,
740
- },
741
- {
742
- accountId: input.accountId,
743
- workspaceId: input.workspaceId,
744
- sandboxGroupId: session.sandboxGroupId,
745
- sessionId: input.sessionId,
746
- // groupBoxBackend, not turn.sandboxBackend: a machine-home turn that
747
- // is not machine-primary resumes a real cloud group box (the
748
- // deployment default), never a "selfhosted" box (which would throw
749
- // for lack of a bound agentId).
750
- backend: groupBoxBackend,
751
- os: groupBoxOs,
752
- environment: sandboxEnvironment,
753
- // Deployment/workspace pins select NEW creates only. The lease
754
- // retains a live group's image through between-turn repins, even
755
- // with other holders; only rotation/reaping elects a new image.
756
- // Select the image for the actual group-box backend; a configured Modal image must
757
- // never override a Docker run. The selfhosted branch
758
- // (establishSelfhostedTurnSession) NEVER passes
759
- // an image — B3 lives only on this managed-box branch.
760
- ...(groupBoxImage
761
- ? {
762
- image: groupBoxImage,
763
- imagePolicy: groupBoxImagePolicy,
764
- }
765
- : {}),
766
- // RIG IS SHARED STATE (M3): stamp the frozen rig version so the lease
767
- // conflicts on a live shared box set up under a different rig (solo
768
- // durable rotation / N-holders SandboxRigConflictError). Omitted for a
769
- // rig-less turn -> never stamped or enforced (shares exactly as today).
770
- ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
771
- },
772
- "turn",
773
- managedOwnership!.holderId,
732
+ trackPhysicalSandboxResume(
733
+ sandboxState.inFlightSandboxResumes,
734
+ resumeBoxForTurn(
735
+ {
736
+ db,
737
+ settings: runSettings,
738
+ logicalFallbackSettings: logicalSandboxSettings,
739
+ cancellationSignal: sandboxResumeSignal,
740
+ sandboxMetrics: runtimeMetricsHooksForObservability(observability),
741
+ observability,
742
+ freshSandboxReadinessReplacementBudget,
743
+ onSandboxLost: publishSandboxLost,
744
+ objectStorage,
745
+ bus,
746
+ },
747
+ {
748
+ accountId: input.accountId,
749
+ workspaceId: input.workspaceId,
750
+ sandboxGroupId: session.sandboxGroupId,
751
+ sessionId: input.sessionId,
752
+ // groupBoxBackend, not turn.sandboxBackend: a machine-home turn that
753
+ // is not machine-primary resumes a real cloud group box (the
754
+ // deployment default), never a "selfhosted" box (which would throw
755
+ // for lack of a bound agentId).
756
+ backend: groupBoxBackend,
757
+ os: groupBoxOs,
758
+ environment: sandboxEnvironment,
759
+ // Deployment/workspace pins select NEW creates only. The lease
760
+ // retains a live group's image through between-turn repins, even
761
+ // with other holders; only rotation/reaping elects a new image.
762
+ // Select the image for the actual group-box backend; a configured Modal image must
763
+ // never override a Docker run. The selfhosted branch
764
+ // (establishSelfhostedTurnSession) NEVER passes
765
+ // an image — B3 lives only on this managed-box branch.
766
+ ...(groupBoxImage
767
+ ? {
768
+ image: groupBoxImage,
769
+ imagePolicy: groupBoxImagePolicy,
770
+ }
771
+ : {}),
772
+ // RIG IS SHARED STATE (M3): stamp the frozen rig version so the lease
773
+ // conflicts on a live shared box set up under a different rig (solo
774
+ // durable rotation / N-holders SandboxRigConflictError). Omitted for a
775
+ // rig-less turn -> never stamped or enforced (shares exactly as today).
776
+ ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
777
+ },
778
+ "turn",
779
+ managedOwnership!.holderId,
780
+ ),
774
781
  ),
775
782
  sandboxResumeSignal,
776
783
  releaseLateSandbox,
@@ -153,6 +153,57 @@ export async function waitForTurnOperation<T>(
153
153
  }
154
154
  }
155
155
 
156
+ /**
157
+ * Upper bound for joining a cancelled attempt's in-flight provider establish.
158
+ * Worker shutdown cancels activities 5s after SIGTERM and force-stops 100s
159
+ * after it; this leaves room for the remaining finalization steps.
160
+ */
161
+ export const PHYSICAL_SANDBOX_RESUME_DRAIN_TIMEOUT_MS = 60_000;
162
+
163
+ /** Register the PHYSICAL lease acquire/provider establish promise, not the
164
+ * cancellable wrapper awaiting it. */
165
+ export function trackPhysicalSandboxResume<T>(
166
+ inFlight: Set<Promise<unknown>>,
167
+ operation: Promise<T>,
168
+ ): Promise<T> {
169
+ inFlight.add(operation);
170
+ const settle = (): void => {
171
+ inFlight.delete(operation);
172
+ };
173
+ operation.then(settle, settle);
174
+ return operation;
175
+ }
176
+
177
+ /**
178
+ * Join every lease acquire/provider establish this attempt started, even after
179
+ * the turn-facing wrapper rejected on cancellation. An establish has no
180
+ * portable abort: after cancellation it still finishes its own exact cleanup
181
+ * (terminate an unpublished box and roll warming back to cold, or keep a
182
+ * published box and drop its holder). If the activity returned first, a
183
+ * shutting-down worker exited mid-cleanup and stranded a warming lease naming a
184
+ * terminated box; the replacement attempt then waited out the warming TTL and
185
+ * inherited a lost-workspace verdict. This wait is deliberately independent of
186
+ * the finalizer cancellation signal (cancellation is what started it) and is
187
+ * bounded so an uncooperative provider call cannot pin the activity.
188
+ */
189
+ export async function drainPhysicalSandboxResumes(
190
+ inFlight: Set<Promise<unknown>>,
191
+ timeoutMs: number = PHYSICAL_SANDBOX_RESUME_DRAIN_TIMEOUT_MS,
192
+ ): Promise<"none" | "settled" | "timed_out"> {
193
+ if (inFlight.size === 0) return "none";
194
+ let timer: ReturnType<typeof setTimeout> | undefined;
195
+ try {
196
+ return await Promise.race([
197
+ Promise.allSettled([...inFlight]).then(() => "settled" as const),
198
+ new Promise<"timed_out">((resolve) => {
199
+ timer = setTimeout(() => resolve("timed_out"), Math.max(1, timeoutMs));
200
+ }),
201
+ ]);
202
+ } finally {
203
+ if (timer) clearTimeout(timer);
204
+ }
205
+ }
206
+
156
207
  export function sleep(ms: number): Promise<void> {
157
208
  return new Promise((resolve) => setTimeout(resolve, ms));
158
209
  }
@@ -5,6 +5,7 @@ import {
5
5
  readLease,
6
6
  accrueWarmSeconds,
7
7
  SandboxWorkspaceMutationFencedError,
8
+ SandboxWorkspaceMutationOutputRejectedError,
8
9
  retainWorkspaceMutationProcess,
9
10
  retainedProviderCommandPersistence,
10
11
  SandboxRetainedProcessPromotionFencedError,
@@ -12,6 +13,7 @@ import {
12
13
  import { createProviderCommandRetainer } from "@opengeni/db/retained-provider-commands";
13
14
  import {
14
15
  RoutingMutationOutcomeUnknownError,
16
+ RoutingMutationOutputRejectedError,
15
17
  ProviderCommandStartOutcomeUnknownError,
16
18
  ProviderCommandObservationUnavailableError,
17
19
  isModalCommandStartOutcomeUnknownError,
@@ -622,6 +624,20 @@ export function createSandboxTurnRuntime(deps: SandboxTurnRuntimeDeps) {
622
624
  try {
623
625
  await settleMutation("resolved");
624
626
  } catch (settlementError) {
627
+ if (
628
+ settlementError instanceof SandboxWorkspaceMutationOutputRejectedError &&
629
+ settlementError.matchesPhysicalSettlement({
630
+ accountId: identity.accountId,
631
+ workspaceId: identity.workspaceId,
632
+ admission,
633
+ operation,
634
+ outcome: "resolved",
635
+ })
636
+ ) {
637
+ throw new RoutingMutationOutputRejectedError(operation, settlementError.code, {
638
+ cause: settlementError,
639
+ });
640
+ }
625
641
  throw new RoutingMutationOutcomeUnknownError(
626
642
  operation,
627
643
  `Platform workspace mutation "${operation}" returned from the provider but lost its durable settlement fence; its outcome is unknown and it was not replayed`,
@@ -79,6 +79,7 @@ import {
79
79
  isCompletedGeneratedImageSdkEvent,
80
80
  } from "../generated-images";
81
81
  import { programmaticApproval, programmaticContinuationNote } from "../programmatic-approvals";
82
+ import { CodemodeAttemptDispatcher } from "../codemode-dispatcher";
82
83
  import { ToolResultSpill } from "./tool-result-spill";
83
84
  import { ownedTurnSandboxForAgent } from "./turn-sandbox-access";
84
85
  import { createTurnCredentialLeases } from "./credential-leases";
@@ -661,6 +662,16 @@ export async function runTurnStreamAttempt(
661
662
  const providerPublishesNativeRequestEvents =
662
663
  resolvedModel?.provider.kind === "codex-subscription" ||
663
664
  resolvedModel?.provider.kind === "xai-subscription";
665
+ // Bounded provider label for the streaming SLIs — the resolved registry
666
+ // provider id (or the built-in OpenAI/Azure provider), never a raw
667
+ // user-supplied model string. Created before the stream starts so the
668
+ // first request's model entry and provider dispatch are observed.
669
+ const streamTiming = new StreamTimingMetrics(observability, {
670
+ provider: streamProvider,
671
+ });
672
+ // Native Codex/SuperGrok transports report their literal dispatch through
673
+ // the shared turn context; generic transports use the wire-dispatch clock.
674
+ eventing.providerDispatchObserver = () => streamTiming.onProviderDispatch();
664
675
  let fallbackProviderRequestStartedAt: number | null = null;
665
676
  let fallbackProviderRequestLifecycleStartedAt: number | null = null;
666
677
  const recordFallbackProviderDispatchAtWire = async (): Promise<void> => {
@@ -853,7 +864,10 @@ export async function runTurnStreamAttempt(
853
864
  }
854
865
  attempt.modelRequestStarted = true;
855
866
  return await runtime.runStream(agent, runInput!, eventing.modelRunSettings, {
856
- beforeModelRequest: modelCallAdmission.beforeModelRequest,
867
+ beforeModelRequest: () => {
868
+ streamTiming.onModelRequestEntry();
869
+ return modelCallAdmission.beforeModelRequest();
870
+ },
857
871
  onModelResponse: (event) => {
858
872
  responseCreditPolicyRevisions.add(creditPolicyRevision);
859
873
  return modelCallAdmission.onModelResponse(event);
@@ -965,6 +979,7 @@ export async function runTurnStreamAttempt(
965
979
  ? {
966
980
  onModelTransportStarted: recordFallbackProviderDispatchAtWire,
967
981
  onModelTransportDispatched: (clock) => {
982
+ streamTiming.onProviderDispatch();
968
983
  eventing.initialModelWireDispatch.record(
969
984
  { provider: streamProvider, dispatchId },
970
985
  clock,
@@ -1006,12 +1021,6 @@ export async function runTurnStreamAttempt(
1006
1021
  modelCallAdmission.close();
1007
1022
  throw error;
1008
1023
  }
1009
- // Bounded provider label for the streaming SLIs — the resolved registry
1010
- // provider id (or the built-in OpenAI/Azure provider), never a raw
1011
- // user-supplied model string.
1012
- const streamTiming = new StreamTimingMetrics(observability, {
1013
- provider: streamProvider,
1014
- });
1015
1024
  eventing.batcher = createRuntimeBatcher(
1016
1025
  async (events) => {
1017
1026
  await eventing.publish!(events);
@@ -1960,14 +1969,26 @@ export async function runTurnStreamAttempt(
1960
1969
 
1961
1970
  // Recover committed waits/results before any model dispatch, including crash replacement.
1962
1971
  // A new attempt uses its own signed caller; origin provenance never changes.
1963
- await eventing.toolPreparationReady;
1964
- const codemodeOperations =
1965
- (await eventing.codemodeDispatcher?.resumeApproved(
1966
- `sandbox:${input.attemptId}`,
1967
- trigger.type === "user.approvalDecision"
1968
- ? (trigger.payload as { approvalId: string }).approvalId
1969
- : undefined,
1970
- )) ?? [];
1972
+ // Join-on-use: lazy MCP preparation is awaited only when stored work must resume.
1973
+ const codemodeOperations = await CodemodeAttemptDispatcher.resumeApproved(
1974
+ db,
1975
+ {
1976
+ accountId: input.accountId,
1977
+ workspaceId: input.workspaceId,
1978
+ sessionId: input.sessionId,
1979
+ turnId: activeTurnId,
1980
+ attemptId: input.attemptId,
1981
+ executionGeneration: attempt.executionGeneration,
1982
+ },
1983
+ `sandbox:${input.attemptId}`,
1984
+ trigger.type === "user.approvalDecision"
1985
+ ? (trigger.payload as { approvalId: string }).approvalId
1986
+ : undefined,
1987
+ async () => {
1988
+ await eventing.toolPreparationReady;
1989
+ return eventing.codemodeDispatcher;
1990
+ },
1991
+ );
1971
1992
  codemodeContinuationNote = programmaticContinuationNote(codemodeOperations);
1972
1993
  programmaticApprovalAcknowledged =
1973
1994
  trigger.type === "user.approvalDecision" &&
@@ -1,6 +1,8 @@
1
1
  export type LeaseRenewReason = "timer" | "runtime_event" | "model_usage";
2
2
  export type LeaseLossReason = "deadline" | "not_found";
3
3
 
4
+ const HEARTBEAT_INTERVAL_MS = 60_000;
5
+
4
6
  type LeaseIdentity = { turnId: string; holderId: string; generation: number };
5
7
  export type SubscriptionLeaseDeps = {
6
8
  ttlMs: number;
@@ -24,11 +26,13 @@ export class SubscriptionTurnLease {
24
26
  private heartbeatInFlight = false;
25
27
  private heartbeatTimer: ReturnType<typeof setInterval> | undefined;
26
28
  private readonly now: () => number;
29
+ private readonly renewalIntervalMs: number;
27
30
 
28
31
  constructor(private readonly leaseDeps: SubscriptionLeaseDeps) {
29
32
  if (!Number.isFinite(leaseDeps.ttlMs) || leaseDeps.ttlMs <= 0)
30
33
  throw new Error("Subscription lease TTL must be positive");
31
34
  this.now = leaseDeps.now ?? (() => performance.now());
35
+ this.renewalIntervalMs = Math.min(HEARTBEAT_INTERVAL_MS, leaseDeps.ttlMs / 5);
32
36
  }
33
37
 
34
38
  private expired = (deadline: number | null): boolean =>
@@ -62,6 +66,11 @@ export class SubscriptionTurnLease {
62
66
  return;
63
67
  }
64
68
  if (this.heartbeatInFlight) return;
69
+ // Acquisition and successful renewal confirm ownership from request start.
70
+ // Reuse that proof between heartbeats; a failed renewal leaves it unchanged
71
+ // so the next checkpoint can retry immediately. Expiry is checked first.
72
+ const renewAt = this.confirmedUntilMs! - this.leaseDeps.ttlMs + this.renewalIntervalMs;
73
+ if (this.now() < renewAt) return;
65
74
  this.heartbeatInFlight = true;
66
75
  const priorDeadline = this.confirmedUntilMs;
67
76
  const startedAt = this.now();
@@ -103,7 +112,7 @@ export class SubscriptionTurnLease {
103
112
 
104
113
  startHeartbeat = (): void => {
105
114
  if (!this.leaseDeps.getTurnId() || this.heartbeatTimer) return;
106
- this.heartbeatTimer = setInterval(() => void this.renew("timer"), 60_000);
115
+ this.heartbeatTimer = setInterval(() => void this.renew("timer"), HEARTBEAT_INTERVAL_MS);
107
116
  this.heartbeatTimer.unref?.();
108
117
  };
109
118
 
@@ -67,6 +67,7 @@ import {
67
67
  scheduledTurnMcpServerIds,
68
68
  hasPermission,
69
69
  requireExplicitPermissionDelegation,
70
+ createWebSearchBilling,
70
71
  } from "@opengeni/core";
71
72
  import { loadWorkspaceEnvironmentForRunWithCredentials } from "../environment";
72
73
  import { withFirstPartyTools } from "../goals";
@@ -116,6 +117,7 @@ import { resolveTurnSandboxAccess } from "./turn-sandbox-access";
116
117
  import { createListModelsAttemptToolDefinition } from "./list-models";
117
118
  import { createRefreshCredentialsAttemptToolDefinition } from "./refresh-credentials";
118
119
  import { codeSearchToolDefinitions, codeSearchWorkspaceFromChannel } from "./code-search";
120
+ import { turnWebSearchPlan, webSearchToolDefinitions } from "./web-search";
119
121
  import { createWorkspaceSkillTools } from "./skill-tools";
120
122
  import { loadConfiguredBundledSkills } from "./skill-selection";
121
123
  import { guardSkillFilesystem } from "./skill-transfer";
@@ -886,6 +888,22 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
886
888
  const attemptToolFamilies = resolveAgentToolFamilies(session.agent, {
887
889
  hasSkills: skillCatalog.length > 0,
888
890
  });
891
+ // Provider web search: only where the turn has no hosted search (or the
892
+ // operator chose `replace`), only when a provider is configured, and only
893
+ // when the session's agent configuration allows web search (filtered below).
894
+ const webSearchTools = webSearchToolDefinitions({
895
+ settings: runSettings,
896
+ tools: turnWebSearchPlan(resolvedModel, runSettings).providerTools,
897
+ scope: {
898
+ accountId: input.accountId,
899
+ workspaceId: input.workspaceId,
900
+ sessionId: input.sessionId,
901
+ turnId: turn.id,
902
+ attemptId: input.attemptId,
903
+ },
904
+ billing: createWebSearchBilling({ db, settings: runSettings }),
905
+ observability,
906
+ });
889
907
  const attemptToolDefinitions = [
890
908
  ...(operationReadStore
891
909
  ? [
@@ -976,6 +994,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
976
994
  ? [googleDrivePublicationTool]
977
995
  : []),
978
996
  ...codeSearchTools,
997
+ ...webSearchTools,
979
998
  ].filter((tool) => attemptToolFamilies.allowsFunctionTool(tool.modelName));
980
999
  recordTurnStartupPhase(observability, {
981
1000
  phase: "tool_context_preparation",
@@ -2,8 +2,11 @@ import {
2
2
  desktopCapableBackend,
3
3
  type ComputerToolMode,
4
4
  type LazyToolTransport,
5
+ type OpenGeniRuntime,
5
6
  } from "@opengeni/runtime";
7
+ import type { TurnExecutionPolicyV1 } from "@opengeni/contracts";
6
8
  import {
9
+ configuredModelForAcceptedTurnExecutionPolicy,
7
10
  isDirectOpenAiApiBaseUrl,
8
11
  type ModelProviderApi,
9
12
  type ResolvedModelProvider,
@@ -208,6 +211,28 @@ export function shouldDeferNonEagerToolPreparation(args: {
208
211
  );
209
212
  }
210
213
 
214
+ /**
215
+ * Resolve the provider routing/gating shape for an accepted turn. The current
216
+ * definition is projected back onto the verified frozen policy: a turn
217
+ * accepted before hosted web search was enabled keeps its frozen tool set on
218
+ * every recovery attempt (stable tool prefix, exact accepted definition); the
219
+ * next accepted logical turn resolves the newly enabled tool.
220
+ */
221
+ export function resolveAcceptedTurnModel(
222
+ runtime: Pick<OpenGeniRuntime, "resolveTurnModel">,
223
+ settings: Settings,
224
+ policy: TurnExecutionPolicyV1,
225
+ ): ReturnType<OpenGeniRuntime["resolveTurnModel"]> {
226
+ const current = runtime.resolveTurnModel(settings, policy.productModelId);
227
+ if (!current) return null;
228
+ const configured = configuredModelForAcceptedTurnExecutionPolicy(
229
+ current.configured,
230
+ current.provider,
231
+ policy,
232
+ );
233
+ return configured === current.configured ? current : { ...current, configured };
234
+ }
235
+
211
236
  /**
212
237
  * Native web search is a runtime capability, not part of the session's MCP
213
238
  * allow-list. Attach it whenever the resolved provider advertises runnable
@@ -103,6 +103,9 @@ export type SandboxRuntimeState = {
103
103
  }>;
104
104
  turnSandboxProvisioner: TurnSandboxProvisioner<ResumedTurnSandbox> | null;
105
105
  resumeManagedGroupBox: (() => Promise<ResumedTurnSandbox>) | null;
106
+ /** Physical resumeBoxForTurn promises still running for this attempt; the
107
+ * finalizer joins them so a cancelled establish completes its own cleanup. */
108
+ inFlightSandboxResumes: Set<Promise<unknown>>;
106
109
  prefetchedManagedBox: Promise<ResumedTurnSandbox> | null;
107
110
  prefetchedManagedBoxResult: ResumedTurnSandbox | null;
108
111
  setupBoxSession: unknown;
@@ -153,6 +156,9 @@ export type EventingState = {
153
156
  firstModelRequestPreparationRecorded: boolean;
154
157
  firstModelRequestCheckpointAt: number | null;
155
158
  initialModelWireDispatch: InitialModelWireDispatchClock;
159
+ /** Diagnostic only: the current stream's timing hook for a native transport's
160
+ * literal provider dispatch. Never joins, fences or fails the request. */
161
+ providerDispatchObserver: (() => void) | null;
156
162
  companyBrainContextContributions: readonly ModelContextContributionSummary[] | null;
157
163
  /** Skill ids in this turn's frozen, model-visible Skill index; telemetry only. */
158
164
  modelVisibleSkillIds: ReadonlySet<string> | null;
@@ -259,6 +265,7 @@ export function createTurnContext(input: {
259
265
  firstModelPreparationNestedSandboxPhases: [],
260
266
  turnSandboxProvisioner: null,
261
267
  resumeManagedGroupBox: null,
268
+ inFlightSandboxResumes: new Set(),
262
269
  prefetchedManagedBox: null,
263
270
  prefetchedManagedBoxResult: null,
264
271
  setupBoxSession: null,
@@ -301,6 +308,7 @@ export function createTurnContext(input: {
301
308
  firstModelRequestPreparationRecorded: false,
302
309
  firstModelRequestCheckpointAt: null,
303
310
  initialModelWireDispatch: new InitialModelWireDispatchClock(),
311
+ providerDispatchObserver: null,
304
312
  companyBrainContextContributions: null,
305
313
  modelVisibleSkillIds: null,
306
314
  },