@opengeni/worker-bundle 1.1.0 → 1.2.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.
Files changed (30) hide show
  1. package/dist/activities/agent-turn/admission.d.ts +3 -2
  2. package/dist/activities/agent-turn/compaction-prep.d.ts +1 -0
  3. package/dist/activities/agent-turn/finalization-monitor.d.ts +6 -1
  4. package/dist/activities/agent-turn/model-usage.d.ts +10 -0
  5. package/dist/{activities-control-XZAIMFQJ.js → activities-control-QSDPXLLD.js} +5 -5
  6. package/dist/activities-control-QSDPXLLD.js.map +1 -0
  7. package/dist/{activities-turn-4EEVNZCC.js → activities-turn-BMY3CDKL.js} +96 -32
  8. package/dist/activities-turn-BMY3CDKL.js.map +1 -0
  9. package/dist/{chunk-OT64XARC.js → chunk-JKMG5ZHH.js} +18 -6
  10. package/dist/{chunk-OT64XARC.js.map → chunk-JKMG5ZHH.js.map} +1 -1
  11. package/dist/{chunk-3ZTRWDMN.js → chunk-XONQEVAD.js} +1 -1
  12. package/dist/chunk-XONQEVAD.js.map +1 -0
  13. package/dist/index.js +2 -2
  14. package/dist/observability-metrics.d.ts +1 -1
  15. package/package.json +21 -21
  16. package/src/activities/agent-turn/admission.ts +11 -5
  17. package/src/activities/agent-turn/claim.ts +1 -0
  18. package/src/activities/agent-turn/compaction-prep.ts +20 -1
  19. package/src/activities/agent-turn/errors.ts +21 -4
  20. package/src/activities/agent-turn/failure-settlement.ts +9 -1
  21. package/src/activities/agent-turn/finalization-monitor.ts +14 -1
  22. package/src/activities/agent-turn/finalization.ts +5 -0
  23. package/src/activities/agent-turn/model-usage.ts +38 -0
  24. package/src/activities/agent-turn/run.ts +1 -0
  25. package/src/activities/agent-turn/stream-attempt.ts +22 -16
  26. package/src/activities/knowledge-indexing.ts +2 -2
  27. package/src/observability-metrics.ts +1 -0
  28. package/dist/activities-control-XZAIMFQJ.js.map +0 -1
  29. package/dist/activities-turn-4EEVNZCC.js.map +0 -1
  30. package/dist/chunk-3ZTRWDMN.js.map +0 -1
@@ -11,6 +11,7 @@ import {
11
11
  safeDatabaseErrorFacts,
12
12
  isRetryableDatabaseTransportFailure,
13
13
  isSessionEventPersistenceError,
14
+ DatabaseTransactionError,
14
15
  SandboxLeaseTransitionError,
15
16
  } from "@opengeni/db";
