@opengeni/runtime 0.2.2 → 0.3.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.
@@ -0,0 +1,149 @@
1
+ import type { AgentInputItem } from "@openai/agents";
2
+
3
+ export const SCREENSHOT_OMITTED_PLACEHOLDER =
4
+ "[screenshot omitted: an older desktop frame — the full image remains in the session event log]";
5
+
6
+ const DATA_IMAGE_BASE64_PATTERN = /data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=_-]+/i;
7
+
8
+ type PathSegment = string | number;
9
+
10
+ type ImageOccurrence = {
11
+ path: PathSegment[];
12
+ replacement: unknown;
13
+ };
14
+
15
+ export type ElideStaleScreenshotsResult<T> = {
16
+ items: T[];
17
+ imageCount: number;
18
+ elidedCount: number;
19
+ };
20
+
21
+ export type ElideStaleScreenshotsOptions = {
22
+ keepLast?: number;
23
+ placeholder?: string;
24
+ };
25
+
26
+ export function elideStaleScreenshotImages<T extends AgentInputItem>(
27
+ items: readonly T[],
28
+ options: ElideStaleScreenshotsOptions = {},
29
+ ): ElideStaleScreenshotsResult<T> {
30
+ const keepLast = Math.max(0, Math.floor(options.keepLast ?? 3));
31
+ const placeholder = options.placeholder ?? SCREENSHOT_OMITTED_PLACEHOLDER;
32
+ const occurrences: ImageOccurrence[] = [];
33
+ for (let i = 0; i < items.length; i += 1) {
34
+ collectItemImageOccurrences(items[i], [i], placeholder, occurrences);
35
+ }
36
+
37
+ const elidedCount = Math.max(0, occurrences.length - keepLast);
38
+ if (elidedCount === 0) {
39
+ return { items: items.slice(), imageCount: occurrences.length, elidedCount: 0 };
40
+ }
41
+
42
+ const cloned = structuredClone(items) as T[];
43
+ for (const occurrence of occurrences.slice(0, elidedCount)) {
44
+ setPath(cloned, occurrence.path, occurrence.replacement);
45
+ }
46
+ return { items: cloned, imageCount: occurrences.length, elidedCount };
47
+ }
48
+
49
+ function collectItemImageOccurrences(
50
+ item: unknown,
51
+ path: PathSegment[],
52
+ placeholder: string,
53
+ out: ImageOccurrence[],
54
+ ): void {
55
+ if (!isRecord(item)) {
56
+ return;
57
+ }
58
+ if (item.type === "message" && (item.role === "user" || item.role === "system")) {
59
+ return;
60
+ }
61
+ if (item.type === "computer_call_result" || item.type === "computer_call_output") {
62
+ collectComputerOutputImages(item, path, placeholder, out);
63
+ return;
64
+ }
65
+ if (item.type === "function_call_result" || item.type === "function_call_output") {
66
+ collectToolResultImages(item.output, [...path, "output"], placeholder, out);
67
+ }
68
+ }
69
+
70
+ function collectComputerOutputImages(
71
+ item: Record<string, unknown>,
72
+ path: PathSegment[],
73
+ placeholder: string,
74
+ out: ImageOccurrence[],
75
+ ): void {
76
+ const output = item.output;
77
+ if (!isRecord(output) || output.type !== "computer_screenshot") {
78
+ return;
79
+ }
80
+ for (const key of ["data", "image_url", "imageUrl"]) {
81
+ if (isImageDataUrl(output[key])) {
82
+ out.push({ path: [...path, "output", key], replacement: placeholder });
83
+ return;
84
+ }
85
+ }
86
+ }
87
+
88
+ function collectToolResultImages(
89
+ value: unknown,
90
+ path: PathSegment[],
91
+ placeholder: string,
92
+ out: ImageOccurrence[],
93
+ ): void {
94
+ if (typeof value === "string") {
95
+ if (isImageDataUrl(value)) {
96
+ out.push({ path, replacement: placeholder });
97
+ }
98
+ return;
99
+ }
100
+ if (Array.isArray(value)) {
101
+ for (let i = 0; i < value.length; i += 1) {
102
+ collectToolResultImages(value[i], [...path, i], placeholder, out);
103
+ }
104
+ return;
105
+ }
106
+ if (!isRecord(value)) {
107
+ return;
108
+ }
109
+ if (value.type === "input_image") {
110
+ for (const key of ["image", "imageUrl", "image_url"]) {
111
+ if (isImageDataUrl(value[key])) {
112
+ out.push({ path, replacement: { type: "input_text", text: placeholder } });
113
+ return;
114
+ }
115
+ }
116
+ }
117
+ for (const key of ["content", "text", "output"]) {
118
+ if (key in value) {
119
+ collectToolResultImages(value[key], [...path, key], placeholder, out);
120
+ }
121
+ }
122
+ }
123
+
124
+ function isImageDataUrl(value: unknown): value is string {
125
+ return typeof value === "string" && DATA_IMAGE_BASE64_PATTERN.test(value);
126
+ }
127
+
128
+ function isRecord(value: unknown): value is Record<string, unknown> {
129
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
130
+ }
131
+
132
+ function setPath(root: unknown, path: PathSegment[], value: unknown): void {
133
+ if (path.length === 0) {
134
+ return;
135
+ }
136
+ let cursor = root;
137
+ for (let i = 0; i < path.length - 1; i += 1) {
138
+ const segment = path[i]!;
139
+ cursor = Array.isArray(cursor)
140
+ ? cursor[segment as number]
141
+ : (cursor as Record<string, unknown>)[segment as string];
142
+ }
143
+ const last = path[path.length - 1]!;
144
+ if (Array.isArray(cursor)) {
145
+ cursor[last as number] = value;
146
+ } else {
147
+ (cursor as Record<string, unknown>)[last as string] = value;
148
+ }
149
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ConfiguredModel, ContextCompactionMode, ModelProviderApi, ResolvedModelProvider, Settings } from "@opengeni/config";
2
- import { AGENT_INSTRUCTIONS_CORE_PLACEHOLDER, collectSandboxEnvironment, contextServerCompactThreshold, firstPartyMcpBaseUrl, parseExposedPorts, resolveContextCompactionMode, resolveModelProvider, sandboxLifecycleHookIds } from "@opengeni/config";
2
+ import { AGENT_INSTRUCTIONS_CORE_PLACEHOLDER, collectSandboxEnvironment, contextInputBudgetTokens, contextServerCompactThreshold, firstPartyMcpBaseUrl, parseExposedPorts, resolveContextCompactionMode, resolveModelProvider, sandboxLifecycleHookIds } from "@opengeni/config";
3
3
  import { CAPABILITY_DESCRIPTORS, isClearedRunStateBlob, signDelegatedAccessToken, type Permission, type ReasoningEffort, type ResourceRef, type SessionEventType, type ToolRef } from "@opengeni/contracts";
