@opengeni/worker-bundle 2.2.0 → 2.2.1-canary.36927100590001

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 (88) hide show
  1. package/dist/activities/agent-run-admission.d.ts +3 -1
  2. package/dist/activities/agent-turn/admission.d.ts +24 -3
  3. package/dist/activities/agent-turn/agent-build.d.ts +1 -0
  4. package/dist/activities/agent-turn/claude-usage-observer.d.ts +13 -1
  5. package/dist/activities/agent-turn/compaction-prep.d.ts +20 -0
  6. package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
  7. package/dist/activities/agent-turn/final-reply.d.ts +18 -0
  8. package/dist/activities/agent-turn/governance-model.d.ts +6 -0
  9. package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
  10. package/dist/activities/agent-turn/run-credentials.d.ts +4 -0
  11. package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -0
  12. package/dist/activities/agent-turn/session-title.d.ts +3 -1
  13. package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
  14. package/dist/activities/agent-turn/turn-context.d.ts +1 -0
  15. package/dist/activities/automations.d.ts +3 -1
  16. package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
  17. package/dist/activities/goals.d.ts +8 -0
  18. package/dist/activities/run-credentials.d.ts +4 -2
  19. package/dist/activities/scheduled-tasks.d.ts +4 -35
  20. package/dist/activities/types.d.ts +4 -1
  21. package/dist/activities/workspace-capture.d.ts +2 -0
  22. package/dist/{activities-control-MHNA3CYV.js → activities-control-4F2A734B.js} +502 -133
  23. package/dist/activities-control-4F2A734B.js.map +1 -0
  24. package/dist/activities-control.d.ts +30 -0
  25. package/dist/{activities-turn-FQ5OO2WW.js → activities-turn-TESOIOEX.js} +696 -251
  26. package/dist/activities-turn-TESOIOEX.js.map +1 -0
  27. package/dist/activities.d.ts +30 -0
  28. package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
  29. package/dist/{chunk-RZAAGRFQ.js → chunk-4PJTUBWQ.js} +220 -88
  30. package/dist/chunk-4PJTUBWQ.js.map +1 -0
  31. package/dist/{chunk-IJZU752L.js → chunk-YG27NEMX.js} +34 -1
  32. package/dist/chunk-YG27NEMX.js.map +1 -0
  33. package/dist/index.d.ts +15 -0
  34. package/dist/index.js +46 -24
  35. package/dist/index.js.map +1 -1
  36. package/dist/observability-metrics.d.ts +16 -1
  37. package/dist/sandbox-resume.d.ts +4 -1
  38. package/dist/workflow-bundle.js +89 -21
  39. package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
  40. package/dist/workflows/scheduled-tasks.d.ts +4 -0
  41. package/dist/workflows.d.ts +1 -0
  42. package/package.json +21 -20
  43. package/src/activities/agent-run-admission.ts +17 -1
  44. package/src/activities/agent-turn/admission.ts +64 -1
  45. package/src/activities/agent-turn/agent-build.ts +24 -3
  46. package/src/activities/agent-turn/claim.ts +69 -15
  47. package/src/activities/agent-turn/claude-usage-observer.ts +33 -1
  48. package/src/activities/agent-turn/compaction-prep.ts +67 -16
  49. package/src/activities/agent-turn/credential-restriction.ts +14 -0
  50. package/src/activities/agent-turn/errors.ts +55 -0
  51. package/src/activities/agent-turn/failure-settlement.ts +26 -6
  52. package/src/activities/agent-turn/final-reply.ts +46 -0
  53. package/src/activities/agent-turn/governance-model.ts +12 -0
  54. package/src/activities/agent-turn/model-call-admission.ts +94 -0
  55. package/src/activities/agent-turn/run-credentials.ts +20 -3
  56. package/src/activities/agent-turn/run.ts +44 -5
  57. package/src/activities/agent-turn/sandbox-establish.ts +9 -5
  58. package/src/activities/agent-turn/session-title.ts +8 -2
  59. package/src/activities/agent-turn/stream-attempt.ts +144 -38
  60. package/src/activities/agent-turn/tool-environment.ts +55 -39
  61. package/src/activities/agent-turn/turn-context.ts +1 -0
  62. package/src/activities/automations.ts +78 -4
  63. package/src/activities/browser-deadline-checkpoint.ts +279 -0
  64. package/src/activities/capabilities.ts +52 -14
  65. package/src/activities/goals.ts +95 -98
  66. package/src/activities/knowledge-indexing.ts +29 -0
  67. package/src/activities/open-suffix-resume.ts +2 -1
  68. package/src/activities/run-credentials.ts +22 -16
  69. package/src/activities/sandbox-lease.ts +22 -0
  70. package/src/activities/scheduled-tasks.ts +172 -38
  71. package/src/activities/site-auth-maintenance.ts +1 -0
  72. package/src/activities/types.ts +4 -0
  73. package/src/activities/workspace-capture.ts +13 -2
  74. package/src/activities/workspace-credential-provider.ts +12 -0
  75. package/src/activities-control.ts +2 -0
  76. package/src/activities.ts +3 -0
  77. package/src/browser-deadline-checkpoint-contract.ts +5 -0
  78. package/src/index.ts +53 -25
  79. package/src/observability-metrics.ts +55 -1
  80. package/src/sandbox-resume.ts +23 -8
  81. package/src/sandbox-routing.ts +4 -2
  82. package/src/workflows/browser-deadline-checkpoint.ts +50 -0
  83. package/src/workflows/scheduled-tasks.ts +26 -13
  84. package/src/workflows.ts +4 -0
  85. package/dist/activities-control-MHNA3CYV.js.map +0 -1
  86. package/dist/activities-turn-FQ5OO2WW.js.map +0 -1
  87. package/dist/chunk-IJZU752L.js.map +0 -1
  88. package/dist/chunk-RZAAGRFQ.js.map +0 -1
