@opengeni/worker-bundle 1.4.0 → 1.4.4
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 +6 -0
- package/dist/activities/agent-turn/sandbox-provision.d.ts +22 -0
- package/dist/activities/agent-turn/turn-context.d.ts +6 -0
- package/dist/activities/agent-turn/web-search.d.ts +5 -1
- package/dist/activities/codemode-dispatcher.d.ts +8 -0
- package/dist/activities/programmatic-approvals.d.ts +0 -17
- package/dist/activities/sandbox-lease.d.ts +9 -0
- package/dist/{activities-control-D5KVEPHI.js → activities-control-VRFS65FT.js} +59 -13
- package/dist/activities-control-VRFS65FT.js.map +1 -0
- package/dist/{activities-turn-4Z3M7Z3F.js → activities-turn-P2YAW2QW.js} +6038 -5890
- package/dist/activities-turn-P2YAW2QW.js.map +1 -0
- package/dist/{chunk-BMTWULF7.js → chunk-BICGFGO7.js} +92 -17
- package/dist/chunk-BICGFGO7.js.map +1 -0
- package/dist/{chunk-FF4HPY7L.js → chunk-HVNNCWEU.js} +108 -3
- package/dist/chunk-HVNNCWEU.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/observability-metrics.d.ts +52 -14
- package/dist/sandbox-resume.d.ts +3 -0
- package/package.json +21 -21
- package/src/activities/agent-turn/errors.ts +61 -7
- package/src/activities/agent-turn/finalization.ts +16 -0
- 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/stream-attempt.ts +37 -50
- package/src/activities/agent-turn/turn-context.ts +8 -0
- package/src/activities/agent-turn/web-search.ts +135 -6
- package/src/activities/codemode-dispatcher.ts +57 -8
- package/src/activities/programmatic-approvals.ts +0 -26
- package/src/activities/sandbox-lease.ts +77 -10
- package/src/observability-metrics.ts +130 -18
- package/src/sandbox-resume.ts +24 -5
- package/src/sandbox-routing.ts +12 -0
- package/dist/activities-control-D5KVEPHI.js.map +0 -1
- package/dist/activities-turn-4Z3M7Z3F.js.map +0 -1
- package/dist/chunk-BMTWULF7.js.map +0 -1
- package/dist/chunk-FF4HPY7L.js.map +0 -1
|
@@ -1071,6 +1071,8 @@ export const SANDBOX_INVENTORY_PROJECTION_DOMAINS = [
|
|
|
1071
1071
|
"retained_processes",
|
|
1072
1072
|
"expired_drains",
|
|
1073
1073
|
"opensandbox_kubernetes",
|
|
1074
|
+
"modal_provider",
|
|
1075
|
+
"interaction_idle",
|
|
1074
1076
|
] as const;
|
|
1075
1077
|
|
|
1076
1078
|
export type SandboxInventoryProjectionDomain =
|
|
@@ -1228,6 +1230,49 @@ export function recordVerifiedSignupTrialDeploymentFlagGauge(
|
|
|
1228
1230
|
});
|
|
1229
1231
|
}
|
|
1230
1232
|
|
|
1233
|
+
/**
|
|
1234
|
+
* Provider-side Modal inventory reconciled against live leases by the orphan
|
|
1235
|
+
* sweep: `running` = every running box in the app, `unleased` = running boxes
|
|
1236
|
+
* no live lease protects (orphans, counted before termination), and
|
|
1237
|
+
* `lease_missing_instance` = warm leases whose exact provider instance is no
|
|
1238
|
+
* longer running (zombies).
|
|
1239
|
+
*/
|
|
1240
|
+
export function recordModalSandboxInventoryGauges(
|
|
1241
|
+
observability: Observability,
|
|
1242
|
+
inventory: { running: number; unleased: number; liveLeaseInstancesMissing: number },
|
|
1243
|
+
): void {
|
|
1244
|
+
const states = {
|
|
1245
|
+
running: inventory.running,
|
|
1246
|
+
unleased: inventory.unleased,
|
|
1247
|
+
lease_missing_instance: inventory.liveLeaseInstancesMissing,
|
|
1248
|
+
};
|
|
1249
|
+
for (const [state, value] of Object.entries(states)) {
|
|
1250
|
+
observability.setGauge({
|
|
1251
|
+
name: "opengeni_modal_sandbox_inventory",
|
|
1252
|
+
help: "Running Modal sandboxes reconciled against live sandbox leases by the orphan sweep.",
|
|
1253
|
+
labels: { state },
|
|
1254
|
+
value: Math.max(0, value),
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** Warm leases held only by Browser/Computer interaction holders, by time since
|
|
1260
|
+
* the newest interaction activity. Such boxes never drain on their own until the
|
|
1261
|
+
* interaction session ends or the provider deadline. */
|
|
1262
|
+
export function recordInteractionOnlyLeaseGauges(
|
|
1263
|
+
observability: Observability,
|
|
1264
|
+
counts: Record<string, number>,
|
|
1265
|
+
): void {
|
|
1266
|
+
for (const [idleBucket, value] of Object.entries(counts)) {
|
|
1267
|
+
observability.setGauge({
|
|
1268
|
+
name: "opengeni_sandbox_leases_interaction_only",
|
|
1269
|
+
help: "Warm sandbox leases held only by Browser/Computer sessions, by time since last interaction activity.",
|
|
1270
|
+
labels: { idle_bucket: idleBucket },
|
|
1271
|
+
value: Math.max(0, value),
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1231
1276
|
export function recordSandboxOrphansTerminated(observability: Observability, count: number): void {
|
|
1232
1277
|
if (count <= 0) {
|
|
1233
1278
|
return;
|
|
@@ -1629,7 +1674,6 @@ export type TurnStartupPhase =
|
|
|
1629
1674
|
| "post_tool_preparation"
|
|
1630
1675
|
| "agent_construction"
|
|
1631
1676
|
| "post_agent_preparation"
|
|
1632
|
-
| "programmatic_operation_recovery"
|
|
1633
1677
|
| "file_materialization"
|
|
1634
1678
|
| "history_preparation"
|
|
1635
1679
|
| "history_system_update_load"
|
|
@@ -2104,34 +2148,61 @@ function contentDeltaClass(type: SessionEventType): StreamDeltaClass | null {
|
|
|
2104
2148
|
return null;
|
|
2105
2149
|
}
|
|
2106
2150
|
|
|
2107
|
-
// TTFT
|
|
2108
|
-
//
|
|
2109
|
-
//
|
|
2110
|
-
|
|
2151
|
+
// TTFT lives on a human-perceptible scale, but reasoning models legitimately
|
|
2152
|
+
// think for tens of seconds before their first streamed token. The finite
|
|
2153
|
+
// buckets therefore run well past 10s: a top bucket at 10s made every slower
|
|
2154
|
+
// first token land in +Inf, so p50/p99 saturated at exactly "10" and hid how
|
|
2155
|
+
// slow the tail really was.
|
|
2156
|
+
const STREAM_TTFT_BUCKETS = [
|
|
2157
|
+
0.02, 0.05, 0.1, 0.2, 0.35, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300,
|
|
2158
|
+
];
|
|
2111
2159
|
const STREAM_INTER_DELTA_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.35, 0.5, 1, 2, 5];
|
|
2160
|
+
// OpenGeni's own per-request work before the provider sees bytes: admission,
|
|
2161
|
+
// durable history/audit checkpoints, request build. Normally milliseconds to a
|
|
2162
|
+
// second; tens of seconds means our database or consumer is the bottleneck.
|
|
2163
|
+
const MODEL_REQUEST_PRE_DISPATCH_BUCKETS = [
|
|
2164
|
+
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60, 120,
|
|
2165
|
+
];
|
|
2166
|
+
|
|
2167
|
+
/** First streamed content of a provider response. `any` is the first reasoning
|
|
2168
|
+
* or answer delta; `text` is the first answer-text delta. They overlap — never
|
|
2169
|
+
* sum across `content`. */
|
|
2170
|
+
export type ProviderFirstContent = "any" | "text";
|
|
2112
2171
|
|
|
2113
2172
|
/**
|
|
2114
|
-
* Per-
|
|
2115
|
-
*
|
|
2173
|
+
* Per-stream timing tracker fed every normalized runtime event in push order,
|
|
2174
|
+
* plus two producer-side hooks. It separates OUR latency from the PROVIDER's:
|
|
2116
2175
|
*
|
|
2117
|
-
* - `opengeni_stream_ttft_seconds{provider}` —
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2176
|
+
* - `opengeni_stream_ttft_seconds{provider}` — user-perceived (re)start latency:
|
|
2177
|
+
* time from a stream start / structural boundary (tool call, tool result,
|
|
2178
|
+
* completed message, usage frame) to the first streamed content delta. It
|
|
2179
|
+
* mixes our between-call work with provider time; keep it for the absolute
|
|
2180
|
+
* dashboard view, not for provider alerting.
|
|
2181
|
+
* - `opengeni_model_request_pre_dispatch_seconds{provider}` — OUR per-request
|
|
2182
|
+
* latency: SDK model entry (the first admission check) to the literal
|
|
2183
|
+
* provider dispatch. Covers admission, the durable history checkpoint,
|
|
2184
|
+
* credit revalidation, the durable request audit and request build.
|
|
2185
|
+
* - `opengeni_model_provider_ttft_seconds{provider,content}` — the PROVIDER's
|
|
2186
|
+
* latency: literal dispatch to the first streamed `any` (reasoning or text)
|
|
2187
|
+
* delta and to the first answer `text` delta. Includes network, provider
|
|
2188
|
+
* queueing, prompt prefill and reasoning before the first streamed token.
|
|
2123
2189
|
* - `opengeni_stream_inter_delta_gap_seconds{provider,class}` — gap between
|
|
2124
|
-
* consecutive content deltas of the SAME class
|
|
2125
|
-
*
|
|
2126
|
-
* measures only the choppiness of a live token stream.
|
|
2190
|
+
* consecutive content deltas of the SAME class, reset at every structural
|
|
2191
|
+
* event so a gap never spans a tool call or a model boundary.
|
|
2127
2192
|
*
|
|
2128
|
-
*
|
|
2193
|
+
* Dispatch timing deliberately survives structural events: the consumer may
|
|
2194
|
+
* still be persisting the previous response's tool results when the producer
|
|
2195
|
+
* dispatches the next request, but the next request's first delta always
|
|
2196
|
+
* follows its own dispatch. Purely observational and clock-injectable.
|
|
2129
2197
|
*/
|
|
2130
2198
|
export class StreamTimingMetrics {
|
|
2131
2199
|
private readonly now: () => number;
|
|
2132
2200
|
private ttftAnchor: number;
|
|
2133
2201
|
private ttftArmed = true;
|
|
2134
2202
|
private readonly lastDeltaAt = new Map<StreamDeltaClass, number>();
|
|
2203
|
+
private modelEntryAt: number | null = null;
|
|
2204
|
+
private dispatchedAt: number | null = null;
|
|
2205
|
+
private readonly providerFirstRecorded = new Set<ProviderFirstContent>();
|
|
2135
2206
|
|
|
2136
2207
|
constructor(
|
|
2137
2208
|
private readonly observability: Observability,
|
|
@@ -2141,6 +2212,31 @@ export class StreamTimingMetrics {
|
|
|
2141
2212
|
this.ttftAnchor = this.now();
|
|
2142
2213
|
}
|
|
2143
2214
|
|
|
2215
|
+
/** Producer side: the SDK entered a model request (first admission check).
|
|
2216
|
+
* Admission can be re-entered for the same request; keep the earliest. */
|
|
2217
|
+
onModelRequestEntry(): void {
|
|
2218
|
+
if (this.modelEntryAt === null) this.modelEntryAt = this.now();
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/** Producer side: request bytes are about to leave this process. */
|
|
2222
|
+
onProviderDispatch(): void {
|
|
2223
|
+
const at = this.now();
|
|
2224
|
+
if (this.modelEntryAt !== null) {
|
|
2225
|
+
this.observability.observeHistogram({
|
|
2226
|
+
name: "opengeni_model_request_pre_dispatch_seconds",
|
|
2227
|
+
help: "Seconds of OpenGeni work from SDK model-request entry to literal provider dispatch.",
|
|
2228
|
+
buckets: MODEL_REQUEST_PRE_DISPATCH_BUCKETS,
|
|
2229
|
+
labels: { provider: this.options.provider },
|
|
2230
|
+
value: Math.max(0, (at - this.modelEntryAt) / 1000),
|
|
2231
|
+
});
|
|
2232
|
+
this.modelEntryAt = null;
|
|
2233
|
+
}
|
|
2234
|
+
// A transport retry re-dispatches the same request; the first delta then
|
|
2235
|
+
// belongs to the latest dispatch.
|
|
2236
|
+
this.dispatchedAt = at;
|
|
2237
|
+
this.providerFirstRecorded.clear();
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2144
2240
|
onEvent(type: SessionEventType): void {
|
|
2145
2241
|
const deltaClass = contentDeltaClass(type);
|
|
2146
2242
|
if (deltaClass === null) {
|
|
@@ -2156,13 +2252,17 @@ export class StreamTimingMetrics {
|
|
|
2156
2252
|
if (this.ttftArmed) {
|
|
2157
2253
|
this.observability.observeHistogram({
|
|
2158
2254
|
name: "opengeni_stream_ttft_seconds",
|
|
2159
|
-
help: "Seconds from a
|
|
2255
|
+
help: "Seconds from a stream start or structural boundary to the next streamed content delta (user-perceived; includes OpenGeni between-call work).",
|
|
2160
2256
|
buckets: STREAM_TTFT_BUCKETS,
|
|
2161
2257
|
labels: { provider: this.options.provider },
|
|
2162
2258
|
value: Math.max(0, (at - this.ttftAnchor) / 1000),
|
|
2163
2259
|
});
|
|
2164
2260
|
this.ttftArmed = false;
|
|
2165
2261
|
}
|
|
2262
|
+
if (this.dispatchedAt !== null) {
|
|
2263
|
+
this.observeProviderFirst("any", at);
|
|
2264
|
+
if (deltaClass === "message") this.observeProviderFirst("text", at);
|
|
2265
|
+
}
|
|
2166
2266
|
const last = this.lastDeltaAt.get(deltaClass);
|
|
2167
2267
|
if (last !== undefined) {
|
|
2168
2268
|
this.observability.observeHistogram({
|
|
@@ -2175,6 +2275,18 @@ export class StreamTimingMetrics {
|
|
|
2175
2275
|
}
|
|
2176
2276
|
this.lastDeltaAt.set(deltaClass, at);
|
|
2177
2277
|
}
|
|
2278
|
+
|
|
2279
|
+
private observeProviderFirst(content: ProviderFirstContent, at: number): void {
|
|
2280
|
+
if (this.dispatchedAt === null || this.providerFirstRecorded.has(content)) return;
|
|
2281
|
+
this.providerFirstRecorded.add(content);
|
|
2282
|
+
this.observability.observeHistogram({
|
|
2283
|
+
name: "opengeni_model_provider_ttft_seconds",
|
|
2284
|
+
help: "Seconds from literal provider dispatch to the first streamed content delta (any = reasoning or text, text = answer text).",
|
|
2285
|
+
buckets: STREAM_TTFT_BUCKETS,
|
|
2286
|
+
labels: { provider: this.options.provider, content },
|
|
2287
|
+
value: Math.max(0, (at - this.dispatchedAt) / 1000),
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2178
2290
|
}
|
|
2179
2291
|
|
|
2180
2292
|
// Batch shapes: sizes are small integers; durations are the append+publish round
|
package/src/sandbox-resume.ts
CHANGED
|
@@ -166,6 +166,9 @@ export type SandboxResumeServices = {
|
|
|
166
166
|
* replacement. Production uses {@link freshSandboxReadinessReplacementDelayMs}.
|
|
167
167
|
* It may be async so a test can observe the rolled-back lease in between. */
|
|
168
168
|
freshSandboxReadinessReplacementDelayMs?: () => number | Promise<number>;
|
|
169
|
+
/** Test seam: runs immediately after the elected spawner published its box
|
|
170
|
+
* warm, before the final cancellation check. */
|
|
171
|
+
onSpawnedSandboxPublished?: () => void | Promise<void>;
|
|
169
172
|
/**
|
|
170
173
|
* The turn attempt's fresh-box readiness replacement budget. The lazy
|
|
171
174
|
* provisioner may call resumeBoxForTurn again after a typed lease
|
|
@@ -438,10 +441,6 @@ export class SandboxLeaseInstanceLostError extends SandboxLeaseSupersededError {
|
|
|
438
441
|
// user-facing and separate from the lease TTL heartbeat/reaper horizon.
|
|
439
442
|
const WARMING_POLL_INTERVAL_MS = 250;
|
|
440
443
|
|
|
441
|
-
async function sleep(ms: number): Promise<void> {
|
|
442
|
-
await new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
443
|
-
}
|
|
444
|
-
|
|
445
444
|
/**
|
|
446
445
|
* A remote provider may return a sandbox handle before its command router
|
|
447
446
|
* accepts the first exec. The upstream session's yieldTimeMs starts only after
|
|
@@ -1576,6 +1575,12 @@ async function resumeBoxForTurnOnce(
|
|
|
1576
1575
|
if (acquired.role === "spawner") {
|
|
1577
1576
|
const expectedEpoch = acquired.lease.leaseEpoch;
|
|
1578
1577
|
let createdEstablished: EstablishedSandboxSession | null = null;
|
|
1578
|
+
// Set once commitWarmingToWarm publishes this box. From then on the box is
|
|
1579
|
+
// the group's shared, attachable workspace: a later cancellation (Pause,
|
|
1580
|
+
// Steer, worker shutdown) only drops this attempt's holder. Terminating it
|
|
1581
|
+
// here would hand the next attempt a warm lease naming a dead box, whose
|
|
1582
|
+
// exact-id resume then records a lost workspace that never lost anything.
|
|
1583
|
+
let published = false;
|
|
1579
1584
|
let providerCreateOperationId: string | undefined;
|
|
1580
1585
|
let providerCreateBindingKey: string | undefined;
|
|
1581
1586
|
let rematerialization: {
|
|
@@ -2021,13 +2026,21 @@ async function resumeBoxForTurnOnce(
|
|
|
2021
2026
|
await release();
|
|
2022
2027
|
throw new SandboxLeaseSupersededError(ids.sandboxGroupId, expectedEpoch);
|
|
2023
2028
|
}
|
|
2029
|
+
published = true;
|
|
2024
2030
|
holderLeaseHeartbeat = {
|
|
2025
2031
|
expectedEpoch: committed.lease.leaseEpoch,
|
|
2026
2032
|
leaseTtlMs,
|
|
2027
2033
|
};
|
|
2034
|
+
await services.onSpawnedSandboxPublished?.();
|
|
2028
2035
|
throwIfReleasedOrCancelled();
|
|
2029
2036
|
return { established, leaseEpoch: committed.lease.leaseEpoch, release };
|
|
2030
2037
|
} catch (error) {
|
|
2038
|
+
if (published) {
|
|
2039
|
+
// The published warm box stays for the replacement attempt to resume
|
|
2040
|
+
// by exact provider id; ordinary idle drain owns its capture/teardown.
|
|
2041
|
+
await release();
|
|
2042
|
+
throw error;
|
|
2043
|
+
}
|
|
2031
2044
|
if (error instanceof SandboxLeaseSupersededError) {
|
|
2032
2045
|
await terminateEstablishedSandbox(createdEstablished);
|
|
2033
2046
|
await release();
|
|
@@ -2203,7 +2216,13 @@ async function waitForWarm(
|
|
|
2203
2216
|
const deadline = Date.now() + settings.sandboxWarmingTimeoutMs;
|
|
2204
2217
|
let instanceId: string | null = null;
|
|
2205
2218
|
while (Date.now() < deadline) {
|
|
2206
|
-
|
|
2219
|
+
// A cancelled waiter owns no box and must not keep polling for up to the
|
|
2220
|
+
// warming budget: its activity finalizer joins this exact promise.
|
|
2221
|
+
if (!(await sleepUnlessCancelled(WARMING_POLL_INTERVAL_MS, services.cancellationSignal))) {
|
|
2222
|
+
throw services.cancellationSignal?.reason instanceof Error
|
|
2223
|
+
? services.cancellationSignal.reason
|
|
2224
|
+
: new Error("Sandbox warming wait was cancelled with its owning turn attempt");
|
|
2225
|
+
}
|
|
2207
2226
|
const lease = await readLease(db, ids.workspaceId, ids.sandboxGroupId);
|
|
2208
2227
|
if (!lease) {
|
|
2209
2228
|
// Lease vanished (cold-reaped). Re-dispatch from scratch.
|
package/src/sandbox-routing.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
readActiveSandbox,
|
|
41
41
|
resolvePersonalMachineConnectionForAttempt,
|
|
42
42
|
SandboxRetainedProcessPromotionFencedError,
|
|
43
|
+
SandboxRetainedProcessTerminalError,
|
|
43
44
|
SandboxWorkspaceMutationOutputRejectedError,
|
|
44
45
|
type Database,
|
|
45
46
|
type EnrollmentRecord,
|
|
@@ -700,6 +701,17 @@ function afterRetainedProcessMutation(
|
|
|
700
701
|
outcome,
|
|
701
702
|
})
|
|
702
703
|
) {
|
|
704
|
+
if (error.retainedProcessTerminal) {
|
|
705
|
+
// Another authority (the reaper's exact-proof reconciliation) settled
|
|
706
|
+
// this retained process terminal between admission and settlement.
|
|
707
|
+
// The admission is physically settled and its output stays rejected;
|
|
708
|
+
// surface the durable terminal truth so routing reports completion
|
|
709
|
+
// instead of failing the turn. Nothing is replayed.
|
|
710
|
+
throw new SandboxRetainedProcessTerminalError(
|
|
711
|
+
error.retainedProcessTerminal.state,
|
|
712
|
+
error.retainedProcessTerminal.exitCode,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
703
715
|
throw new RoutingMutationOutputRejectedError(op, error.code, { cause: error });
|
|
704
716
|
}
|
|
705
717
|
throw error;
|