@opengeni/runtime 4.0.3 → 4.1.0-canary.36239117573001

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 (38) hide show
  1. package/dist/assets/codemode-client.json +1 -1
  2. package/dist/{chunk-GBVN7WSK.js → chunk-ORTF5RRC.js} +371 -28
  3. package/dist/chunk-ORTF5RRC.js.map +1 -0
  4. package/dist/{chunk-LL2YQO5H.js → chunk-YAA7WDNO.js} +638 -71
  5. package/dist/chunk-YAA7WDNO.js.map +1 -0
  6. package/dist/codex-tool-search.d.ts +12 -0
  7. package/dist/context-compaction.d.ts +4 -2
  8. package/dist/index.d.ts +27 -8
  9. package/dist/index.js +18 -2
  10. package/dist/knowledge-model-projection.d.ts +28 -0
  11. package/dist/metrics.d.ts +17 -0
  12. package/dist/model-provider-routing.d.ts +14 -0
  13. package/dist/operational-instructions.d.ts +1 -1
  14. package/dist/provider-quota.d.ts +76 -0
  15. package/dist/sandbox/channel-a.d.ts +43 -0
  16. package/dist/sandbox/index.d.ts +1 -1
  17. package/dist/sandbox/index.js +5 -1
  18. package/dist/tool-result-spill.d.ts +11 -3
  19. package/dist/workspace-tool-gateway.js +2 -2
  20. package/package.json +11 -11
  21. package/src/codex-tool-search.ts +54 -6
  22. package/src/context-compaction.ts +19 -3
  23. package/src/index.ts +277 -56
  24. package/src/knowledge-model-projection.ts +169 -0
  25. package/src/lazy-tool-transport.ts +4 -0
  26. package/src/metrics.ts +26 -0
  27. package/src/model-context-inspector.ts +1 -0
  28. package/src/model-provider-client.ts +37 -16
  29. package/src/model-provider-routing.ts +32 -6
  30. package/src/operational-instructions.ts +1 -1
  31. package/src/provider-quota.ts +412 -0
  32. package/src/sandbox/browser-control-client.ts +70 -25
  33. package/src/sandbox/channel-a.ts +370 -0
  34. package/src/sandbox/index.ts +3 -0
  35. package/src/sandbox/routing/routing-session.ts +25 -4
  36. package/src/tool-result-spill.ts +17 -5
  37. package/dist/chunk-GBVN7WSK.js.map +0 -1
  38. package/dist/chunk-LL2YQO5H.js.map +0 -1
