@opengeni/worker-bundle 1.2.1-canary.37222965366001 → 1.4.0
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/subscription-lease.d.ts +1 -0
- package/dist/activities/agent-turn/tool-policy.d.ts +10 -1
- package/dist/activities/agent-turn/web-search.d.ts +33 -0
- package/dist/activities/codemode-dispatcher.d.ts +6 -2
- package/dist/activities/open-suffix-resume.d.ts +2 -0
- package/dist/activities/programmatic-approvals.d.ts +27 -0
- package/dist/activities/run-input.d.ts +2 -0
- package/dist/{activities-control-QSDPXLLD.js → activities-control-D5KVEPHI.js} +3 -3
- package/dist/{activities-turn-BMY3CDKL.js → activities-turn-4Z3M7Z3F.js} +554 -113
- package/dist/activities-turn-4Z3M7Z3F.js.map +1 -0
- package/dist/{chunk-JKMG5ZHH.js → chunk-BMTWULF7.js} +115 -31
- package/dist/chunk-BMTWULF7.js.map +1 -0
- package/dist/{chunk-XONQEVAD.js → chunk-FF4HPY7L.js} +1 -1
- package/dist/chunk-FF4HPY7L.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/observability-metrics.d.ts +1 -1
- package/package.json +21 -21
- package/src/activities/agent-turn/agent-build.ts +6 -4
- package/src/activities/agent-turn/errors.ts +82 -3
- package/src/activities/agent-turn/governance-model.ts +8 -8
- package/src/activities/agent-turn/sandbox-runtime.ts +16 -0
- package/src/activities/agent-turn/stream-attempt.ts +100 -21
- package/src/activities/agent-turn/subscription-lease.ts +10 -1
- package/src/activities/agent-turn/tool-environment.ts +20 -0
- package/src/activities/agent-turn/tool-policy.ts +26 -5
- package/src/activities/agent-turn/web-search.ts +252 -0
- package/src/activities/codemode-dispatcher.ts +145 -7
- package/src/activities/connector-attachments.ts +3 -1
- package/src/activities/open-suffix-resume.ts +107 -86
- package/src/activities/programmatic-approvals.ts +52 -0
- package/src/activities/run-input.ts +4 -1
- package/src/activity-services.ts +8 -0
- package/src/observability-metrics.ts +1 -0
- package/src/sandbox-routing.ts +58 -24
- package/dist/activities-turn-BMY3CDKL.js.map +0 -1
- package/dist/chunk-JKMG5ZHH.js.map +0 -1
- package/dist/chunk-XONQEVAD.js.map +0 -1
- /package/dist/{activities-control-QSDPXLLD.js.map → activities-control-D5KVEPHI.js.map} +0 -0
|
@@ -27,8 +27,12 @@ import {
|
|
|
27
27
|
isModalTaskExecStartPreDispatchUnavailableError,
|
|
28
28
|
isModalCommandStartOutcomeUnknownError,
|
|
29
29
|
isProviderCommandObservationUnavailableError,
|
|
30
|
+
ProviderCommandInputOutcomeUnknownError,
|
|
31
|
+
ProviderCommandStartOutcomeUnknownError,
|
|
30
32
|
isRoutingMutationOutcomeUnknownError,
|
|
33
|
+
isRoutingMutationOutputRejectedError,
|
|
31
34
|
RoutingWorkspaceRootChangedError,
|
|
35
|
+
RoutingBackendRecoveryRequiredError,
|
|
32
36
|
ResponsesStreamingTerminalError,
|
|
33
37
|
SandboxMaterializationVerificationError,
|
|
34
38
|
materializationVerificationDiagnostic,
|
|
@@ -380,7 +384,7 @@ export function preClaimAdmissionFailure(error: unknown): ApplicationFailure {
|
|
|
380
384
|
* objects (including array containers), not queue positions/duplicate refs.
|
|
381
385
|
* The separate link ceiling bounds huge duplicate arrays without truncating
|
|
382
386
|
* them into permission. Cycles are harmless; overflow/unreadable edges hold. */
|
|
383
|
-
function
|
|
387
|
+
function structuredRecoveryCauseGraph(error: unknown): Map<object, Set<object>> | null {
|
|
384
388
|
const graph = new Map<object, Set<object>>();
|
|
385
389
|
const queue: object[] = [];
|
|
386
390
|
let links = 0;
|
|
@@ -414,7 +418,7 @@ function retryableDatabaseFailureCode(
|
|
|
414
418
|
requireDatabaseProvenance = false,
|
|
415
419
|
): PostClaimDatabaseRecoveryDetail["code"] | null {
|
|
416
420
|
try {
|
|
417
|
-
const graph =
|
|
421
|
+
const graph = structuredRecoveryCauseGraph(error);
|
|
418
422
|
if (!graph) return null;
|
|
419
423
|
const transports = new Set<object>();
|
|
420
424
|
const boundaries = new Set<object>();
|
|
@@ -429,7 +433,8 @@ function retryableDatabaseFailureCode(
|
|
|
429
433
|
// Ask the canonical transport predicate about ONLY this node's facts. Its
|
|
430
434
|
// recursive search must not pair a DB sibling with an unrelated provider.
|
|
431
435
|
for (const node of graph.keys()) {
|
|
432
|
-
if (isRoutingMutationOutcomeUnknownError(node)
|
|
436
|
+
if (isRoutingMutationOutcomeUnknownError(node) || isRoutingMutationOutputRejectedError(node))
|
|
437
|
+
return null;
|
|
433
438
|
const record = node as Record<string, unknown>;
|
|
434
439
|
if (
|
|
435
440
|
requireDatabaseProvenance
|
|
@@ -742,13 +747,56 @@ export function modelPreparationFailureEventPayload(error: unknown, durationMs:
|
|
|
742
747
|
* only those structural error links with a strict bound; never classify from
|
|
743
748
|
* message text, which could originate in model or tool content.
|
|
744
749
|
*/
|
|
750
|
+
/**
|
|
751
|
+
* Only the home resolver's positive pre-dispatch signal permits a fresh attempt.
|
|
752
|
+
* The same error class can also escape AFTER a provider mutation, so neither its
|
|
753
|
+
* retryable property nor message alone establishes replay safety. Inspect the
|
|
754
|
+
* complete bounded SDK cause graph and let uncertain peer outcomes veto this
|
|
755
|
+
* narrow recovery; unreadable or overflowing graphs fail closed.
|
|
756
|
+
*/
|
|
757
|
+
function isPreDispatchHomeBackendRecoveryRequired(error: unknown): boolean {
|
|
758
|
+
const graph = structuredRecoveryCauseGraph(error);
|
|
759
|
+
if (!graph) return false;
|
|
760
|
+
let found = false;
|
|
761
|
+
try {
|
|
762
|
+
for (const node of graph.keys()) {
|
|
763
|
+
if (
|
|
764
|
+
isRoutingMutationOutcomeUnknownError(node) ||
|
|
765
|
+
isRoutingMutationOutputRejectedError(node) ||
|
|
766
|
+
isRawProviderCommandOutcomeUnknown(node) ||
|
|
767
|
+
isModalCommandStartOutcomeUnknownError(node) ||
|
|
768
|
+
isProviderCommandObservationUnavailableError(node)
|
|
769
|
+
)
|
|
770
|
+
return false;
|
|
771
|
+
if (node instanceof RoutingBackendRecoveryRequiredError) {
|
|
772
|
+
if (
|
|
773
|
+
node.op !== "resolve_home_backend" ||
|
|
774
|
+
!Number.isSafeInteger(node.leaseEpoch) ||
|
|
775
|
+
node.leaseEpoch < 0 ||
|
|
776
|
+
!node.retryable ||
|
|
777
|
+
(node.recovery !== "pending" && node.recovery !== "superseded")
|
|
778
|
+
)
|
|
779
|
+
return false;
|
|
780
|
+
found = true;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
} catch {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
return found;
|
|
787
|
+
}
|
|
788
|
+
|
|
745
789
|
export function sandboxRouteTransitionCode(
|
|
746
790
|
error: unknown,
|
|
747
791
|
):
|
|
748
792
|
| "home_unavailable_this_turn"
|
|
749
793
|
| "workspace_root_changed_this_turn"
|
|
750
794
|
| "native_capabilities_changed_this_attempt"
|
|
795
|
+
| "home_backend_recovery_pending"
|
|
751
796
|
| null {
|
|
797
|
+
if (isPreDispatchHomeBackendRecoveryRequired(error)) {
|
|
798
|
+
return "home_backend_recovery_pending";
|
|
799
|
+
}
|
|
752
800
|
const pending: unknown[] = [error];
|
|
753
801
|
const seen = new WeakSet<object>();
|
|
754
802
|
let inspected = 0;
|
|
@@ -1273,10 +1321,41 @@ function anthropicRequestDiagnostic(error: unknown): AnthropicRequestError | und
|
|
|
1273
1321
|
: undefined;
|
|
1274
1322
|
}
|
|
1275
1323
|
|
|
1324
|
+
function isRawProviderCommandOutcomeUnknown(error: unknown): boolean {
|
|
1325
|
+
try {
|
|
1326
|
+
return (
|
|
1327
|
+
error instanceof ProviderCommandInputOutcomeUnknownError ||
|
|
1328
|
+
error instanceof ProviderCommandStartOutcomeUnknownError
|
|
1329
|
+
);
|
|
1330
|
+
} catch {
|
|
1331
|
+
return false;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1276
1335
|
export function agentRunFailurePayload(
|
|
1277
1336
|
error: unknown,
|
|
1278
1337
|
options: { isCodexTurn?: boolean } = {},
|
|
1279
1338
|
): ReturnType<typeof baseAgentRunFailurePayload> {
|
|
1339
|
+
const graph = structuredRecoveryCauseGraph(error);
|
|
1340
|
+
const nodes = graph ? [...graph.keys()] : [];
|
|
1341
|
+
const outputRejected = nodes.find(isRoutingMutationOutputRejectedError);
|
|
1342
|
+
if (outputRejected) {
|
|
1343
|
+
// A known receipt applies only to its own mutation. Uncertain peers retain
|
|
1344
|
+
// their established terminal classification, regardless of SDK graph order.
|
|
1345
|
+
const unknown = nodes.find(
|
|
1346
|
+
(node) =>
|
|
1347
|
+
isRoutingMutationOutcomeUnknownError(node) ||
|
|
1348
|
+
isModalCommandStartOutcomeUnknownError(node) ||
|
|
1349
|
+
isProviderCommandObservationUnavailableError(node) ||
|
|
1350
|
+
isRawProviderCommandOutcomeUnknown(node),
|
|
1351
|
+
);
|
|
1352
|
+
if (unknown) return { ...baseAgentRunFailurePayload(unknown, options), retryable: false };
|
|
1353
|
+
return {
|
|
1354
|
+
error: outputRejected.message,
|
|
1355
|
+
code: outputRejected.code,
|
|
1356
|
+
retryable: false,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1280
1359
|
const failure = baseAgentRunFailurePayload(error, options);
|
|
1281
1360
|
const diagnostic = materializationVerificationDiagnostic(error);
|
|
1282
1361
|
const anthropic = anthropicRequestDiagnostic(error);
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
lazyToolTransportForTurn,
|
|
60
60
|
openAiHostedImageProviderBindingForTurn,
|
|
61
61
|
modelAttachmentInputPolicyForTurn,
|
|
62
|
+
resolveAcceptedTurnModel,
|
|
62
63
|
} from "./tool-policy";
|
|
63
64
|
|
|
64
65
|
import type { ClaimTurnOk } from "./claim";
|
|
@@ -361,10 +362,11 @@ export async function prepareGovernanceAndModel(
|
|
|
361
362
|
// a chat-only Fireworks model. Resolving against the default-model settings
|
|
362
363
|
// keeps gating consistent with the router. Cost accounting covers registry
|
|
363
364
|
// models via configuredModelPricing.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
365
|
+
//
|
|
366
|
+
// The accepted policy is then projected back onto the resolved shape: a
|
|
367
|
+
// turn frozen before hosted web search was enabled keeps its frozen tool set
|
|
368
|
+
// on every attempt, so recovery never adds a tool mid-turn.
|
|
369
|
+
const resolvedModel = resolveAcceptedTurnModel(runtime, capabilitySettings, turnExecutionPolicy);
|
|
368
370
|
const providerApi = resolvedModel?.provider.api ?? "responses";
|
|
369
371
|
const nativeImageProviderBinding =
|
|
370
372
|
providerApi === "responses"
|
|
@@ -372,10 +374,8 @@ export async function prepareGovernanceAndModel(
|
|
|
372
374
|
: null;
|
|
373
375
|
const lazyToolTransport = lazyToolTransportForTurn(resolvedModel);
|
|
374
376
|
const modelInputPolicy = modelAttachmentInputPolicyForTurn(resolvedModel);
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// image transport for that wire yet; exposing view_image there would turn
|
|
378
|
-
// pixels into a multi-megabyte text/base64 function result.
|
|
377
|
+
// The shared input policy combines model modality with the supported wire
|
|
378
|
+
// transport, including typed image projection for vision-capable Chat models.
|
|
379
379
|
const supportsImageInput = modelInputPolicy.supportsImageInput;
|
|
380
380
|
media.modelCanReceiveRetainedSessionImages = supportsImageInput;
|
|
381
381
|
const attachmentProjector = createModelHistoryAttachmentProjector(
|
|
@@ -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`,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getHumanInputResumeForEvent,
|
|
8
8
|
getSessionHumanInputRequest,
|
|
9
9
|
getWorkspace,
|
|
10
|
+
listTurnCodemodeApprovals,
|
|
10
11
|
recordUsageEvent,
|
|
11
12
|
registerPendingSessionToolCall,
|
|
12
13
|
recordPendingSessionToolCallResult,
|
|
@@ -77,6 +78,11 @@ import {
|
|
|
77
78
|
generatedImageFromSdkEvent,
|
|
78
79
|
isCompletedGeneratedImageSdkEvent,
|
|
79
80
|
} from "../generated-images";
|
|
81
|
+
import {
|
|
82
|
+
programmaticApproval,
|
|
83
|
+
programmaticContinuationNote,
|
|
84
|
+
recoverProgrammaticOperationsBeforeFirstRequest,
|
|
85
|
+
} from "../programmatic-approvals";
|
|
80
86
|
import { ToolResultSpill } from "./tool-result-spill";
|
|
81
87
|
import { ownedTurnSandboxForAgent } from "./turn-sandbox-access";
|
|
82
88
|
import { createTurnCredentialLeases } from "./credential-leases";
|
|
@@ -449,6 +455,8 @@ export async function runTurnStreamAttempt(
|
|
|
449
455
|
await parallelSessionTitle?.cancel();
|
|
450
456
|
};
|
|
451
457
|
let runInput: Awaited<ReturnType<typeof turnInput>>["input"] | null = null;
|
|
458
|
+
let codemodeContinuationNote: string | undefined;
|
|
459
|
+
let programmaticApprovalAcknowledged = false;
|
|
452
460
|
const prepareRunAttemptInput = async () => {
|
|
453
461
|
const historyPreparationStartedAt = performance.now();
|
|
454
462
|
let historyPreparationOutcome: "completed" | "failed" = "completed";
|
|
@@ -461,6 +469,8 @@ export async function runTurnStreamAttempt(
|
|
|
461
469
|
subjectId: fileAuthoritySubjectId,
|
|
462
470
|
},
|
|
463
471
|
recovering: turn.executionGeneration > 1,
|
|
472
|
+
programmaticApprovalAcknowledged,
|
|
473
|
+
...(codemodeContinuationNote ? { codemodeContinuationNote } : {}),
|
|
464
474
|
...(unavailableSandboxFilesNote ? { unavailableSandboxFilesNote } : {}),
|
|
465
475
|
...(runCredentialsNote ? { runCredentialsNote } : {}),
|
|
466
476
|
...(mcpAvailabilityNote ? { mcpAvailabilityNote } : {}),
|
|
@@ -1656,10 +1666,21 @@ export async function runTurnStreamAttempt(
|
|
|
1656
1666
|
});
|
|
1657
1667
|
}
|
|
1658
1668
|
}
|
|
1659
|
-
|
|
1669
|
+
const programmaticPending = (
|
|
1670
|
+
await listTurnCodemodeApprovals(db, {
|
|
1671
|
+
accountId: input.accountId,
|
|
1672
|
+
workspaceId: input.workspaceId,
|
|
1673
|
+
sessionId: input.sessionId,
|
|
1674
|
+
turnId: activeTurnId,
|
|
1675
|
+
})
|
|
1676
|
+
)
|
|
1677
|
+
.filter((operation) => operation.state === "waiting_for_approval")
|
|
1678
|
+
.map(programmaticApproval);
|
|
1679
|
+
if (eventing.stream.interruptions.length > 0 || programmaticPending.length > 0) {
|
|
1660
1680
|
await eventing.preparedTools?.inputWaitYield?.sealForSettlement(runtimeCancellationSignal);
|
|
1661
1681
|
await historySink.reconcileConversationTruth({ requireDurable: true });
|
|
1662
|
-
const
|
|
1682
|
+
const sdkApprovals = runtime.serializeApprovals(eventing.stream.interruptions);
|
|
1683
|
+
const approvals = [...sdkApprovals, ...programmaticPending];
|
|
1663
1684
|
const humanInputInterruptions =
|
|
1664
1685
|
runtime.serializeHumanInputRequests?.(eventing.stream.interruptions) ?? [];
|
|
1665
1686
|
const interactionInterventionInterruptions =
|
|
@@ -1739,28 +1760,34 @@ export async function runTurnStreamAttempt(
|
|
|
1739
1760
|
const interruptionCallIds = interruptionCallIdsFromPause({
|
|
1740
1761
|
humanInputRequests,
|
|
1741
1762
|
interactionInterventionRequests,
|
|
1742
|
-
pendingApprovals
|
|
1763
|
+
pendingApprovals: [
|
|
1764
|
+
...sdkApprovals,
|
|
1765
|
+
...interactionInterventionInterruptions.map((interruption) => interruption.approval),
|
|
1766
|
+
],
|
|
1743
1767
|
});
|
|
1744
1768
|
assertOpenSuffixResumable(suffixMembers, interruptionCallIds);
|
|
1745
1769
|
const suffixByCallId = new Map(suffixMembers.map((member) => [member.callId, member]));
|
|
1746
|
-
const attached =
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1770
|
+
const attached =
|
|
1771
|
+
interruptionCallIds.length === 0
|
|
1772
|
+
? { accepted: true }
|
|
1773
|
+
: await attachOpenSuffixToPendingToolCalls(db, {
|
|
1774
|
+
accountId: input.accountId,
|
|
1775
|
+
workspaceId: input.workspaceId,
|
|
1776
|
+
sessionId: input.sessionId,
|
|
1777
|
+
turnId: activeTurnId,
|
|
1778
|
+
executionGeneration: attempt.executionGeneration,
|
|
1779
|
+
attemptId: input.attemptId,
|
|
1780
|
+
members: interruptionCallIds.map((callId) => {
|
|
1781
|
+
const member = suffixByCallId.get(callId)!;
|
|
1782
|
+
return {
|
|
1783
|
+
callId,
|
|
1784
|
+
interruptionKind: interruptionKindForCallItem(
|
|
1785
|
+
member.callItem as Record<string, unknown>,
|
|
1786
|
+
),
|
|
1787
|
+
reasoningItems: member.reasoningItems as Array<Record<string, unknown>>,
|
|
1788
|
+
};
|
|
1789
|
+
}),
|
|
1790
|
+
});
|
|
1764
1791
|
if (!attached.accepted) {
|
|
1765
1792
|
return claimedResult({ status: "cancelled" });
|
|
1766
1793
|
}
|
|
@@ -1935,7 +1962,59 @@ export async function runTurnStreamAttempt(
|
|
|
1935
1962
|
return claimedResult({ status: "idle" });
|
|
1936
1963
|
};
|
|
1937
1964
|
|
|
1965
|
+
// Recover committed waits/results before any model dispatch, including crash replacement.
|
|
1966
|
+
// A new attempt uses its own signed caller; origin provenance never changes.
|
|
1967
|
+
// Lazy MCP preparation is never awaited unless stored work must resume.
|
|
1968
|
+
// Measured as its own startup phase so a first-request barrier cannot hide.
|
|
1969
|
+
const programmaticRecoveryStartedAt = performance.now();
|
|
1970
|
+
let programmaticRecoveryOutcome: "completed" | "failed" = "failed";
|
|
1971
|
+
const codemodeOperations = await recoverProgrammaticOperationsBeforeFirstRequest({
|
|
1972
|
+
approvalDecisionId:
|
|
1973
|
+
trigger.type === "user.approvalDecision"
|
|
1974
|
+
? (trigger.payload as { approvalId: string }).approvalId
|
|
1975
|
+
: undefined,
|
|
1976
|
+
hasUnfinishedOperations: async () =>
|
|
1977
|
+
(
|
|
1978
|
+
await listTurnCodemodeApprovals(db, {
|
|
1979
|
+
accountId: input.accountId,
|
|
1980
|
+
workspaceId: input.workspaceId,
|
|
1981
|
+
sessionId: input.sessionId,
|
|
1982
|
+
turnId: activeTurnId,
|
|
1983
|
+
})
|
|
1984
|
+
).length > 0,
|
|
1985
|
+
toolPreparationReady: eventing.toolPreparationReady,
|
|
1986
|
+
resumeApproved: (decisionOperationId) =>
|
|
1987
|
+
eventing.codemodeDispatcher?.resumeApproved(
|
|
1988
|
+
`sandbox:${input.attemptId}`,
|
|
1989
|
+
decisionOperationId,
|
|
1990
|
+
),
|
|
1991
|
+
})
|
|
1992
|
+
.then((operations) => {
|
|
1993
|
+
programmaticRecoveryOutcome = "completed";
|
|
1994
|
+
return operations;
|
|
1995
|
+
})
|
|
1996
|
+
.finally(() => {
|
|
1997
|
+
recordTurnStartupPhase(observability, {
|
|
1998
|
+
phase: "programmatic_operation_recovery",
|
|
1999
|
+
provider: turnExecutionPolicy.providerId,
|
|
2000
|
+
backend: activeSandboxBackend ?? groupBoxBackend,
|
|
2001
|
+
outcome: programmaticRecoveryOutcome,
|
|
2002
|
+
durationSeconds: (performance.now() - programmaticRecoveryStartedAt) / 1_000,
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
codemodeContinuationNote = programmaticContinuationNote(codemodeOperations);
|
|
2006
|
+
programmaticApprovalAcknowledged =
|
|
2007
|
+
trigger.type === "user.approvalDecision" &&
|
|
2008
|
+
codemodeOperations.some(
|
|
2009
|
+
(operation) =>
|
|
2010
|
+
operation.operationId === (trigger.payload as { approvalId?: unknown }).approvalId,
|
|
2011
|
+
);
|
|
2012
|
+
const pendingProgrammaticApprovals = codemodeOperations
|
|
2013
|
+
.filter((operation) => operation.state === "waiting_for_approval")
|
|
2014
|
+
.map(programmaticApproval);
|
|
1938
2015
|
const openSuffixResume = await settleOpenSuffixResumeIfNeeded({
|
|
2016
|
+
additionalApprovals: pendingProgrammaticApprovals,
|
|
2017
|
+
programmaticApprovalAcknowledged,
|
|
1939
2018
|
db,
|
|
1940
2019
|
agent,
|
|
1941
2020
|
accountId: input.accountId,
|
|
@@ -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",
|
|
@@ -1234,6 +1253,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
|
|
|
1234
1253
|
{},
|
|
1235
1254
|
(name) => mcpToolDisplayMetadata(tools.mcpServers, name),
|
|
1236
1255
|
(entry) => toolFamilyForCatalogIdentity(entry, runSettings.mcpServers),
|
|
1256
|
+
tools.inputWaitYield,
|
|
1237
1257
|
);
|
|
1238
1258
|
eventing.codemodeDispatcher.start();
|
|
1239
1259
|
};
|
|
@@ -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
|
|
@@ -294,12 +319,8 @@ export function modelAttachmentInputPolicyForTurn(
|
|
|
294
319
|
};
|
|
295
320
|
} | null,
|
|
296
321
|
): ModelAttachmentInputPolicy {
|
|
297
|
-
const typedTransport =
|
|
298
|
-
resolvedModel === null ||
|
|
299
|
-
resolvedModel.provider.api === "responses" ||
|
|
300
|
-
resolvedModel.provider.api === "anthropic-messages";
|
|
301
322
|
return {
|
|
302
|
-
supportsImageInput:
|
|
323
|
+
supportsImageInput: modelSupportsImageInputForTurn(resolvedModel),
|
|
303
324
|
inputFileMediaTypes: [],
|
|
304
325
|
};
|
|
305
326
|
}
|