@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.
- package/dist/activities/agent-turn/errors.d.ts +1 -13
- package/dist/activities/agent-turn/sandbox-provision.d.ts +22 -0
- package/dist/activities/agent-turn/subscription-lease.d.ts +1 -0
- package/dist/activities/agent-turn/tool-policy.d.ts +10 -1
- package/dist/activities/agent-turn/turn-context.d.ts +6 -0
- package/dist/activities/agent-turn/web-search.d.ts +37 -0
- package/dist/activities/codemode-dispatcher.d.ts +8 -0
- package/dist/activities/sandbox-lease.d.ts +4 -0
- package/dist/{activities-control-PEXBWKQJ.js → activities-control-AKJZHDTY.js} +53 -13
- package/dist/activities-control-AKJZHDTY.js.map +1 -0
- package/dist/{activities-turn-W27A2SBO.js → activities-turn-AOXNE6BM.js} +5984 -5571
- package/dist/activities-turn-AOXNE6BM.js.map +1 -0
- package/dist/{chunk-XONQEVAD.js → chunk-DCQ7XTXW.js} +108 -3
- package/dist/chunk-DCQ7XTXW.js.map +1 -0
- package/dist/{chunk-QUX5LT2B.js → chunk-WVZQJBGD.js} +166 -45
- package/dist/chunk-WVZQJBGD.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/observability-metrics.d.ts +51 -13
- package/dist/sandbox-resume.d.ts +3 -0
- package/package.json +21 -21
- package/src/activities/agent-turn/agent-build.ts +4 -2
- package/src/activities/agent-turn/errors.ts +89 -10
- package/src/activities/agent-turn/finalization.ts +16 -0
- package/src/activities/agent-turn/governance-model.ts +8 -8
- package/src/activities/agent-turn/run.ts +12 -0
- package/src/activities/agent-turn/sandbox-establish.ts +87 -80
- package/src/activities/agent-turn/sandbox-provision.ts +51 -0
- package/src/activities/agent-turn/sandbox-runtime.ts +16 -0
- package/src/activities/agent-turn/stream-attempt.ts +36 -15
- package/src/activities/agent-turn/subscription-lease.ts +10 -1
- package/src/activities/agent-turn/tool-environment.ts +19 -0
- package/src/activities/agent-turn/tool-policy.ts +25 -0
- package/src/activities/agent-turn/turn-context.ts +8 -0
- package/src/activities/agent-turn/web-search.ts +381 -0
- package/src/activities/codemode-dispatcher.ts +57 -8
- package/src/activities/connector-attachments.ts +3 -1
- package/src/activities/sandbox-lease.ts +63 -10
- package/src/activity-services.ts +8 -0
- package/src/observability-metrics.ts +130 -17
- package/src/sandbox-resume.ts +24 -5
- package/src/sandbox-routing.ts +70 -24
- package/dist/activities-control-PEXBWKQJ.js.map +0 -1
- package/dist/activities-turn-W27A2SBO.js.map +0 -1
- package/dist/chunk-QUX5LT2B.js.map +0 -1
- 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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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:
|
|
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
|
-
|
|
1964
|
-
const codemodeOperations =
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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"),
|
|
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
|
},
|