4
4
  import {
5
5
  Agent,
@@ -82,8 +82,17 @@ import { dirname, isAbsolute, join, posix as posixPath, relative } from "node:pa
82
82
  import { fileURLToPath } from "node:url";
83
83
 
84
84
  import { computerCallNormalizingFetch, normalizeComputerCallActions, sanitizeHistoryItemsForModel } from "./history-sanitizer";
85
+ import { elideStaleScreenshotImages } from "./image-history";
85
86
  import { installCodexToolSearch } from "./codex-tool-search";
86
- import { enforceInputBudget, estimateItemTokens } from "./context-compaction";
87
+ import {
88
+ CompactionNeededError,
89
+ SUMMARY_BUFFER_TOKENS,
90
+ clientCompactionThresholdTokens,
91
+ enforceInputBudget,
92
+ estimateItemTokens,
93
+ estimateTokens,
94
+ renderCompactionPromptInputForChat,
95
+ } from "./context-compaction";
87
96
  import {
88
97
  createSandboxClient,
89
98
  deserializeSandboxSessionStateEnvelope,
@@ -134,22 +143,34 @@ export type { HistoryItem } from "./history-sanitizer";
134
143
  export { OpenAIChatCompletionsModel, OpenAIResponsesModel } from "@openai/agents";
135
144
 
136
145
  export {
137
- planCompaction,
146
+ CompactionNeededError,
147
+ buildCompactionPromptInput,
148
+ buildCompactionReplacementHistory,
149
+ clientCompactionThresholdTokens,
150
+ decideClientCompaction,
138
151
  enforceInputBudget,
139
152
  buildSummaryItem,
140
- buildCompactionMessages,
153
+ findCompactionNeededError,
141
154
  isCompactionSummary,
142
155
  isUserMessage,
143
156
  findKeepBoundary,
144
157
  estimateTokens,
145
158
  estimateItemTokens,
146
- compactionSummaryText,
147
- renderPrefixTranscript,
159
+ renderCompactionPromptInputForChat,
148
160
  COMPACTION_SUMMARY_MARKER,
161
+ COMPACTION_PROMPT,
162
+ COMPACT_USER_MESSAGE_MAX_TOKENS,
163
+ CLIENT_COMPACTION_TRIGGER_FRACTION,
164
+ SUMMARY_BUFFER_TOKENS,
149
165
  SUMMARY_PREFIX,
150
- SUMMARY_INSTRUCTIONS,
166
+ USER_MESSAGE_TRUNCATION_MARKER,
151
167
  } from "./context-compaction";
152
- export type { CompactionItem, CompactionPlan, PlanCompactionInput } from "./context-compaction";
168
+ export type { ClientCompactionDecision, CompactionItem } from "./context-compaction";
169
+ export {
170
+ elideStaleScreenshotImages,
171
+ SCREENSHOT_OMITTED_PLACEHOLDER,
172
+ } from "./image-history";
173
+ export type { ElideStaleScreenshotsOptions, ElideStaleScreenshotsResult } from "./image-history";
153
174
 
154
175
  ensureReadableStreamFrom();
155
176
 
@@ -420,7 +441,7 @@ export class MultiProviderModelProvider implements ModelProvider {
420
441
 
421
442
  async getModel(modelName?: string): Promise<Model> {
422
443
  if (modelName) {
423
- const resolved = resolveTurnModel(this.settings, modelName);
444
+ const resolved = resolveTurnModel(settingsForRunScopedModelResolution(this.settings, modelName), modelName);
424
445
  if (resolved) {
425
446
  // Fail-loud floor (defense in depth): a `codex/<slug>` id must only ever
426
447
  // resolve through the synthetic codex-subscription provider (which installs
@@ -458,6 +479,27 @@ export class MultiProviderModelProvider implements ModelProvider {
458
479
  }
459
480
  }
460
481
 
482
+ function settingsForRunScopedModelResolution(settings: Settings, modelName: string): Settings {
483
+ if (modelName !== settings.openaiModel) {
484
+ return settings;
485
+ }
486
+ const builtinAllowed = splitOpenaiAllowedModels(settings.openaiAllowedModels);
487
+ const fallbackBuiltin = builtinAllowed.find((id) => id !== modelName);
488
+ if (!fallbackBuiltin) {
489
+ return settings;
490
+ }
491
+ // The worker sets runSettings.openaiModel to the turn's model. For namespaced
492
+ // registry ids configuredModels filters the built-in entry out, but a unique
493
+ // bare registry id would otherwise be claimed by the built-in only because of
494
+ // that per-turn override. Resolve the run-scoped router against the deployment
495
+ // allow-list head instead; real built-in models stay in the allow-list.
496
+ return builtinAllowed.includes(modelName) ? settings : { ...settings, openaiModel: fallbackBuiltin };
497
+ }
498
+
499
+ function splitOpenaiAllowedModels(value: string): string[] {
500
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
501
+ }
502
+
461
503
  /**
462
504
  * A `codex/<slug>` turn reached the model router but the workspace has no active
463
505
  * Codex subscription connected (the worker overlay never injected the synthetic
@@ -500,10 +542,10 @@ export function configureOpenAI(settings: Settings): void {
500
542
 
501
543
  /**
502
544
  * Run the compaction summarizer as one plain, tool-less, non-streaming model
503
- * call against the resolved provider. `system`/`user` come from
504
- * buildCompactionMessages. Returns the trimmed summary text, or null on any
545
+ * call against the resolved provider. `input` is the active history plus
546
+ * Codex's checkpoint prompt. Returns the trimmed summary text, or null on any
505
547
  * failure (the caller treats a failed summarize as "skip compaction this turn"
506
- * never fatal). The call deliberately does NOT request reasoning encryption,
548
+ * - never fatal). The call deliberately does NOT request reasoning encryption,
507
549
  * tools, or server-side compaction; it is a self-contained summarize.
508
550
  *
509
551
  * Provider-aware: the summary always runs on the SAME provider that serves the
@@ -517,22 +559,19 @@ export function configureOpenAI(settings: Settings): void {
517
559
  */
518
560
  export async function summarizeForCompaction(
519
561
  settings: Settings,
520
- messages: { system: string; user: string },
562
+ input: Array<Record<string, unknown>>,
521
563
  options: { client?: OpenAI; api?: ModelProviderApi; maxOutputTokens?: number; model?: string } = {},
522
564
  ): Promise<string | null> {
523
565
  const client = options.client ?? buildOpenAIClientFromSettings(settings);
524
566
  const api = options.api ?? "responses";
525
567
  const model = options.model ?? settings.openaiModel;
526
- const maxTokens = options.maxOutputTokens ?? settings.contextSummaryMaxTokens;
568
+ const maxTokens = options.maxOutputTokens ?? SUMMARY_BUFFER_TOKENS;
527
569
  try {
528
570
  if (api === "chat") {
529
571
  const completion = await client.chat.completions.create({
530
572
  model,
531
573
  max_tokens: maxTokens,
532
- messages: [
533
- { role: "system", content: messages.system },
534
- { role: "user", content: messages.user },
535
- ],
574
+ messages: [{ role: "user", content: renderCompactionPromptInputForChat(input) }],
536
575
  } as any);
537
576
  const text = (completion as { choices?: Array<{ message?: { content?: unknown } }> }).choices?.[0]?.message?.content;
538
577
  const trimmed = typeof text === "string" ? text.trim() : "";
@@ -545,10 +584,7 @@ export async function summarizeForCompaction(
545
584
  // built-in path (api "responses"), so gate it on the built-in provider.
546
585
  ...(settings.openaiProvider === "azure" ? {} : { store: false }),
547
586
  max_output_tokens: maxTokens,
548
- input: [
549
- { role: "system", content: messages.system },
550
- { role: "user", content: messages.user },
551
- ],
587
+ input,
552
588
  } as any);
553
589
  const text = extractResponseOutputText(response);
554
590
  const trimmed = text.trim();
@@ -696,6 +732,14 @@ export type BuildAgentOptions = {
696
732
  // restyle the persona but never drop the goal-loop contract or environment
697
733
  // block.
698
734
  instructionsTemplate?: string;
735
+ // Per-SESSION persona/system instructions (the per-agent-type prompt lever an
736
+ // embedding host supplies at session create). Composed AFTER the workspace
737
+ // instructionsTemplate + the non-bypassable CORE, so it refines the workspace
738
+ // persona for this one session without dropping the goal-loop/environment
739
+ // contract. Rides the SAME instructions channel (system-level) — NEVER a user/
740
+ // timeline message. Omitted ⇒ the composed instructions are byte-identical to
741
+ // a workspace-only persona.
742
+ sessionInstructions?: string;
699
743
  // Skills delivered by enabled capability packs. They join the bundled
700
744
  // skills in the sandbox skill index (mounted under .agents/) so
701
745
  // skills/<name> references resolve like any other indexed skill.
@@ -778,6 +822,27 @@ export function composeAgentInstructions(template: string, workspaceEnvironment?
778
822
  return core ? `${template} ${core}` : template;
779
823
  }
780
824
 
825
+ /**
826
+ * Appends the per-session persona instructions to the already-composed
827
+ * (workspace + CORE) instructions, joined by " " — exactly the join used
828
+ * throughout the persona composition. The session slice is intentionally LAST
829
+ * (session-specific refinement of the workspace persona). An absent/blank value
830
+ * is a no-op that returns the composed string byte-for-byte.
831
+ */
832
+ export function appendSessionInstructions(composed: string, sessionInstructions?: string): string {
833
+ const trimmed = sessionInstructions?.trim();
834
+ return trimmed ? `${composed} ${trimmed}` : composed;
835
+ }
836
+
837
+ /**
838
+ * Appends the one-shot genesis title directive (genesis turn only), joined by
839
+ * " " and always LAST so a white-label persona template or a per-session
840
+ * instruction can't drop it. A no-op when the hint is absent.
841
+ */
842
+ export function appendGenesisTitleDirective(instructions: string, genesisTitleHint?: boolean): string {
843
+ return genesisTitleHint ? `${instructions} ${GENESIS_TITLE_DIRECTIVE}` : instructions;
844
+ }
845
+
781
846
  const agentFileDownloads = new WeakMap<object, SandboxFileDownload[]>();
782
847
  const agentRepositoryCloneHooks = new WeakMap<object, SandboxLifecycleHook[]>();
783
848
  // TOKEN-BROKER (B1): the per-turn git token seed, stashed alongside the agent's
@@ -822,9 +887,21 @@ export function buildOpenGeniAgent(settings: Settings, resources: ResourceRef[],
822
887
  // ownership + workspace-environment block) at the {{core}} marker, or
823
888
  // appends it when the template omits the marker. With the default template
824
889
  // and no environment this is byte-identical to the historical preamble.
825
- instructions: options.genesisTitleHint
826
- ? `${composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment)} ${GENESIS_TITLE_DIRECTIVE}`
827
- : composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
890
+ // Persona composition order (all one system-level instructions string):
891
+ // 1. workspace instructionsTemplate (or deployment default) with the
892
+ // non-bypassable CORE substituted at {{core}} — composeAgentInstructions,
893
+ // 2. + the per-session persona instructions (session-specific, LAST so it
894
+ // refines the workspace persona),
895
+ // 3. + the one-shot genesis title directive (genesis turn only).
896
+ // With no session instructions and no genesis hint this is byte-identical to
897
+ // the historical composed instructions.
898
+ instructions: appendGenesisTitleDirective(
899
+ appendSessionInstructions(
900
+ composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
901
+ options.sessionInstructions,
902
+ ),
903
+ options.genesisTitleHint,
904
+ ),
828
905
  modelSettings: {
829
906
  reasoning: { effort: options.reasoningEffort ?? settings.openaiReasoningEffort, summary: "detailed" },
830
907
  // Server-side compaction (OpenAI platform) requires store=false: the
@@ -1573,6 +1650,7 @@ export type RunAgentStreamOptions = {
1573
1650
  sandboxClient?: unknown;
1574
1651
  sandboxEnvironment?: Record<string, string>;
1575
1652
  onRuntimeEvent?: (event: NormalizedRuntimeEvent) => Promise<void> | void;
1653
+ contextCompactionSignalTokens?: () => number | null | undefined;
1576
1654
  // OWNERSHIP INVERSION (P1.2): an externally-owned, already-live sandbox
1577
1655
  // session resolved by the per-turn resume-by-id path. When present,
1578
1656
  // runAgentStream does NOT build (or resume, or discard) a client — it threads
@@ -1603,6 +1681,11 @@ export type RunAgentStreamOptions = {
1603
1681
  callModelInputFilter?: CallModelInputFilter;
1604
1682
  };
1605
1683
 
1684
+ export type ContextRobustnessFilterOptions = {
1685
+ contextCompactionSignalTokens?: () => number | null | undefined;
1686
+ throwOnCompactionNeeded?: boolean;
1687
+ };
1688
+
1606
1689
  // One-shot directive appended to the agent's system prompt on the genesis turn
1607
1690
  // (see buildOpenGeniAgent's genesisTitleHint). Delivered through the
1608
1691
  // authoritative instructions channel so the model reliably obeys; references
@@ -1656,6 +1739,59 @@ export const normalizeComputerCallsFilter: CallModelInputFilter = ({ modelData }
1656
1739
  ) as unknown as AgentInputItem[],
1657
1740
  });
1658
1741
 
1742
+ export function contextRobustnessFilterForSettings(
1743
+ settings: Settings,
1744
+ options: ContextRobustnessFilterOptions = {},
1745
+ ): CallModelInputFilter {
1746
+ const inputBudgetTokens = modelCallBudgetTokens(settings);
1747
+ const clientCompactionMode = resolveContextCompactionMode(settings) === "client";
1748
+ const compactionThresholdTokens = clientCompactionThresholdTokens(settings);
1749
+ return ({ modelData }) => {
1750
+ const images = elideStaleScreenshotImages(modelData.input);
1751
+ if (images.elidedCount > 0) {
1752
+ console.warn(
1753
+ `per-call image history policy elided ${images.elidedCount} older screenshot image(s), keeping the last ${Math.min(3, images.imageCount)} full image(s)`,
1754
+ );
1755
+ }
1756
+ let input = images.items;
1757
+ if (inputBudgetTokens !== undefined) {
1758
+ const guarded = enforceInputBudget(
1759
+ input as unknown as Array<Record<string, unknown>>,
1760
+ inputBudgetTokens,
1761
+ );
1762
+ if (guarded.trimmed) {
1763
+ console.warn(
1764
+ `per-call budget guard trimmed ${guarded.droppedCount} oldest history item(s) to fit input budget (${inputBudgetTokens} tokens); the over-budget model call was NOT sent`,
1765
+ );
1766
+ input = guarded.items as unknown as AgentInputItem[];
1767
+ }
1768
+ }
1769
+ if (clientCompactionMode && options.throwOnCompactionNeeded) {
1770
+ const reported = options.contextCompactionSignalTokens?.();
1771
+ const hasReported = typeof reported === "number" && reported > 0;
1772
+ const signalTokens = hasReported
1773
+ ? reported
1774
+ : estimateTokens(input as unknown as Array<Record<string, unknown>>);
1775
+ if (signalTokens > compactionThresholdTokens) {
1776
+ throw new CompactionNeededError({
1777
+ signalTokens,
1778
+ thresholdTokens: compactionThresholdTokens,
1779
+ signalSource: hasReported ? "provider" : "estimate",
1780
+ });
1781
+ }
1782
+ }
1783
+ return { ...modelData, input };
1784
+ };
1785
+ }
1786
+
1787
+ function modelCallBudgetTokens(settings: Settings): number | undefined {
1788
+ if (resolveContextCompactionMode(settings) !== "client") {
1789
+ return undefined;
1790
+ }
1791
+ const budget = contextInputBudgetTokens(settings);
1792
+ return budget > 0 ? budget : undefined;
1793
+ }
1794
+
1659
1795
  /**
1660
1796
  * Compose a list of callModelInputFilters into one, applied left-to-right so
1661
1797
  * each sees the prior filter's output.
@@ -1674,13 +1810,18 @@ function composeCallModelInputFilters(filters: CallModelInputFilter[]): CallMode
1674
1810
  * The model-input filter applied before every model call. The computer_call
1675
1811
  * action/actions normalizer is ALWAYS on (the Azure endpoint 400s without it);
1676
1812
  * the provider-item-id strip is layered on top when the configured policy
1677
- * selects it.
1813
+ * selects it; the context-robustness guard then elides stale screenshots on
1814
+ * every mode and applies hard budget trimming only on the client-compaction path.
1678
1815
  */
1679
- export function callModelInputFilterForSettings(settings: Settings): CallModelInputFilter | undefined {
1816
+ export function callModelInputFilterForSettings(
1817
+ settings: Settings,
1818
+ options: ContextRobustnessFilterOptions = {},
1819
+ ): CallModelInputFilter | undefined {
1680
1820
  const filters: CallModelInputFilter[] = [normalizeComputerCallsFilter];
1681
1821
  if (settings.openaiProviderItemIds === "strip") {
1682
1822
  filters.push(stripProviderItemIdsFilter);
1683
1823
  }
1824
+ filters.push(contextRobustnessFilterForSettings(settings, options));
1684
1825
  return composeCallModelInputFilters(filters);
1685
1826
  }
1686
1827
 
@@ -1759,7 +1900,15 @@ export async function runAgentStream(agent: Agent<any, any>, input: PreparedAgen
1759
1900
  // through the client during this run (it is inert for the provided session).
1760
1901
  const decoratedClient = withSandboxLifecycleHooks(resourceClient, ownedHooks, ownedHookContext);
1761
1902
  const ownedFilter = composeCallModelInputFilters(
1762
- [callModelInputFilterForSettings(settings), overrides.callModelInputFilter].filter(
1903
+ [
1904
+ callModelInputFilterForSettings(settings, {
1905
+ throwOnCompactionNeeded: Boolean(overrides.contextCompactionSignalTokens),
1906
+ ...(overrides.contextCompactionSignalTokens
1907
+ ? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens }
1908
+ : {}),
1909
+ }),
1910
+ overrides.callModelInputFilter,
1911
+ ].filter(
1763
1912
  (f): f is CallModelInputFilter => Boolean(f),
1764
1913
  ),
1765
1914
  );
@@ -1806,23 +1955,31 @@ export async function runAgentStream(agent: Agent<any, any>, input: PreparedAgen
1806
1955
  ?? (prepared.serializedRunStateForSandbox && client
1807
1956
  ? await restoredSandboxSessionState(await RunState.fromString(agent, prepared.serializedRunStateForSandbox), client)
1808
1957
  : undefined);
1809
- // Strip provider item ids first, then apply any per-turn filter (genesis
1810
- // title directive). Composed left-to-right so the directive lands on the
1811
- // already-id-stripped input. A callModelInputFilter only shapes the per-call
1812
- // model input, never the persisted run-state history.
1958
+ // Apply the built-in per-call filters (computer-call normalization, optional
1959
+ // provider-id stripping, image/budget guard), then any per-turn filter
1960
+ // (genesis title directive). A callModelInputFilter only shapes the per-call
1961
+ // model input; the SDK persists filtered clones into its session view, while
1962
+ // OpenGeni's durable conversation truth is still reconciled explicitly below.
1813
1963
  const callModelInputFilter = composeCallModelInputFilters(
1814
- [callModelInputFilterForSettings(settings), overrides.callModelInputFilter].filter(
1964
+ [
1965
+ callModelInputFilterForSettings(settings, {
1966
+ throwOnCompactionNeeded: Boolean(overrides.contextCompactionSignalTokens),
1967
+ ...(overrides.contextCompactionSignalTokens
1968
+ ? { contextCompactionSignalTokens: overrides.contextCompactionSignalTokens }
1969
+ : {}),
1970
+ }),
1971
+ overrides.callModelInputFilter,
1972
+ ].filter(
1815
1973
  (f): f is CallModelInputFilter => Boolean(f),
1816
1974
  ),
1817
1975
  );
1818
1976
  const runOptions: Parameters<typeof run>[2] = {
1819
1977
  stream: true,
1820
1978
  maxTurns: settings.agentMaxModelCallsPerTurn,
1821
- // Strip provider-assigned item ids from every model call (turn-start
1822
- // history replay AND mid-turn follow-ups) so requests never depend on the
1823
- // provider's server-side response store. A stored response can vanish
1824
- // between two calls of the same turn, failing the run with 400 "Item with
1825
- // id 'rs_…' not found"; with the ids gone the request is self-contained.
1979
+ // Built-in per-call guard chain: normalize computer calls, optionally strip
1980
+ // provider ids, elide stale screenshots in every mode, and trim to the input
1981
+ // budget on the client-compaction path. This runs for turn-start replay AND
1982
+ // every mid-turn follow-up.
1826
1983
  callModelInputFilter,
1827
1984
  };
1828
1985
  void settings.disableOpenaiTracing;