16
17
  import {
@@ -419,6 +420,12 @@ function retryableDatabaseFailureCode(
419
420
  const boundaries = new Set<object>();
420
421
  const ownDatabaseNodes = new Set<object>();
421
422
  const codes = new Set<PostClaimDatabaseRecoveryDetail["code"]>();
423
+ const ownChildren = (node: object): object[] =>
424
+ node instanceof DatabaseTransactionError
425
+ ? // Callback failures retained beside a failed rollback supply vetoes,
426
+ // never driver provenance for a provider's connection-looking error.
427
+ [...graph.get(node)!].filter((child) => child === node.cause)
428
+ : [...graph.get(node)!];
422
429
  // Ask the canonical transport predicate about ONLY this node's facts. Its
423
430
  // recursive search must not pair a DB sibling with an unrelated provider.
424
431
  for (const node of graph.keys()) {
@@ -441,7 +448,11 @@ function retryableDatabaseFailureCode(
441
448
  : isDatabaseConnectionSqlState(sqlState)
442
449
  )
443
450
  transports.add(node);
444
- if (node instanceof DrizzleQueryError || isSessionEventPersistenceError(node)) {
451
+ if (
452
+ node instanceof DrizzleQueryError ||
453
+ node instanceof DatabaseTransactionError ||
454
+ isSessionEventPersistenceError(node)
455
+ ) {
445
456
  // Only actual errors raised at our ORM/typed persistence boundary own
446
457
  // their driver subtree. A PostgresError name, SDK wrapper or provider
447
458
  // socket by itself is never own-client provenance for a running turn.
@@ -449,7 +460,7 @@ function retryableDatabaseFailureCode(
449
460
  for (const source of queue) {
450
461
  if (ownDatabaseNodes.has(source)) continue;
451
462
  ownDatabaseNodes.add(source);
452
- queue.push(...graph.get(source)!);
463
+ queue.push(...ownChildren(source));
453
464
  }
454
465
  }
455
466
  }
@@ -460,7 +471,7 @@ function retryableDatabaseFailureCode(
460
471
  if (seen.has(node)) continue;
461
472
  seen.add(node);
462
473
  if (transports.has(node)) return true;
463
- queue.push(...graph.get(node)!);
474
+ queue.push(...ownChildren(node));
464
475
  }
465
476
  return false;
466
477
  };
@@ -469,7 +480,7 @@ function retryableDatabaseFailureCode(
469
480
  // order; a deeper transport/reset cannot override it either.
470
481
  for (const node of graph.keys()) {
471
482
  const record = node as Record<string, unknown>;
472
- if (node instanceof DrizzleQueryError) {
483
+ if (node instanceof DrizzleQueryError || node instanceof DatabaseTransactionError) {
473
484
  boundaries.add(node);
474
485
  if (hasOwnTransport(node)) codes.add("db_failure");
475
486
  }
@@ -513,6 +524,12 @@ function retryableDatabaseFailureCode(
513
524
  }
514
525
 
515
526
  const RUNNING_TURN_DATABASE_TRANSPORT_CODES = new Set([
527
+ // postgres.js reports these for a physically lost connection, including
528
+ // transaction cleanup after its socket has already closed. Own-client
529
+ // provenance and the unknown-outcome veto remain mandatory above.
530
+ "CONNECTION_CLOSED",
531
+ "CONNECTION_DESTROYED",
532
+ "CONNECTION_ENDED",
516
533
  "ECONNREFUSED",
517
534
  "ECONNRESET",
518
535
  "CONNECT_TIMEOUT",
@@ -277,12 +277,20 @@ export async function settleTurnFailure(deps: TurnFailureDeps): Promise<RunAgent
277
277
  try {
278
278
  return await settleTurnFailureInAttempt(deps);
279
279
  } catch (error) {
280
+ if (
281
+ deps.control.activityStatus === "recovering" &&
282
+ error instanceof ApplicationFailure &&
283
+ error.type === "OpenGeniPostClaimDatabaseRecovery"
284
+ )
285
+ throw error;
280
286
  // Connectivity can disappear while settling an unrelated run error too.
281
287
  // Do not overwrite a possibly committed settlement; the control lane
282
288
  // re-reads exact ownership and becomes a stale no-op if it already closed.
283
289
  if (deps.attempt.turnId && deps.attempt.triggerEventId) {
284
290
  const recovery = postClaimDatabaseRecoveryFailure({
285
- error,
291
+ // A failed rollback/terminal write must not erase no-replay evidence
292
+ // from the failure we were settling (notably unknown tool effects).
293
+ error: new AggregateError([error, deps.error], "Turn failure settlement failed"),
286
294
  turnId: deps.attempt.turnId,
287
295
  triggerEventId: deps.attempt.triggerEventId,
288
296
  executionGeneration: deps.attempt.executionGeneration,
@@ -1,4 +1,4 @@
1
- import type { Observability } from "@opengeni/observability";
1
+ import { turnExecutionTelemetryKey, type Observability } from "@opengeni/observability";
2
2
  import type { TurnHeartbeatDetails } from "../../op-journal";
3
3
  import { armTurnQuiescenceWatchdog } from "./quiescence";
4
4
 
@@ -42,10 +42,21 @@ export function startTurnFinalizationMonitor(input: {
42
42
  details: TurnHeartbeatDetails;
43
43
  heartbeat: (details: TurnHeartbeatDetails) => void;
44
44
  requestWorkerDrain: () => void;
45
+ execution?: { workspaceId: string; sessionId: string; attemptId: string };
45
46
  timeoutMs?: number;
46
47
  slowAfterMs?: number;
47
48
  }) {
48
49
  const { observability, details } = input;
50
+ const correlation: { correlationId?: string } = {};
51
+ diagnose(() => {
52
+ if (input.execution) {
53
+ correlation.correlationId = turnExecutionTelemetryKey(
54
+ input.execution.workspaceId,
55
+ input.execution.sessionId,
56
+ input.execution.attemptId,
57
+ );
58
+ }
59
+ });
49
60
  if (!initialized.has(observability)) {
50
61
  for (const stage of TURN_FINALIZATION_STAGES) {
51
62
  diagnose(() => observability.incrementGauge({ ...INFLIGHT, labels: { stage }, amount: 0 }));
@@ -88,6 +99,7 @@ export function startTurnFinalizationMonitor(input: {
88
99
  surface: "turn_finalization",
89
100
  outcome: "containment",
90
101
  reason: next,
102
+ ...correlation,
91
103
  });
92
104
  } catch {
93
105
  // Diagnostic failure must never disable physical containment.
@@ -104,6 +116,7 @@ export function startTurnFinalizationMonitor(input: {
104
116
  surface: "turn_finalization",
105
117
  outcome: "slow",
106
118
  reason: next,
119
+ ...correlation,
107
120
  });
108
121
  }),
109
122
  input.slowAfterMs ?? 30_000,
@@ -128,6 +128,11 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
128
128
  }
129
129
  },
130
130
  requestWorkerDrain: deps.requestWorkerDrain,
131
+ execution: {
132
+ workspaceId: deps.input.workspaceId,
133
+ sessionId: deps.input.sessionId,
134
+ attemptId: deps.input.attemptId,
135
+ },
131
136
  });
132
137
  try {
133
138
  monitor.enter("tool_writers");
@@ -1,3 +1,4 @@
1
+ import { canonicalizeConfiguredModelId } from "@opengeni/config";
1
2
  import {
2
3
  applyCreditDebitUpToBalance,
3
4
  recordUsageEvent,
@@ -55,6 +56,27 @@ export function modelUsageSourceKey(input: {
55
56
  return input.dispatchId ? `${input.dispatchId}:${input.positionalKey}` : input.positionalKey;
56
57
  }
57
58
 
59
+ /** Legacy aggregate usage may only debit a policy shared by its completed calls. */
60
+ export function aggregateCreditPolicyRevision(input: {
61
+ responseRevisions: ReadonlySet<number | undefined>;
62
+ lastAdmittedRevision: number | undefined;
63
+ chargesOpenGeniCredits: boolean;
64
+ totalTokens: number | null;
65
+ }): number | undefined {
66
+ if (
67
+ input.chargesOpenGeniCredits &&
68
+ input.responseRevisions.size > 1 &&
69
+ (input.totalTokens ?? 0) > 0
70
+ ) {
71
+ throw new Error("Aggregate model usage spans different credit policy revisions");
72
+ }
73
+ // Bind the completed response, even if later preparation changed admission.
74
+ // Older runtimes without response callbacks retain their admitted snapshot.
75
+ return input.responseRevisions.size === 1
76
+ ? input.responseRevisions.values().next().value
77
+ : input.lastAdmittedRevision;
78
+ }
79
+
58
80
  export function providerContextTokens(
59
81
  usage:
60
82
  | {
@@ -209,6 +231,7 @@ export async function processModelResponseTerminalEvent(input: {
209
231
  externallyBilled: boolean;
210
232
  chargesOpenGeniCredits?: boolean;
211
233
  countsTowardTokenCap?: boolean;
234
+ creditPolicyRevision?: number | undefined;
212
235
  servingCredentialId: string | null;
213
236
  priorSessionCredentialId: string | null;
214
237
  emittedSourceKeys: Set<string>;
@@ -231,6 +254,15 @@ export async function processModelResponseTerminalEvent(input: {
231
254
  if (!terminal) {
232
255
  return { status: "not_response" };
233
256
  }
257
+ // Some providers mirror a terminal response before normalized usage arrives.
258
+ // Claiming that empty raw mirror would discard the SDK's billable response.
259
+ if (
260
+ !terminal.usage &&
261
+ input.event.type === "raw_model_stream_event" &&
262
+ input.event.data.type !== "response_done"
263
+ ) {
264
+ return { status: "not_response" };
265
+ }
234
266
 
235
267
  const responseOrdinal = input.state.responseCount + 1;
236
268
  const sourceKey = modelUsageSourceKey({
@@ -268,6 +300,7 @@ export async function processModelResponseTerminalEvent(input: {
268
300
  turnId: input.turnId,
269
301
  turnAttemptId: input.turnAttemptId,
270
302
  model: input.model,
303
+ creditPolicyRevision: input.creditPolicyRevision,
271
304
  externallyBilled: input.externallyBilled,
272
305
  ...(input.chargesOpenGeniCredits !== undefined
273
306
  ? { chargesOpenGeniCredits: input.chargesOpenGeniCredits }
@@ -374,6 +407,7 @@ export async function processCompactionModelUsageEvent(input: {
374
407
  externallyBilled: boolean;
375
408
  chargesOpenGeniCredits?: boolean;
376
409
  countsTowardTokenCap?: boolean;
410
+ creditPolicyRevision?: number | undefined;
377
411
  servingCredentialId: string | null;
378
412
  priorSessionCredentialId: string | null;
379
413
  emittedSourceKeys: Set<string>;
@@ -416,6 +450,7 @@ export async function processCompactionModelUsageEvent(input: {
416
450
  turnId: input.turnId,
417
451
  turnAttemptId: input.turnAttemptId,
418
452
  model: input.model,
453
+ creditPolicyRevision: input.creditPolicyRevision,
419
454
  externallyBilled: input.externallyBilled,
420
455
  ...(input.chargesOpenGeniCredits !== undefined
421
456
  ? { chargesOpenGeniCredits: input.chargesOpenGeniCredits }
@@ -651,6 +686,7 @@ export async function recordModelUsageAndDebitCredits(
651
686
  externallyBilled: boolean;
652
687
  chargesOpenGeniCredits?: boolean;
653
688
  countsTowardTokenCap?: boolean;
689
+ creditPolicyRevision?: number | undefined;
654
690
  gatewayBilling?: ModelResponseUsage["gatewayBilling"];
655
691
  usage?: ModelUsageInput | ModelCallUsageInput | null;
656
692
  normalizedUsage?: ModelCallUsageNormalization;
@@ -842,6 +878,8 @@ export async function recordModelUsageAndDebitCredits(
842
878
  workspaceId: input.workspaceId,
843
879
  type: "model_usage_debit",
844
880
  requestedAmountMicros: costMicros,
881
+ modelId: canonicalizeConfiguredModelId(settings, input.model),
882
+ creditPolicyRevision: input.creditPolicyRevision,
845
883
  sourceType: "model_response",
846
884
  sourceId: `${input.turnId}:${input.sourceKey}`,
847
885
  idempotencyKey: `credit:model_usage_debit:${input.turnId}:${input.sourceKey}`,
@@ -1090,6 +1090,7 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
1090
1090
  ),
1091
1091
  );
1092
1092
  const compactionPrep = await prepareCompaction({
1093
+ entitlements,
1093
1094
  input,
1094
1095
  settings: capabilitySettings,
1095
1096
  db,
@@ -122,6 +122,7 @@ import {
122
122
  completedToolCallFromSdkEvent,
123
123
  } from "./history";
124
124
  import {
125
+ aggregateCreditPolicyRevision,
125
126
  modelUsageSourceKey,
126
127
  recordCompletedModelCallBeforeOwnershipFences,
127
128
  TurnEventPublisher,
@@ -384,6 +385,8 @@ export async function runTurnStreamAttempt(
384
385
  );
385
386
  let parallelSessionTitle: ReturnType<typeof startParallelSessionTitleGeneration> | null = null;
386
387
  let parallelSessionTitleFinished = false;
388
+ let creditPolicyRevision: number | undefined;
389
+ let titleCreditPolicyRevision: number | undefined;
387
390
  const finishParallelSessionTitle = async (): Promise<void> => {
388
391
  if (parallelSessionTitleFinished) return;
389
392
  parallelSessionTitleFinished = true;
@@ -393,6 +396,7 @@ export async function runTurnStreamAttempt(
393
396
  if (generated.usage) {
394
397
  await processSessionTitleModelUsageEvent({
395
398
  usage: generated.usage,
399
+ creditPolicyRevision: titleCreditPolicyRevision,
396
400
  state: sessionTitleUsageState,
397
401
  dispatchId: modelUsageDispatchId,
398
402
  settings,
@@ -595,7 +599,8 @@ export async function runTurnStreamAttempt(
595
599
  let finalReplyNudged = false;
596
600
  const revalidateModelCallAdmission = async () => {
597
601
  await historySink.reconcileConversationTruth({ requireDurable: true });
598
- await ensureRunAllowedBetweenModelCalls({
602
+ creditPolicyRevision = await ensureRunAllowedBetweenModelCalls({
603
+ modelId: resolvedModel?.configured.id ?? turn.model,
599
604
  settings,
600
605
  db,
601
606
  accountId: input.accountId,
@@ -625,6 +630,7 @@ export async function runTurnStreamAttempt(
625
630
  signal: runtimeCancellationSignal,
626
631
  admit: revalidateModelCallAdmission,
627
632
  });
633
+ const responseCreditPolicyRevisions = new Set<number | undefined>();
628
634
  const responseCountBeforeStream = modelResponseState.responseCount;
629
635
  eventing.batcher = null;
630
636
  // The SDK emits every processed call item for one model response before
@@ -842,7 +848,10 @@ export async function runTurnStreamAttempt(
842
848
  attempt.modelRequestStarted = true;
843
849
  return await runtime.runStream(agent, runInput!, eventing.modelRunSettings, {
844
850
  beforeModelRequest: modelCallAdmission.beforeModelRequest,
845
- onModelResponse: modelCallAdmission.onModelResponse,
851
+ onModelResponse: (event) => {
852
+ responseCreditPolicyRevisions.add(creditPolicyRevision);
853
+ return modelCallAdmission.onModelResponse(event);
854
+ },
846
855
  signal: runtimeCancellationSignal,
847
856
  sandboxEnvironment,
848
857
  onModelVisibleContext: async (snapshot) => {
@@ -1068,6 +1077,7 @@ export async function runTurnStreamAttempt(
1068
1077
  ? await media.retainNativeGeneratedImage(generatedImage)
1069
1078
  : null;
1070
1079
  const responseResult = await processModelResponseTerminalEvent({
1080
+ creditPolicyRevision,
1071
1081
  event: next.value,
1072
1082
  state: modelResponseState,
1073
1083
  dispatchId: modelUsageDispatchId,
@@ -1158,21 +1168,9 @@ export async function runTurnStreamAttempt(
1158
1168
  await historySink.reconcileConversationTruth();
1159
1169
  turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
1160
1170
  modelCheckpointMemoryCollector.schedule(observability);
1161
- await ensureRunAllowedBetweenModelCalls({
1162
- settings,
1163
- db,
1164
- accountId: input.accountId,
1165
- workspaceId: input.workspaceId,
1166
- isExternallyBilledTurn: billingState.isExternallyBilledTurn,
1167
- entitlements,
1168
- chargesOpenGeniCredits: billingState.chargesOpenGeniCredits,
1169
- countsTowardTokenCap: billingState.countsTowardTokenCap,
1170
- initiatingHumanSubjectId: turn.initiatingHumanSubjectId,
1171
- serializedRunState: () =>
1172
- media.compactMediaRunState(String(eventing.stream!.state.toString())),
1173
- });
1174
1171
  }
1175
- // Release only after both the debit and frozen-human admission finish.
1172
+ // Release after settlement. The producer checks fresh admission before
1173
+ // its next request; a final response keeps the revision it ran under.
1176
1174
  modelCallAdmission.settle(next.value);
1177
1175
  const durableSdkEvent = generatedImageReceipt
1178
1176
  ? compactGeneratedImageSdkEvent(next.value, generatedImageReceipt)
@@ -1561,6 +1559,12 @@ export async function runTurnStreamAttempt(
1561
1559
  if (!streamSawPerResponseUsage) {
1562
1560
  const aggregateUsage = eventing.stream.state.usage;
1563
1561
  const normalizedAggregateUsage = normalizeModelCallUsage(aggregateUsage);
1562
+ const aggregatePolicyRevision = aggregateCreditPolicyRevision({
1563
+ responseRevisions: responseCreditPolicyRevisions,
1564
+ lastAdmittedRevision: creditPolicyRevision,
1565
+ chargesOpenGeniCredits: billingState.chargesOpenGeniCredits,
1566
+ totalTokens: normalizedAggregateUsage.totalTokens,
1567
+ });
1564
1568
  const aggregateInput = normalizedAggregateUsage.telemetry.inputTokens;
1565
1569
  const aggregateSourceKey = modelUsageSourceKey({
1566
1570
  responseId: null,
@@ -1583,6 +1587,7 @@ export async function runTurnStreamAttempt(
1583
1587
  leaseLostMessage: "Provider credential lease expired during the active turn",
1584
1588
  recordUsage: async () => {
1585
1589
  const billing = await recordModelUsageAndDebitCredits(settings, db, {
1590
+ creditPolicyRevision: aggregatePolicyRevision,
1586
1591
  accountId: input.accountId,
1587
1592
  workspaceId: input.workspaceId,
1588
1593
  sessionId: input.sessionId,
@@ -1975,6 +1980,7 @@ export async function runTurnStreamAttempt(
1975
1980
  turnExecutionPolicy.providerId,
1976
1981
  turnExecutionPolicy.latencyMode,
1977
1982
  );
1983
+ titleCreditPolicyRevision = creditPolicyRevision;
1978
1984
  parallelSessionTitle = startParallelSessionTitleGeneration({
1979
1985
  signal: runtimeCancellationSignal,
1980
1986
  generate: async (signal) =>
@@ -10,7 +10,7 @@ import {
10
10
  creditDebitAttributionMetadata,
11
11
  deferKnowledgeIndexJob,
12
12
  freezeKnowledgeIndexBillingMode,
13
- getBillingBalance,
13
+ getSpendableCreditBalance,
14
14
  guardPaidKnowledgeIndexPublication,
15
15
  knowledgeIndexBillingActivationTime,
16
16
  readKnowledgeIndexSource,
@@ -211,7 +211,7 @@ export function createKnowledgeIndexingActivities(
211
211
  return;
212
212
  }
213
213
  if (paid && current.nextIndex === 0) {
214
- const balance = await getBillingBalance(lockedDb, claim.accountId);
214
+ const balance = await getSpendableCreditBalance(lockedDb, claim.accountId);
215
215
  if (balance.balanceMicros <= 0) {
216
216
  await waitKnowledgeIndexForFunding(lockedDb, claim);
217
217
  result.deferred++;
@@ -1308,6 +1308,7 @@ export function recordSandboxDeadlineRotationsRequested(
1308
1308
  export const SANDBOX_COMMAND_CONTAINMENT_OUTCOMES = [
1309
1309
  "idle_enrolled",
1310
1310
  "deadline_enrolled",
1311
+ "quiescence_enrolled",
1311
1312
  "resumed_enrolled",
1312
1313
  "not_eligible",
1313
1314
  "inspection_failed",