@@ -12,6 +12,7 @@ import {
12
12
  ReasoningEffort,
13
13
  type FirstPartyMcpToolName,
14
14
  type Permission,
15
+ type ResolvedAgentConfig,
15
16
  type ToolRef,
16
17
  } from "@opengeni/contracts";
17
18
 
@@ -23,6 +24,7 @@ const SESSION_TITLE_DESCRIPTION =
23
24
  export function shouldRequestMissingSessionTitle(input: {
24
25
  title: string | null;
25
26
  titleSource: "user" | "agent" | null;
27
+ agentConfig?: ResolvedAgentConfig | null;
26
28
  firstPartyMcpTools: readonly FirstPartyMcpToolName[];
27
29
  firstPartyMcpPermissions: readonly Permission[] | null;
28
30
  }): boolean {
@@ -30,7 +32,9 @@ export function shouldRequestMissingSessionTitle(input: {
30
32
  const needsSemanticTitle =
31
33
  input.titleSource !== "user" && (!title || title === AUTOMATIC_SESSION_TITLE_FALLBACK);
32
34
  if (!needsSemanticTitle) return false;
33
- if (!input.firstPartyMcpTools.includes("set_session_title")) return false;
35
+ // Configured sessions title through the runtime, not an opted-in capability.
36
+ // Legacy sessions keep their exact selected-tool and permission admission.
37
+ if (!input.agentConfig && !input.firstPartyMcpTools.includes("set_session_title")) return false;
34
38
  const permissions = input.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS;
35
39
  return hasPermission([...permissions], "sessions:control");
36
40
  }
@@ -51,6 +55,7 @@ export function routeAllowsSessionTitleRequests(
51
55
 
52
56
  export function sessionTitleToolPlan(input: {
53
57
  tools: readonly ToolRef[];
58
+ agentConfig?: ResolvedAgentConfig | null;
54
59
  selectedFirstPartyMcpTools: readonly FirstPartyMcpToolName[];
55
60
  shouldRequestTitle: boolean;
56
61
  parallelGenerationAvailable: boolean;
@@ -63,7 +68,8 @@ export function sessionTitleToolPlan(input: {
63
68
  } {
64
69
  const titleToolAvailable =
65
70
  input.shouldRequestTitle &&
66
- input.tools.some((tool) => tool.kind === "mcp" && tool.id === "opengeni");
71
+ (input.agentConfig != null ||
72
+ input.tools.some((tool) => tool.kind === "mcp" && tool.id === "opengeni"));
67
73
  const titleRequestAllowed = titleToolAvailable && input.routeAllowsTitleRequests;
68
74
  const generateTitleInParallel = titleRequestAllowed && input.parallelGenerationAvailable;
69
75
  const promoteTitleTool = titleRequestAllowed && !generateTitleInParallel;
@@ -1,5 +1,8 @@
1
1
  import { measureMcpPhase, withMcpCallIdentity } from "@opengeni/observability";
2
2
  import {
3
+ appendSessionHistoryItems,
4
+ sessionTurnFinalReplyFacts,
5
+ sessionTurnHasFinalReplyNudge,
3
6
  getSessionEvent,
4
7
  getHumanInputResumeForEvent,
5
8
  getSessionHumanInputRequest,
@@ -85,19 +88,20 @@ import {
85
88
  import {
86
89
  OPEN_SUFFIX_RUN_STATE_BLOB,
87
90
  resolveWorkspaceAgentHumanInputEnabled,
91
+ withPublicApprovalFields,
88
92
  type RetainedArtifactMetadata,
89
93
  type SessionEvent,
90
94
  type SessionTurn,
91
95
  } from "@opengeni/contracts";
92
96
  import { createModelCheckpointMemoryCollector } from "../../model-checkpoint-memory-collector";
97
+ import { createModelCallAdmission } from "./model-call-admission";
93
98
 
94
99
  import {
95
100
  assertWorkspaceHumanInputAllowed,
96
101
  stableHumanInputRequestId,
97
102
  stableInteractionInterventionId,
98
103
  stableInteractionInterventionOperationId,
99
- BudgetExhaustedError,
100
- ensureRunAllowed,
104
+ ensureRunAllowedBetweenModelCalls,
101
105
  } from "./admission";
102
106
  import {
103
107
  compactionFailureReason,
@@ -141,6 +145,7 @@ import {
141
145
  import { inputWaitReply, latestDurableTurnMessageText } from "./input-wait-reply";
142
146
  import { waitForTurnOperation } from "./sandbox-provision";
143
147
  import { createSharedRigSetupCoordinator } from "./sandbox-shared-preparation";
148
+ import { finalReplyNudge, needsFinalReply } from "./final-reply";
144
149
 
145
150
  import type { CompactionSummarizer } from "../context-compaction";
146
151
  import type { TurnExecutionPolicyV1 } from "@opengeni/contracts";
@@ -235,6 +240,7 @@ export type TurnStreamAttemptDeps = {
235
240
  groupBoxBackend: Settings["sandboxBackend"];
236
241
  turnExecutionPolicy: TurnExecutionPolicyV1;
237
242
  turn: Pick<SessionTurn, "initiator" | "initiatorContext"> & {
243
+ initiatingHumanSubjectId: string | null;
238
244
  id: string;
239
245
  executionGeneration: number;
240
246
  model: string;
@@ -583,14 +589,41 @@ export async function runTurnStreamAttempt(
583
589
  // durably: the reply a wait-ended human turn records on turn.completed.
584
590
  let latestAssistantMessageText: string | null = null;
585
591
  let workerPreparationTotalRecorded = false;
592
+ let toolsExecuted = false;
593
+ let finalReplyNudged = false;
594
+ const revalidateModelCallAdmission = async () => {
595
+ await historySink.reconcileConversationTruth({ requireDurable: true });
596
+ await ensureRunAllowedBetweenModelCalls({
597
+ settings,
598
+ db,
599
+ accountId: input.accountId,
600
+ workspaceId: input.workspaceId,
601
+ isExternallyBilledTurn: billingState.isExternallyBilledTurn,
602
+ entitlements,
603
+ chargesOpenGeniCredits: billingState.chargesOpenGeniCredits,
604
+ countsTowardTokenCap: billingState.countsTowardTokenCap,
605
+ initiatingHumanSubjectId: turn.initiatingHumanSubjectId,
606
+ });
607
+ };
586
608
  const runStreamAttempt = async (options: {
587
609
  requireTerminalModelResponse: boolean;
588
- }): Promise<RunAgentTurnResult> => {
610
+ }): Promise<RunAgentTurnResult | "reply_nudge"> => {
589
611
  if (!runInput) {
590
612
  throw new Error("Run input was not prepared");
591
613
  }
592
- const responseCountBeforeStream = modelResponseState.responseCount;
614
+ // The previous stream was persisted before compaction; the sink is now
615
+ // seeded from its durable replacement. Do not reconcile the old prefix
616
+ // against that replacement while checking the next stream's admission.
593
617
  eventing.stream = undefined;
618
+ // Compaction commits its paid usage and replacement history before this
619
+ // boundary, both during preparation and in-activity recovery. Revalidate
620
+ // the accepted turn's frozen human before another stream can dispatch.
621
+ if (options.requireTerminalModelResponse) await revalidateModelCallAdmission();
622
+ const modelCallAdmission = createModelCallAdmission({
623
+ signal: runtimeCancellationSignal,
624
+ admit: revalidateModelCallAdmission,
625
+ });
626
+ const responseCountBeforeStream = modelResponseState.responseCount;
594
627
  eventing.batcher = null;
595
628
  // The SDK emits every processed call item for one model response before
596
629
  // it emits any result for that response. Keep that response-local batch
@@ -806,6 +839,8 @@ export async function runTurnStreamAttempt(
806
839
  }
807
840
  attempt.modelRequestStarted = true;
808
841
  return await runtime.runStream(agent, runInput!, eventing.modelRunSettings, {
842
+ beforeModelRequest: modelCallAdmission.beforeModelRequest,
843
+ onModelResponse: modelCallAdmission.onModelResponse,
809
844
  signal: runtimeCancellationSignal,
810
845
  sandboxEnvironment,
811
846
  onModelVisibleContext: async (snapshot) => {
@@ -941,7 +976,13 @@ export async function runTurnStreamAttempt(
941
976
  if (leases.xai.lost) {
942
977
  throw new Error("xAI credential lease expired before the model run");
943
978
  }
944
- eventing.stream = await withProviderRequestContext(runStreamOnce);
979
+ try {
980
+ eventing.stream = await withProviderRequestContext(runStreamOnce);
981
+ } catch (error) {
982
+ modelCallAdmission.fail(error);
983
+ modelCallAdmission.close();
984
+ throw error;
985
+ }
945
986
  // Bounded provider label for the streaming SLIs — the resolved registry
946
987
  // provider id (or the built-in OpenAI/Azure provider), never a raw
947
988
  // user-supplied model string.
@@ -1094,35 +1135,22 @@ export async function runTurnStreamAttempt(
1094
1135
  await historySink.reconcileConversationTruth();
1095
1136
  turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
1096
1137
  modelCheckpointMemoryCollector.schedule(observability);
1097
- try {
1098
- await ensureRunAllowed(
1099
- settings,
1100
- db,
1101
- input.accountId,
1102
- input.workspaceId,
1103
- billingState.isExternallyBilledTurn,
1104
- entitlements,
1105
- billingState.chargesOpenGeniCredits,
1106
- billingState.countsTowardTokenCap,
1107
- );
1108
- } catch (limitError) {
1109
- // Capture the run state at the boundary so the budget valve in
1110
- // the outer catch can end this segment gracefully with full
1111
- // conversation context preserved for the post-top-up resume.
1112
- let serializedRunState: string | null = null;
1113
- try {
1114
- serializedRunState = media.compactMediaRunState(
1115
- String(eventing.stream.state.toString()),
1116
- );
1117
- } catch {
1118
- serializedRunState = null;
1119
- }
1120
- throw new BudgetExhaustedError(
1121
- limitError instanceof Error ? limitError.message : String(limitError),
1122
- serializedRunState,
1123
- );
1124
- }
1138
+ await ensureRunAllowedBetweenModelCalls({
1139
+ settings,
1140
+ db,
1141
+ accountId: input.accountId,
1142
+ workspaceId: input.workspaceId,
1143
+ isExternallyBilledTurn: billingState.isExternallyBilledTurn,
1144
+ entitlements,
1145
+ chargesOpenGeniCredits: billingState.chargesOpenGeniCredits,
1146
+ countsTowardTokenCap: billingState.countsTowardTokenCap,
1147
+ initiatingHumanSubjectId: turn.initiatingHumanSubjectId,
1148
+ serializedRunState: () =>
1149
+ media.compactMediaRunState(String(eventing.stream!.state.toString())),
1150
+ });
1125
1151
  }
1152
+ // Release only after both the debit and frozen-human admission finish.
1153
+ modelCallAdmission.settle(next.value);
1126
1154
  const durableSdkEvent = generatedImageReceipt
1127
1155
  ? compactGeneratedImageSdkEvent(next.value, generatedImageReceipt)
1128
1156
  : next.value;
@@ -1159,6 +1187,7 @@ export async function runTurnStreamAttempt(
1159
1187
  }
1160
1188
  const completedToolCall = completedToolCallFromSdkEvent(durableSdkEvent);
1161
1189
  if (completedToolCall) {
1190
+ toolsExecuted = true;
1162
1191
  retainedScreenshotMetadata =
1163
1192
  media.retainedScreenshotReceiptsByCallId.get(completedToolCall.callId) ?? null;
1164
1193
  const typedScreenshot = retainedScreenshotMetadata
@@ -1396,6 +1425,7 @@ export async function runTurnStreamAttempt(
1396
1425
  }
1397
1426
  }
1398
1427
  } catch (error) {
1428
+ modelCallAdmission.fail(error);
1399
1429
  // Event processing can fail while SDK completion is still pending.
1400
1430
  // Close this stream before any failure publication; a legitimate
1401
1431
  // compaction retry may start a new, independently fenced generation.
@@ -1451,6 +1481,7 @@ export async function runTurnStreamAttempt(
1451
1481
  }
1452
1482
  throw error;
1453
1483
  } finally {
1484
+ modelCallAdmission.close();
1454
1485
  if (!streamDone) {
1455
1486
  // ReadableStream cancellation synchronously trips the Agents SDK's
1456
1487
  // abort controller, but its returned promise may wait for an
@@ -1472,7 +1503,10 @@ export async function runTurnStreamAttempt(
1472
1503
  // External Codemode stays reachable until finalization. Close wait
1473
1504
  // admission before any terminal output/history decision, and drain an
1474
1505
  // already-admitted wait before consulting the actual runner-yield latch.
1475
- await eventing.preparedTools?.inputWaitYield?.sealForSettlement(runtimeCancellationSignal);
1506
+ // Drain trusted receipts before deciding whether an empty stream may need
1507
+ // a handoff. Do not terminally seal the attempt until that decision: the
1508
+ // one same-turn stream still uses the ordinary input-wait admission gate.
1509
+ await eventing.preparedTools?.inputWaitYield?.drainForHandoff(runtimeCancellationSignal);
1476
1510
  if (
1477
1511
  options.requireTerminalModelResponse &&
1478
1512
  !eventing.preparedTools?.inputWaitYield?.yielded &&
@@ -1585,6 +1619,7 @@ export async function runTurnStreamAttempt(
1585
1619
  }
1586
1620
  }
1587
1621
  if (eventing.stream.interruptions.length > 0) {
1622
+ await eventing.preparedTools?.inputWaitYield?.sealForSettlement(runtimeCancellationSignal);
1588
1623
  await historySink.reconcileConversationTruth({ requireDurable: true });
1589
1624
  const approvals = runtime.serializeApprovals(eventing.stream.interruptions);
1590
1625
  const humanInputInterruptions =
@@ -1702,9 +1737,11 @@ export async function runTurnStreamAttempt(
1702
1737
  type: "session.requiresAction" as const,
1703
1738
  payload: {
1704
1739
  approvals: approvals.map((approval) =>
1705
- withMcpToolDisplayMetadata(
1706
- eventing.preparedTools?.mcpServers ?? [],
1707
- approval,
1740
+ withPublicApprovalFields(
1741
+ withMcpToolDisplayMetadata(
1742
+ eventing.preparedTools?.mcpServers ?? [],
1743
+ approval,
1744
+ ),
1708
1745
  ),
1709
1746
  ),
1710
1747
  },
@@ -1739,6 +1776,60 @@ export async function runTurnStreamAttempt(
1739
1776
  const finalOutput = String(
1740
1777
  requireAgentStreamFinalOutput(eventing.stream.finalOutput, inputWaitYielded),
1741
1778
  );
1779
+ const durableReplyFacts =
1780
+ !inputWaitYielded && finalOutput.trim().length === 0
1781
+ ? await sessionTurnFinalReplyFacts(db, input.workspaceId, input.sessionId, activeTurnId)
1782
+ : { toolsExecuted: false, completedGoal: false };
1783
+ let emptyFinalReply = false;
1784
+ if (
1785
+ needsFinalReply({
1786
+ output: finalOutput,
1787
+ inputWaitYielded:
1788
+ inputWaitYielded || eventing.preparedTools?.inputWaitYield?.requested === true,
1789
+ interrupted: false, // interruption settlement returned above
1790
+ maintenance: turn.source === "compaction",
1791
+ toolsExecuted: toolsExecuted || finalReplyNudged || durableReplyFacts.toolsExecuted,
1792
+ completedGoal: durableReplyFacts.completedGoal,
1793
+ })
1794
+ ) {
1795
+ await historySink.reconcileConversationTruth({ requireDurable: true });
1796
+ // Consult retained truth, including inactive compacted rows, so an
1797
+ // attempt replacement or compaction cannot spend this bound again.
1798
+ if (
1799
+ finalReplyNudged ||
1800
+ (await sessionTurnHasFinalReplyNudge(
1801
+ db,
1802
+ input.workspaceId,
1803
+ input.sessionId,
1804
+ activeTurnId,
1805
+ finalReplyNudge(activeTurnId).content[0]!.text,
1806
+ ))
1807
+ ) {
1808
+ // A second empty response is a delivery-quality notice, not failed
1809
+ // execution: preserve goal continuation and later machine-input wakes.
1810
+ emptyFinalReply = true;
1811
+ } else {
1812
+ const appended = await appendSessionHistoryItems(db, {
1813
+ accountId: input.accountId,
1814
+ workspaceId: input.workspaceId,
1815
+ sessionId: input.sessionId,
1816
+ turnId: activeTurnId,
1817
+ expectedExecutionGeneration: attempt.executionGeneration,
1818
+ expectedAttemptId: input.attemptId,
1819
+ items: [
1820
+ {
1821
+ position: await nextSessionHistoryPosition(db, input.workspaceId, input.sessionId),
1822
+ item: finalReplyNudge(activeTurnId),
1823
+ },
1824
+ ],
1825
+ });
1826
+ if (!appended) throw new TurnAttemptFencedError("turn ended before final reply handoff");
1827
+ finalReplyNudged = true;
1828
+ await prepareRunAttemptInput();
1829
+ return "reply_nudge";
1830
+ }
1831
+ }
1832
+ await eventing.preparedTools?.inputWaitYield?.sealForSettlement(runtimeCancellationSignal);
1742
1833
  // The final output is the newest message this stream completed, already
1743
1834
  // durable with its provider identity and phase. A phase-less settlement
1744
1835
  // copy is published only when this stream did not complete that text.
@@ -1773,7 +1864,11 @@ export async function runTurnStreamAttempt(
1773
1864
  : [{ type: "agent.message.completed" as const, payload: { text: finalOutput } }]),
1774
1865
  {
1775
1866
  type: "turn.completed",
1776
- payload: { output: finalOutput, ...(reply === null ? {} : { reply }) },
1867
+ payload: {
1868
+ output: finalOutput,
1869
+ ...(reply === null ? {} : { reply }),
1870
+ ...(emptyFinalReply ? { emptyFinalReply: true } : {}),
1871
+ },
1777
1872
  },
1778
1873
  { type: "session.status.changed", payload: { status: "idle" } },
1779
1874
  ],
@@ -1834,6 +1929,9 @@ export async function runTurnStreamAttempt(
1834
1929
  ) {
1835
1930
  return claimedResult({ status: "cancelled" });
1836
1931
  }
1932
+ // Preparation may have spent the last allowance on a completed summary.
1933
+ // Neither the title sidecar nor ordinary inference may dispatch afterward.
1934
+ await revalidateModelCallAdmission();
1837
1935
  if (
1838
1936
  turn.source !== "compaction" &&
1839
1937
  generateSessionTitleInParallel &&
@@ -1871,11 +1969,19 @@ export async function runTurnStreamAttempt(
1871
1969
  }
1872
1970
  try {
1873
1971
  let retriedAfterCompaction = false;
1972
+ finalReplyNudged = await sessionTurnHasFinalReplyNudge(
1973
+ db,
1974
+ input.workspaceId,
1975
+ input.sessionId,
1976
+ activeTurnId,
1977
+ finalReplyNudge(activeTurnId).content[0]!.text,
1978
+ );
1874
1979
  while (true) {
1875
1980
  try {
1876
1981
  const result = await runStreamAttempt({
1877
1982
  requireTerminalModelResponse: retriedAfterCompaction,
1878
1983
  });
1984
+ if (result === "reply_nudge") continue;
1879
1985
  if (retriedAfterCompaction) {
1880
1986
  observability.info("context compaction recovery succeeded after in-activity retry", {
1881
1987
  sessionId: input.sessionId,
@@ -68,8 +68,10 @@ import {
68
68
  buildApiIntegrationMcpServers,
69
69
  resolveCatalogSettings,
70
70
  resolveWorkspaceModelSelection,
71
+ loadWorkspaceCodexModelAvailability,
71
72
  withFrozenPersonalConnectionDelegations,
72
- resolveSessionToolPolicy,
73
+ resolveTurnToolPolicy,
74
+ scheduledTurnMcpServerIds,
73
75
  hasPermission,
74
76
  } from "@opengeni/core";
75
77
  import { loadWorkspaceEnvironmentForRunWithCredentials } from "../environment";
@@ -87,6 +89,7 @@ import { SandboxChannelAService } from "@opengeni/runtime/sandbox";
87
89
  import { sandboxRunAs } from "@opengeni/runtime";
88
90
  import {
89
91
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
92
+ resolveAgentToolFamilies,
90
93
  type ResourceRef,
91
94
  type ToolAuthNeededPayload,
92
95
  } from "@opengeni/contracts";
@@ -124,6 +127,7 @@ import { codeSearchToolDefinitions, codeSearchWorkspaceFromChannel } from "./cod
124
127
  import { createWorkspaceSkillTools } from "./skill-tools";
125
128
  import { loadConfiguredBundledSkills } from "./skill-selection";
126
129
  import { guardSkillFilesystem } from "./skill-transfer";
130
+ import { turnCredentialRestriction } from "./credential-restriction";
127
131
 
128
132
  export type PrepareTurnToolPolicyDeps = {
129
133
  input: RunAgentTurnInput;
@@ -223,38 +227,36 @@ export async function prepareTurnToolPolicy(deps: PrepareTurnToolPolicyDeps) {
223
227
  // sessions follow the current configured MCP set,
224
228
  // while explicit, inherited-fixed, and legacy sessions remain narrowed
225
229
  // to their stored materialized allow-list.
226
- const scheduledEffectiveMcpServerIds = (() => {
227
- const value =
228
- turn.metadata && typeof turn.metadata === "object" && !Array.isArray(turn.metadata)
229
- ? (turn.metadata as Record<string, unknown>).scheduledEffectiveMcpServerIds
230
- : null;
231
- return Array.isArray(value) && value.every((id) => typeof id === "string")
232
- ? [...new Set(value)].sort()
233
- : null;
234
- })();
235
- const currentMcpServerIds = new Set(runSettings.mcpServers.map((server) => server.id));
236
- const resolvedToolPolicy = resolveSessionToolPolicy({
230
+ const resolvedToolPolicy = resolveTurnToolPolicy({
237
231
  toolPolicy: session.toolPolicy,
238
- sessionTools: scheduledEffectiveMcpServerIds ? turn.tools : session.tools,
239
- availableMcpServerIds: scheduledEffectiveMcpServerIds
240
- ? scheduledEffectiveMcpServerIds.filter((id) => currentMcpServerIds.has(id))
241
- : [...currentMcpServerIds],
232
+ session,
233
+ turn,
234
+ availableMcpServerIds: runSettings.mcpServers.map((server) => server.id),
242
235
  defaultMcpServerIds:
243
- scheduledEffectiveMcpServerIds ??
244
- (session.toolPolicy.mode === "workspace_default"
236
+ scheduledTurnMcpServerIds(turn) === null && session.toolPolicy.mode === "workspace_default"
245
237
  ? await workspaceSessionToolPolicyDefaultServerIds(
246
238
  db,
247
239
  input.workspaceId,
248
240
  capabilitySettings,
249
241
  fileAuthoritySubjectId ?? undefined,
250
242
  )
251
- : []),
243
+ : [],
252
244
  });
253
245
  const mcpAvailabilityNote = unavailableMcpOperationalContext({
254
246
  droppedIds: resolvedToolPolicy.effectivePolicy.droppedIds,
255
247
  droppedCount: resolvedToolPolicy.effectivePolicy.counts.dropped,
256
248
  });
257
- const effectivePolicyTools = resolvedToolPolicy.toolRefs;
249
+ const families = resolveAgentToolFamilies(session.agent, {
250
+ productServerIds: new Set([
251
+ ...session.mcpServers.map((server) => server.id),
252
+ ...(session.toolPolicy.mode !== "workspace_default"
253
+ ? session.tools.map((tool) => tool.id)
254
+ : []),
255
+ ]),
256
+ });
257
+ const effectivePolicyTools = resolvedToolPolicy.toolRefs.filter(
258
+ (tool) => tool.kind !== "mcp" || families.allowsMcpServer(tool.id),
259
+ );
258
260
  const turnTools = withFirstPartyTools(runSettings, effectivePolicyTools);
259
261
  // §7.6 connection-credential provider — load (and decrypt) selected Variable Sets via the
260
262
  // host `sandboxSecrets` provider when bound; unset → today's local decrypt. Preserve the
@@ -421,6 +423,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
421
423
  });
422
424
  const runSettings = accountRoutes.settings;
423
425
  const turnTools = accountRoutes.tools;
426
+ const credentialRestriction = turnCredentialRestriction(turnExecutionPolicy, session.metadata);
424
427
  const toolContextPreparationStartedAt = performance.now();
425
428
  throwIfWorkerShuttingDown();
426
429
  throwIfTurnCancelled();
@@ -590,14 +593,16 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
590
593
  (permission) => hasPermission(linkedAuthority.permissions, permission),
591
594
  )
592
595
  : session.firstPartyMcpPermissions;
593
- const selectedFirstPartyMcpTools = allowedFirstPartyMcpToolsForSession(
594
- runSettings,
595
- session.firstPartyMcpTools,
596
+ const toolFamilies = resolveAgentToolFamilies(session.agent);
597
+ const selectedFirstPartyMcpTools = toolFamilies.firstPartyTools(
598
+ allowedFirstPartyMcpToolsForSession(runSettings, session.firstPartyMcpTools),
596
599
  );
597
600
  const titleToolPlan = sessionTitleToolPlan({
601
+ agentConfig: session.agent,
598
602
  tools: turnTools,
599
603
  selectedFirstPartyMcpTools,
600
604
  shouldRequestTitle: shouldRequestMissingSessionTitle({
605
+ agentConfig: session.agent,
601
606
  title: session.title,
602
607
  titleSource: session.titleSource,
603
608
  firstPartyMcpTools: selectedFirstPartyMcpTools,
@@ -669,20 +674,23 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
669
674
  workspaceId: input.workspaceId,
670
675
  ...(deps.fileAuthoritySubjectId ? { subjectId: deps.fileAuthoritySubjectId } : {}),
671
676
  });
672
- const skillCatalog = [
673
- ...sharedSkillDescriptors
674
- .filter((entry) => entry.activationMode === "workspace_managed")
675
- .map((entry) => ({
676
- id: entry.id,
677
- name: entry.title,
678
- description: entry.description,
679
- })),
680
- ...selectedSkills.map((entry) => ({
681
- id: entry.id,
682
- name: entry.artifact.name,
683
- description: entry.artifact.description || entry.artifact.name,
684
- })),
685
- ];
677
+ const skillCatalog =
678
+ toolFamilies.skills === false
679
+ ? []
680
+ : [
681
+ ...sharedSkillDescriptors
682
+ .filter((entry) => entry.activationMode === "workspace_managed")
683
+ .map((entry) => ({
684
+ id: entry.id,
685
+ name: entry.title,
686
+ description: entry.description,
687
+ })),
688
+ ...selectedSkills.map((entry) => ({
689
+ id: entry.id,
690
+ name: entry.artifact.name,
691
+ description: entry.artifact.description || entry.artifact.name,
692
+ })),
693
+ ];
686
694
  const skillTools = createWorkspaceSkillTools({
687
695
  db,
688
696
  settings: runSettings,
@@ -861,6 +869,9 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
861
869
  );
862
870
  },
863
871
  });
872
+ const attemptToolFamilies = resolveAgentToolFamilies(session.agent, {
873
+ hasSkills: skillCatalog.length > 0,
874
+ });
864
875
  const attemptToolDefinitions = [
865
876
  ...(operationReadStore
866
877
  ? [
@@ -908,6 +919,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
908
919
  organizationGatewayCustomModels,
909
920
  organizationOpenRouterConnectionActive,
910
921
  organizationOpenRouterCustomModels,
922
+ codexModelAvailability,
911
923
  ] = await Promise.all([
912
924
  getWorkspaceConnectionModelRestrictions(
913
925
  db,
@@ -953,9 +965,11 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
953
965
  workspaceId: input.workspaceId,
954
966
  providerKind: "openrouter",
955
967
  }),
968
+ loadWorkspaceCodexModelAvailability(db, currentSettings, input.workspaceId),
956
969
  ]);
957
970
  return {
958
971
  selections: resolveWorkspaceModelSelection({
972
+ observations: codexModelAvailability,
959
973
  connectionModelRestrictions,
960
974
  settings: currentSettings,
961
975
  policy,
@@ -997,6 +1011,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
997
1011
  : []),
998
1012
  ...createFirstPartyInteractionAttemptToolDefinitions({
999
1013
  settings: runSettings,
1014
+ ...(credentialRestriction ? { credentialRestriction } : {}),
1000
1015
  scope: {
1001
1016
  accountId: input.accountId,
1002
1017
  workspaceId: input.workspaceId,
@@ -1017,7 +1032,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
1017
1032
  ? [googleDrivePublicationTool]
1018
1033
  : []),
1019
1034
  ...codeSearchTools,
1020
- ];
1035
+ ].filter((tool) => attemptToolFamilies.allowsFunctionTool(tool.modelName));
1021
1036
  recordTurnStartupPhase(observability, {
1022
1037
  phase: "tool_context_preparation",
1023
1038
  provider: turnExecutionPolicy.providerId,
@@ -1092,6 +1107,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
1092
1107
  try {
1093
1108
  eventing.preparedTools = await waitForTurnOperation(
1094
1109
  runtime.prepareTools(githubRestMcp.settings, githubRestMcp.tools, {
1110
+ ...(credentialRestriction ? { credentialRestriction } : {}),
1095
1111
  mcpAccountLabels: accountRoutes.accountLabels,
1096
1112
  accountId: input.accountId,
1097
1113
  workspaceId: input.workspaceId,
@@ -1110,7 +1126,7 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
1110
1126
  sessionAttachedRemoteMcpTargets: selectedSessionRemoteMcpTargets(
1111
1127
  githubRestMcp.settings,
1112
1128
  session.mcpServers ?? [],
1113
- turn.tools ?? [],
1129
+ githubRestMcp.tools,
1114
1130
  localMcpServers,
1115
1131
  ),
1116
1132
  ...(deps.runMcpCredentials ? { runMcpCredentials: deps.runMcpCredentials } : {}),
@@ -48,6 +48,7 @@ export type TurnSettleFn = (input: {
48
48
  sessionStatus: SessionStatus;
49
49
  activeTurnId: string | null;
50
50
  suppressGoalContinuation?: boolean;
51
+ allowanceGoalPause?: ApplySessionTurnSettlementInput["allowanceGoalPause"];
51
52
  consumeRequestedCompactionFailure?: boolean;
52
53
  runState?: ApplySessionTurnSettlementInput["runState"];
53
54
  }) => Promise<boolean>;