@@ -0,0 +1,169 @@
1
+ import {
2
+ KnowledgeEntryListResponse,
3
+ KnowledgeSavePreparationResponse,
4
+ type AttemptToolIdentity,
5
+ type AttemptToolResult as AttemptToolResultValue,
6
+ } from "@opengeni/contracts";
7
+
8
+ type JsonRecord = Record<string, unknown>;
9
+
10
+ type KnowledgeModelProjection = "entry_list" | "save_preparation";
11
+
12
+ /**
13
+ * The model-visible copy of a Knowledge discovery result.
14
+ *
15
+ * `knowledge_search` (first-party and Docs servers) and `knowledge_prepare_save`
16
+ * return the complete contract to every caller. Codemode and other programmatic
17
+ * callers keep those exact bytes; only the model caller receives this
18
+ * projection (see `projectAttemptToolResultForCaller`). It is the same JSON with
19
+ * bookkeeping and repeated text removed:
20
+ *
21
+ * - entry timestamps, rank score, revision lineage/session/batch/number fields;
22
+ * - defaults: `archived: false`, `change: "upsert"`, `sourceKind: null`, and a
23
+ * collection's `descriptionTruncated: false`;
24
+ * - `publishedRevisionId`/`latestRevisionId` and `revision.entryId` when they
25
+ * repeat `revision.id`/`id` (a differing value is kept);
26
+ * - `revision.preview` only when its complete text already appears verbatim in
27
+ * the title or an excerpt, and title-field excerpts contained in the title;
28
+ * - a collection descriptor's `revisionId` (collections are saved by `id` and
29
+ * `version` and read by `view`; they are never cited as evidence).
30
+ *
31
+ * Everything needed to act is kept: entry and collection IDs, `version` (the
32
+ * `expectedVersion` for an update), `revision.id` (evidence pins), scope,
33
+ * view/outcome status, titles, kinds, group/parent IDs, descriptions, excerpts
34
+ * with their offsets, index status and pagination cursors. Nothing is truncated.
35
+ * A result that is an error, carries structured content, or does not strictly
36
+ * match the contract is returned unchanged.
37
+ */
38
+ export function projectKnowledgeToolResultForModel(
39
+ identity: AttemptToolIdentity,
40
+ result: AttemptToolResultValue,
41
+ ): AttemptToolResultValue {
42
+ const projection = knowledgeModelProjection(identity);
43
+ if (!projection || result.isError === true || result.structuredContent !== undefined) {
44
+ return result;
45
+ }
46
+ if (result.content.length !== 1) return result;
47
+ const [content] = result.content;
48
+ if (content?.type !== "text") return result;
49
+ let value: unknown;
50
+ try {
51
+ value = JSON.parse(content.text);
52
+ } catch {
53
+ return result;
54
+ }
55
+ const compact =
56
+ projection === "entry_list" ? compactEntryList(value) : compactSavePreparation(value);
57
+ if (!compact) return result;
58
+ const text = JSON.stringify(compact);
59
+ if (text.length >= content.text.length) return result;
60
+ return { ...result, content: [{ ...content, text }] };
61
+ }
62
+
63
+ function knowledgeModelProjection(identity: AttemptToolIdentity): KnowledgeModelProjection | null {
64
+ if (
65
+ identity.toolName === "knowledge_search" &&
66
+ (identity.serverId === "opengeni" || identity.serverId === "docs")
67
+ ) {
68
+ return "entry_list";
69
+ }
70
+ if (identity.toolName === "knowledge_prepare_save" && identity.serverId === "opengeni") {
71
+ return "save_preparation";
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function compactEntryList(value: unknown): JsonRecord | null {
77
+ if (!KnowledgeEntryListResponse.safeParse(value).success) return null;
78
+ return compactListResponse(value as JsonRecord);
79
+ }
80
+
81
+ function compactSavePreparation(value: unknown): JsonRecord | null {
82
+ if (!KnowledgeSavePreparationResponse.safeParse(value).success) return null;
83
+ const response = value as JsonRecord & {
84
+ collections: JsonRecord & { entries: JsonRecord[] };
85
+ matches: JsonRecord & { published: JsonRecord; needs_review: JsonRecord };
86
+ };
87
+ return {
88
+ ...response,
89
+ collections: {
90
+ ...response.collections,
91
+ entries: response.collections.entries.map(compactCollection),
92
+ },
93
+ matches: {
94
+ ...response.matches,
95
+ published: compactListResponse(response.matches.published),
96
+ needs_review: compactListResponse(response.matches.needs_review),
97
+ },
98
+ };
99
+ }
100
+
101
+ function compactListResponse(response: JsonRecord): JsonRecord {
102
+ return { ...response, entries: (response.entries as JsonRecord[]).map(compactEntrySummary) };
103
+ }
104
+
105
+ function compactCollection(collection: JsonRecord): JsonRecord {
106
+ const { revisionId: _revisionId, descriptionTruncated, ...kept } = collection;
107
+ return { ...kept, ...(descriptionTruncated === false ? {} : { descriptionTruncated }) };
108
+ }
109
+
110
+ function compactEntrySummary(entry: JsonRecord): JsonRecord {
111
+ const {
112
+ score: _score,
113
+ createdAt: _createdAt,
114
+ updatedAt: _updatedAt,
115
+ archived,
116
+ publishedRevisionId,
117
+ latestRevisionId,
118
+ excerpts,
119
+ revision,
120
+ ...kept
121
+ } = entry;
122
+ const {
123
+ entryId,
124
+ number: _number,
125
+ change,
126
+ previousRevisionId: _previousRevisionId,
127
+ restoredFromRevisionId: _restoredFromRevisionId,
128
+ createdAt: _revisionCreatedAt,
129
+ createdBySessionId: _createdBySessionId,
130
+ reviewBatchId: _reviewBatchId,
131
+ sourceKind,
132
+ preview,
133
+ ...keptRevision
134
+ } = revision as JsonRecord;
135
+ const revisionId = keptRevision.id;
136
+ const excerptList = (excerpts ?? []) as JsonRecord[];
137
+ return {
138
+ ...kept,
139
+ ...(archived === false ? {} : { archived }),
140
+ ...(publishedRevisionId === revisionId ? {} : { publishedRevisionId }),
141
+ ...(latestRevisionId === revisionId ? {} : { latestRevisionId }),
142
+ revision: {
143
+ ...keptRevision,
144
+ ...(entryId === entry.id ? {} : { entryId }),
145
+ ...(change === "upsert" ? {} : { change }),
146
+ ...(sourceKind === null ? {} : { sourceKind }),
147
+ ...(previewAlreadyShown(preview, excerptList) ? {} : { preview }),
148
+ },
149
+ ...(excerpts === undefined ? {} : { excerpts }),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * The preview is the start of the entry's content. It is redundant only when
155
+ * it is empty or a content excerpt that starts at offset 0 already begins with
156
+ * it. The title never counts: a short content (a decision's answer) can be a
157
+ * substring of its title and still be the only place the answer appears.
158
+ */
159
+ function previewAlreadyShown(preview: unknown, excerpts: readonly JsonRecord[]): boolean {
160
+ if (typeof preview !== "string") return false;
161
+ if (preview === "") return true;
162
+ return excerpts.some(
163
+ (excerpt) =>
164
+ excerpt.field === "content" &&
165
+ excerpt.start === 0 &&
166
+ typeof excerpt.text === "string" &&
167
+ excerpt.text.startsWith(preview),
168
+ );
169
+ }
@@ -38,6 +38,10 @@ const ALWAYS_VISIBLE_BASE_TOOL_NAMES: ReadonlySet<string> = new Set([
38
38
  "repository_skill_read",
39
39
  "request_human_input",
40
40
  "list_models",
41
+ // Jev-backed code search. Present only when the deployment and workspace
42
+ // enable it; hiding it behind search would spend a model round trip to find
43
+ // the tool that exists to save round trips.
44
+ "code_search",
41
45
  ]);
42
46
  const DISPATCH_MARKER_KEY = "opengeni.lazy_dispatch.v1";
43
47
  const SEARCH_MARKER_KEY = "opengeni.lazy_search.v1";
package/src/metrics.ts CHANGED
@@ -15,6 +15,23 @@ export const MCP_LIFECYCLE_PHASES = ["connect", "close"] as const;
15
15
  export const MCP_LIFECYCLE_POLICIES = ["strict", "best_effort"] as const;
16
16
  export const MCP_LIFECYCLE_OUTCOMES = ["completed", "failed"] as const;
17
17
 
18
+ /**
19
+ * Closed outcomes of one fresh-box command-readiness replacement decision:
20
+ * the replacement became ready, it also missed readiness, it failed for another
21
+ * typed reason, cancellation won the pause or the replacement, or the turn
22
+ * attempt had already spent its single replacement.
23
+ */
24
+ export const SANDBOX_READINESS_REPLACEMENT_OUTCOMES = [
25
+ "replaced",
26
+ "failed_again",
27
+ "replacement_failed",
28
+ "cancelled",
29
+ "budget_spent",
30
+ ] as const;
31
+
32
+ export type SandboxReadinessReplacementOutcome =
33
+ (typeof SANDBOX_READINESS_REPLACEMENT_OUTCOMES)[number];
34
+
18
35
  export type McpLifecyclePhase = (typeof MCP_LIFECYCLE_PHASES)[number];
19
36
  export type McpLifecyclePolicy = (typeof MCP_LIFECYCLE_POLICIES)[number];
20
37
  export type McpLifecycleOutcome = (typeof MCP_LIFECYCLE_OUTCOMES)[number];
@@ -35,6 +52,15 @@ export type RuntimeMetricsHooks = {
35
52
  backend: string;
36
53
  stage: "exec_readiness" | "sibling_warming";
37
54
  }) => void;
55
+ /**
56
+ * One fresh-box readiness replacement decision. The first readiness miss is
57
+ * still counted by `onSandboxWarmingTimeout`; this hook says whether the
58
+ * single per-turn-attempt replacement happened and whether it worked.
59
+ */
60
+ onSandboxReadinessReplacement?: (input: {
61
+ backend: string;
62
+ outcome: SandboxReadinessReplacementOutcome;
63
+ }) => void;
38
64
  onSandboxProviderApiThrottle?: (input: {
39
65
  backend: string;
40
66
  operation: "create" | "renew";
@@ -101,6 +101,7 @@ const LAYER_TITLES: Record<ModelContextInstructionLayerId, string> = {
101
101
  session_instructions: "Session instructions",
102
102
  workspace_memory: "Workspace memory",
103
103
  codemode: "Codemode",
104
+ code_search: "Code search",
104
105
  git_bindings: "Git credential bindings",
105
106
  genesis_title: "Missing-title directive",
106
107
  sdk_capability_instructions: "SDK capability instructions",
@@ -28,6 +28,7 @@ import { isModelCallFetch, vercelGatewayRoutingFetch } from "./model-provider-tr
28
28
  import { ReplayableJsonOpenAI } from "./replayable-json-body";
29
29
  import { recordModelTransportStarted } from "./model-preparation-diagnostics";
30
30
  import { captureProviderRequestBody } from "./model-request-capture";
31
+ import { withoutQuotaExhaustedRetries } from "./provider-quota";
31
32
 
32
33
  let runtimeMetricsHooks: RuntimeMetricsHooks | null = null;
33
34
 
@@ -85,7 +86,10 @@ export function buildOpenAIClientFromSettings(
85
86
  settings.azureOpenaiAdToken && !settings.azureOpenaiApiKey
86
87
  ? { Authorization: `Bearer ${settings.azureOpenaiAdToken}` }
87
88
  : undefined,
88
- fetch: instrumentedModelFetch(providerId, globalThis.fetch),
89
+ fetch: sdkRetryingModelFetch(
90
+ settings.openaiMaxRetries,
91
+ instrumentedModelFetch(providerId, globalThis.fetch),
92
+ ),
89
93
  },
90
94
  { modelRequestPolicy: azureModelRequestPolicy },
91
95
  );
@@ -94,10 +98,23 @@ export function buildOpenAIClientFromSettings(
94
98
  apiKey: settings.openaiApiKey ?? process.env.OPENAI_API_KEY,
95
99
  ...(settings.openaiBaseUrl ? { baseURL: settings.openaiBaseUrl } : {}),
96
100
  maxRetries: settings.openaiMaxRetries,
97
- fetch: instrumentedModelFetch(providerId, globalThis.fetch),
101
+ fetch: sdkRetryingModelFetch(
102
+ settings.openaiMaxRetries,
103
+ instrumentedModelFetch(providerId, globalThis.fetch),
104
+ ),
98
105
  });
99
106
  }
100
107
 
108
+ /**
109
+ * A client that lets the OpenAI SDK retry 429s must not replay an exhausted
110
+ * provider quota: the refusal cannot clear within the SDK's backoff, and each
111
+ * retry only delays the typed turn failure. Clients with SDK retries disabled
112
+ * need no veto.
113
+ */
114
+ function sdkRetryingModelFetch(maxRetries: number, inner: typeof fetch): typeof fetch {
115
+ return maxRetries > 0 ? withoutQuotaExhaustedRetries(inner) : inner;
116
+ }
117
+
101
118
  /**
102
119
  * One OpenAI client per deployment-scoped provider configuration, built lazily
103
120
  * and cached for the process. Workspace-connection providers are deliberately
@@ -282,6 +299,11 @@ export function buildProviderClient(provider: ResolvedModelProvider, settings: S
282
299
  throw new WorkspaceGatewayUnavailableError();
283
300
  }
284
301
  const anonymousProvider = provider.kind === "anonymous";
302
+ // Gateway and OpenRouter requests can incur upstream work or charges
303
+ // before a retryable response failure reaches this process. Neither
304
+ // transport has a provider idempotency key tied to our durable call,
305
+ // so never let the SDK replay them blindly.
306
+ const registryMaxRetries = gatewayProvider || openRouterProvider ? 0 : settings.openaiMaxRetries;
285
307
  const client = provider.builtin
286
308
  ? buildOpenAIClientFromSettings(settings, provider.id)
287
309
  : provider.kind === "codex-subscription"
@@ -342,24 +364,23 @@ export function buildProviderClient(provider: ResolvedModelProvider, settings: S
342
364
  ? { apiKey: provider.apiKey }
343
365
  : {}),
344
366
  ...(provider.baseUrl ? { baseURL: provider.baseUrl } : {}),
345
- // Gateway and OpenRouter requests can incur upstream work or charges
346
- // before a retryable response failure reaches this process. Neither
347
- // transport has a provider idempotency key tied to our durable call,
348
- // so never let the SDK replay them blindly.
349
- maxRetries: gatewayProvider || openRouterProvider ? 0 : settings.openaiMaxRetries,
367
+ maxRetries: registryMaxRetries,
350
368
  ...(provider.defaultQuery ? { defaultQuery: provider.defaultQuery } : {}),
351
369
  ...(provider.defaultHeaders ? { defaultHeaders: provider.defaultHeaders } : {}),
352
- fetch: anonymousProvider
353
- ? withoutAuthenticationHeaders(
354
- instrumentedModelFetch(provider.id, globalThis.fetch),
355
- )
356
- : gatewayProvider
357
- ? vercelGatewayRoutingFetch(
358
- provider.kind as "vercel-gateway-managed" | "vercel-gateway-workspace",
370
+ fetch: sdkRetryingModelFetch(
371
+ registryMaxRetries,
372
+ anonymousProvider
373
+ ? withoutAuthenticationHeaders(
359
374
  instrumentedModelFetch(provider.id, globalThis.fetch),
360
- gatewayPolicies,
361
375
  )
362
- : instrumentedModelFetch(provider.id, globalThis.fetch),
376
+ : gatewayProvider
377
+ ? vercelGatewayRoutingFetch(
378
+ provider.kind as "vercel-gateway-managed" | "vercel-gateway-workspace",
379
+ instrumentedModelFetch(provider.id, globalThis.fetch),
380
+ gatewayPolicies,
381
+ )
382
+ : instrumentedModelFetch(provider.id, globalThis.fetch),
383
+ ),
363
384
  },
364
385
  { modelRequestPolicy: modelRequestPolicyForProvider(provider, gatewayPolicies) },
365
386
  );
@@ -163,6 +163,16 @@ export class MultiProviderModelProvider implements ModelProvider {
163
163
  constructor(private readonly settings: Settings) {}
164
164
 
165
165
  async getModel(modelName?: string): Promise<Model> {
166
+ return this.resolveBinding(modelName).model;
167
+ }
168
+
169
+ /**
170
+ * The provider, owned client, and upstream model id that `getModel` binds.
171
+ * Standalone requests outside an agent run (the session-title sidecar) use
172
+ * this to call the provider directly instead of the runner-facing
173
+ * `Model.getResponse()`, which requires an active trace.
174
+ */
175
+ resolveBinding(modelName?: string): ModelProviderBinding {
166
176
  if (modelName) {
167
177
  const resolved = resolveTurnModel(
168
178
  settingsForRunScopedModelResolution(this.settings, modelName),
@@ -189,7 +199,12 @@ export class MultiProviderModelProvider implements ModelProvider {
189
199
  ) {
190
200
  throw new XaiSubscriptionUnavailableError(modelName);
191
201
  }
192
- return resolved.model;
202
+ return {
203
+ provider: resolved.provider,
204
+ client: resolved.client,
205
+ model: resolved.model,
206
+ modelId: resolved.configured.upstreamModelId,
207
+ };
193
208
  }
194
209
  // A `codex/<slug>` id only resolves when the per-workspace worker overlay
195
210
  // (settingsWithCodexCredential) has injected the synthetic codex-subscription
@@ -215,14 +230,25 @@ export class MultiProviderModelProvider implements ModelProvider {
215
230
  // JSON parse/stringify transport wrapper on the fallback path.
216
231
  const builtin = configuredProviders(this.settings)[0];
217
232
  if (!builtin) throw new Error("Built-in model provider is unavailable");
218
- return new OpenGeniResponsesModel(
219
- buildProviderClient(builtin, this.settings),
220
- modelName ?? this.settings.openaiModel,
221
- builtin,
222
- );
233
+ const client = buildProviderClient(builtin, this.settings);
234
+ const modelId = modelName ?? this.settings.openaiModel;
235
+ return {
236
+ provider: builtin,
237
+ client,
238
+ model: new OpenGeniResponsesModel(client, modelId, builtin),
239
+ modelId,
240
+ };
223
241
  }
224
242
  }
225
243
 
244
+ export type ModelProviderBinding = {
245
+ provider: ResolvedModelProvider;
246
+ client: OpenAI;
247
+ model: Model;
248
+ /** The id sent on the provider wire (the upstream id for a registry model). */
249
+ modelId: string;
250
+ };
251
+
226
252
  function settingsForRunScopedModelResolution(settings: Settings, modelName: string): Settings {
227
253
  if (modelName !== settings.openaiModel) {
228
254
  return settings;
@@ -156,7 +156,7 @@ Skills are reusable instructions supplied dynamically for the current session. W
156
156
 
157
157
  # Integration setup
158
158
 
159
- Use available integration tools directly. If the task needs an integration you cannot access, discover it with \`capability_catalog_search\` before declaring a setup blocker. For a suitable match with \`setup.nextAction\`, call \`capability_authorization_request\` with the returned capability ID and a brief task-specific rationale to show its Connect card in chat. Requesting the card does not need integration-management permission; the authenticated human must authorize setup. A card is for an integration required by the authorized design, including established or delegated choices. Resolve out-of-scope architecture choices before requesting setup. After setup, rediscover the tools and continue the task, verifying the access it needs. If access remains blocked, explain the specific blocker from the returned facts. If either setup tool is unavailable, report the missing setup path.
159
+ Use available integration tools directly. If access is missing, search \`capability_catalog_search\` first. For a suitable match with \`setup.nextAction\`, call \`capability_authorization_request\` with its ID and a task-specific rationale. If no match exists and the task needs a remote MCP whose exact HTTPS URL the user supplied or reliable documentation establishes, call \`custom_mcp_setup_request\` with its name, URL, and rationale. Never invent URLs or request credentials in chat. Showing either card does not need integration-management permission and grants no access; the authenticated human must authorize setup. A card is for an integration required by the authorized design, including established or delegated choices. Resolve out-of-scope architecture choices before requesting setup. After setup, rediscover tools and verify access. If blocked or a setup tool is unavailable, explain the specific gap.
160
160
 
161
161
  # Session coordination
162
162