@opengeni/runtime 3.0.2-canary.38894290271001 → 3.1.2
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/assets/codemode-client.json +1 -1
- package/dist/{chunk-EQQ3YOKR.js → chunk-7OUG7WBV.js} +165 -121
- package/dist/chunk-7OUG7WBV.js.map +1 -0
- package/dist/{chunk-JDNBDSQV.js → chunk-TE3JIUL6.js} +330 -76
- package/dist/chunk-TE3JIUL6.js.map +1 -0
- package/dist/context-compaction.d.ts +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -2
- package/dist/interaction-tools.d.ts +11 -1
- package/dist/model-call.js +1 -1
- package/dist/model-image-sizing.d.ts +10 -4
- package/dist/workspace-tool-gateway.js +2 -2
- package/package.json +14 -14
- package/src/bundled_site_skills/opengeni-sites/SKILL.md +41 -0
- package/src/codex-tool-search.ts +48 -0
- package/src/context-compaction.ts +51 -31
- package/src/history-sanitizer.ts +3 -0
- package/src/index.ts +26 -16
- package/src/interaction-tools.ts +478 -68
- package/src/model-image-sizing.ts +30 -5
- package/dist/chunk-EQQ3YOKR.js.map +0 -1
- package/dist/chunk-JDNBDSQV.js.map +0 -1
|
@@ -47,7 +47,8 @@ export declare function jsonSerializedUtf8ByteLength(value: unknown): number;
|
|
|
47
47
|
/** Count raw UTF-8 bytes using the standard three-byte lone-surrogate replacement. */
|
|
48
48
|
export declare function utf8ByteLength(value: string): number;
|
|
49
49
|
export declare const UNKNOWN_IMAGE_TOKENS = 4096;
|
|
50
|
-
|
|
50
|
+
/** OpenAI rejects images above 30,000 of its patches (about 36,000 tokens). */
|
|
51
|
+
export declare const MAX_NATIVE_IMAGE_TOKENS = 36000;
|
|
51
52
|
export type NativeImageEstimateReason = "dimensions" | "bounded_fallback";
|
|
52
53
|
export type NativeImageTokenEstimate = {
|
|
53
54
|
tokens: number;
|
|
@@ -265,7 +266,8 @@ export declare function prepareCompactionPromptInput(items: readonly CompactionI
|
|
|
265
266
|
/**
|
|
266
267
|
* Build the active history after compaction:
|
|
267
268
|
* the newest user/system input messages that fit one cumulative, model-bounded budget
|
|
268
|
-
* (prior summaries excluded
|
|
269
|
+
* (prior summaries excluded) plus one marked summary item. Only the newest
|
|
270
|
+
* user message with attachments keeps its images inline.
|
|
269
271
|
* Each distinct session goal context is kept once, and answered transient
|
|
270
272
|
* machine-status batches are left to the summary.
|
|
271
273
|
* Attachments of messages that were not retained are listed in one dated
|
|
@@ -281,8 +283,8 @@ export declare function isRetainedRemoteV2Message(item: unknown): boolean;
|
|
|
281
283
|
* newest retained user/system/developer messages within the CLI 64k budget plus the
|
|
282
284
|
* opaque `{ type: "compaction", encrypted_content }` item.
|
|
283
285
|
*
|
|
284
|
-
* Both modes
|
|
285
|
-
*
|
|
286
|
+
* Both modes keep attachments inline only on the newest user message that has
|
|
287
|
+
* them, and charge its projected image tokens against the budget.
|
|
286
288
|
*/
|
|
287
289
|
export declare function buildRemoteV2ReplacementHistory(items: readonly CompactionItem[], compactionItem: CompactionItem, retainedItemTokens?: (item: CompactionItem) => number, attachmentUploadTimes?: AttachmentUploadTimes): CompactionItem[];
|
|
288
290
|
/** Upload time of each referenced file, frozen into a catalog when compaction creates it. */
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type InteractionToolErrorResult } from "./interaction-tools.js";
|
|
|
2
2
|
export { preparedCompactionRequest, queuePreparedCompaction } from "./prepared-compaction-request.js";
|
|
3
3
|
export { createAnthropicCompactionSizer, fitCompactionPrefix, compactionPrefixCuts, } from "./anthropic-compaction.js";
|
|
4
4
|
export { AnthropicProviderRejection } from "./anthropic-messages.js";
|
|
5
|
+
export { createModelImageSizer, type ModelImageSizerOptions, type SizedImage, } from "./model-image-sizing.js";
|
|
5
6
|
export { runSingleModelCall, SingleModelCallProviderError, SingleModelCallUnsupportedError, type SingleModelCallContentPart, type SingleModelCallFinishReason, type SingleModelCallMessage, type SingleModelCallOptions, type SingleModelCallOutputFormat, type SingleModelCallRequest, type SingleModelCallResult, type SingleModelCallTarget, } from "./single-model-call.js";
|
|
6
7
|
import type { ModelProviderApi, ResolvedModelProvider, Settings } from "@opengeni/config";
|
|
7
8
|
import { RunMcpCredentials } from "./mcp-run-credentials.js";
|
package/dist/index.js
CHANGED
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
modelResponseHasVisibleText,
|
|
118
118
|
modelToolResultFits,
|
|
119
119
|
modelToolResultOverflowError,
|
|
120
|
+
nativeCuaSchema,
|
|
120
121
|
normalizeComputerCallsFilter,
|
|
121
122
|
observeMcpOperation,
|
|
122
123
|
oneShotGenesisTitleInputFilter,
|
|
@@ -186,7 +187,7 @@ import {
|
|
|
186
187
|
workspaceEnvironmentInstructions,
|
|
187
188
|
wrapAttemptToolDefinitions,
|
|
188
189
|
wrapAttemptToolExecute
|
|
189
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-TE3JIUL6.js";
|
|
190
191
|
import {
|
|
191
192
|
ActiveBackendUnresolvableError,
|
|
192
193
|
BROWSER_CONTROL_ADMIN_TOKEN_FILE,
|
|
@@ -491,6 +492,7 @@ import {
|
|
|
491
492
|
compactionSummaryOutputTokens,
|
|
492
493
|
compactionThresholdTokens,
|
|
493
494
|
configureRuntimeMetricsHooks,
|
|
495
|
+
createModelImageSizer,
|
|
494
496
|
decideCompaction,
|
|
495
497
|
describeCompactionProviderRejection,
|
|
496
498
|
elideSupersededViewImagePairs,
|
|
@@ -556,7 +558,7 @@ import {
|
|
|
556
558
|
toolCallIdFromSdkItem,
|
|
557
559
|
vercelGatewayRoutingFetch,
|
|
558
560
|
withClaudeUsageObserver
|
|
559
|
-
} from "./chunk-
|
|
561
|
+
} from "./chunk-7OUG7WBV.js";
|
|
560
562
|
import {
|
|
561
563
|
UnsupportedProtocolJsonValueError,
|
|
562
564
|
normalizeProtocolJsonValue
|
|
@@ -999,6 +1001,7 @@ export {
|
|
|
999
1001
|
createFirstPartyInteractionAttemptToolDefinitions,
|
|
1000
1002
|
createInteractionAttemptToolDefinitions,
|
|
1001
1003
|
createMockSelfhostedOpStream,
|
|
1004
|
+
createModelImageSizer,
|
|
1002
1005
|
createModelVisibleContextCaptureFilter,
|
|
1003
1006
|
createProductionAgentRuntime,
|
|
1004
1007
|
createSandboxClient,
|
|
@@ -1143,6 +1146,7 @@ export {
|
|
|
1143
1146
|
modelToolResultFits,
|
|
1144
1147
|
modelToolResultOverflowError,
|
|
1145
1148
|
modelUsageTokenCountOrNull,
|
|
1149
|
+
nativeCuaSchema,
|
|
1146
1150
|
negotiateCapabilities,
|
|
1147
1151
|
negotiateSelfhostedCapabilities,
|
|
1148
1152
|
nextDurableOpId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InteractionIntervention, type FirstPartyMcpToolName, type Permission, type AttemptToolResult as AttemptToolResultValue } from "@opengeni/contracts";
|
|
1
|
+
import { InteractionIntervention, type FirstPartyMcpToolName, type Permission, type AttemptToolJsonSchema, type AttemptToolResult as AttemptToolResultValue } from "@opengeni/contracts";
|
|
2
2
|
import { type Settings } from "@opengeni/config";
|
|
3
3
|
import type { AttemptToolDefinition, AttemptToolScope } from "@opengeni/codemode";
|
|
4
4
|
import { type InteractionControlFailure, type InteractionTransport } from "@opengeni/sdk";
|
|
@@ -18,6 +18,16 @@ export type CreateInteractionAttemptToolsInput = {
|
|
|
18
18
|
interventionResume?: InteractionInterventionResume | null;
|
|
19
19
|
};
|
|
20
20
|
export declare function createInteractionAttemptToolDefinitions(input: CreateInteractionAttemptToolsInput): AttemptToolDefinition[];
|
|
21
|
+
/**
|
|
22
|
+
* One top-level object schema for every platform variant of a native CUA tool.
|
|
23
|
+
* Model providers require a plain object at the top level of a function schema
|
|
24
|
+
* (a top-level anyOf is rejected or ignored, so the model loses every argument
|
|
25
|
+
* type). Properties are the union across platforms; a property whose platform
|
|
26
|
+
* schemas differ in more than wording keeps those alternatives at property
|
|
27
|
+
* level. Only properties every platform requires stay required. The session's
|
|
28
|
+
* CUA worker still validates arguments against its own exact platform schema.
|
|
29
|
+
*/
|
|
30
|
+
export declare function nativeCuaSchema(variants: Record<string, unknown>[]): AttemptToolJsonSchema;
|
|
21
31
|
export type CreateFirstPartyInteractionAttemptToolsInput = Omit<CreateInteractionAttemptToolsInput, "transport" | "workspaceId" | "sessionId"> & {
|
|
22
32
|
settings: Settings;
|
|
23
33
|
scope: AttemptToolScope;
|
package/dist/model-call.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type SizedImage = {
|
|
1
|
+
export type SizedImage = {
|
|
2
2
|
data: string;
|
|
3
3
|
mediaType: string;
|
|
4
4
|
originalWidth: number;
|
|
@@ -6,6 +6,12 @@ type SizedImage = {
|
|
|
6
6
|
width: number;
|
|
7
7
|
height: number;
|
|
8
8
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export type ModelImageSizerOptions = {
|
|
10
|
+
/**
|
|
11
|
+
* Re-encode a resized JPEG as JPEG at this quality instead of starting with
|
|
12
|
+
* lossless PNG. Photos then stay small without a PNG encode per image.
|
|
13
|
+
*/
|
|
14
|
+
jpegQuality?: number;
|
|
15
|
+
};
|
|
16
|
+
/** A bounded cache. Image count never changes an image's projection. */
|
|
17
|
+
export declare function createModelImageSizer(maxDimension: number, maxEncodedBytes?: number, options?: ModelImageSizerOptions): (data: string, mediaType: string) => Promise<SizedImage>;
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
prepareAgentTools,
|
|
4
4
|
secretArgumentMode,
|
|
5
5
|
sessionMcpApprovalConnectionId
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-TE3JIUL6.js";
|
|
7
7
|
import "./chunk-462CH77G.js";
|
|
8
8
|
import "./chunk-4KFT2G3W.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-7OUG7WBV.js";
|
|
10
10
|
import "./chunk-5ULGMFKR.js";
|
|
11
11
|
import "./chunk-XQ23VCZ4.js";
|
|
12
12
|
import "./chunk-RSHQSLEJ.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -75,19 +75,19 @@
|
|
|
75
75
|
"@openai/agents": "0.14.3",
|
|
76
76
|
"@openai/agents-core": "0.14.3",
|
|
77
77
|
"@openai/agents-extensions": "0.14.3",
|
|
78
|
-
"@opengeni/agent-proto": "3.
|
|
79
|
-
"@opengeni/capabilities": "3.
|
|
80
|
-
"@opengeni/codemode": "3.
|
|
81
|
-
"@opengeni/codex": "3.
|
|
82
|
-
"@opengeni/config": "3.
|
|
83
|
-
"@opengeni/contracts": "3.
|
|
84
|
-
"@opengeni/interaction": "3.
|
|
85
|
-
"@opengeni/network": "3.
|
|
86
|
-
"@opengeni/observability": "3.
|
|
87
|
-
"@opengeni/sdk": "3.
|
|
88
|
-
"@opengeni/tool-gateway": "3.
|
|
89
|
-
"@opengeni/vaults": "3.
|
|
90
|
-
"@opengeni/xai-subscription": "3.
|
|
78
|
+
"@opengeni/agent-proto": "3.1.2",
|
|
79
|
+
"@opengeni/capabilities": "3.1.2",
|
|
80
|
+
"@opengeni/codemode": "3.1.2",
|
|
81
|
+
"@opengeni/codex": "3.1.2",
|
|
82
|
+
"@opengeni/config": "3.1.2",
|
|
83
|
+
"@opengeni/contracts": "3.1.2",
|
|
84
|
+
"@opengeni/interaction": "3.1.2",
|
|
85
|
+
"@opengeni/network": "3.1.2",
|
|
86
|
+
"@opengeni/observability": "3.1.2",
|
|
87
|
+
"@opengeni/sdk": "3.1.2",
|
|
88
|
+
"@opengeni/tool-gateway": "3.1.2",
|
|
89
|
+
"@opengeni/vaults": "3.1.2",
|
|
90
|
+
"@opengeni/xai-subscription": "3.1.2",
|
|
91
91
|
"debug": "4.4.3",
|
|
92
92
|
"modal": "0.9.0",
|
|
93
93
|
"openai": "6.47.0",
|
|
@@ -61,6 +61,45 @@ say which designs are possible and ask before building.
|
|
|
61
61
|
- Keep secrets, Opengeni access keys, OAuth tokens, workspace ids, API base
|
|
62
62
|
URLs, and signed URLs out of the source and generated HTML.
|
|
63
63
|
|
|
64
|
+
## Make new Sites readable by default
|
|
65
|
+
|
|
66
|
+
Choose a clear type scale before building. For new Sites without an explicit
|
|
67
|
+
type scale, start main reading text at 16–18px, controls and table content at
|
|
68
|
+
14–16px, and reserve smaller text for genuinely secondary metadata, not
|
|
69
|
+
descriptions, navigation, labels needed to use a control, or key results.
|
|
70
|
+
Use comfortable line heights (about 1.5–1.7 for prose) and reading widths of
|
|
71
|
+
roughly 60–75 characters. Check the chosen font at its rendered size: a large
|
|
72
|
+
headline does not make small body copy readable.
|
|
73
|
+
|
|
74
|
+
Use `rem`/`em` for text, keep the browser's root font size, and allow browser
|
|
75
|
+
zoom and user text scaling. Do not shrink primary copy on mobile to fit more
|
|
76
|
+
columns; reflow the layout, wrap content, or scroll a wide table instead.
|
|
77
|
+
Keep form text readable and let controls grow with their labels.
|
|
78
|
+
|
|
79
|
+
These are authoring defaults, not a mandatory theme or runtime CSS override.
|
|
80
|
+
Honor a user's deliberate design, retain an existing app's type system, and
|
|
81
|
+
leave embedded components such as `SessionConversation` on their own styles.
|
|
82
|
+
Do not inject a blanket font-size rule into existing Sites or use `!important`
|
|
83
|
+
to force this scale onto a component library. If `frontend-design` is available,
|
|
84
|
+
use it for visual direction alongside these readability defaults.
|
|
85
|
+
|
|
86
|
+
For a new page, this optional starting point applies only to elements you mark
|
|
87
|
+
with these classes; adapt it to the design rather than importing a global reset.
|
|
88
|
+
The sizes below assume the usual 16px browser default and scale with it.
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
.site-copy { font-size: 1.0625rem; line-height: 1.6; max-width: 70ch; }
|
|
92
|
+
.site-control { font: inherit; font-size: 1rem; line-height: 1.5; }
|
|
93
|
+
.site-table { font-size: 0.9375rem; line-height: 1.5; }
|
|
94
|
+
.site-meta { font-size: 0.8125rem; line-height: 1.5; }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Before publishing, inspect actual paragraphs, labels, inputs, buttons, table
|
|
98
|
+
cells and empty/error states at desktop and narrow mobile widths, then with
|
|
99
|
+
text enlarged to 200%. Check computed sizes and look for clipped or overlapping
|
|
100
|
+
text; fix layout pressure rather than making the content smaller. A fixture can
|
|
101
|
+
demonstrate a type scale, but does not prove every generated Site follows it.
|
|
102
|
+
|
|
64
103
|
## Use normal Bun + React
|
|
65
104
|
|
|
66
105
|
Use the repository's pinned Bun version and ordinary package scripts. A minimal
|
|
@@ -672,6 +711,8 @@ with `textContent` or React text, never `innerHTML`.
|
|
|
672
711
|
- The compiled runtime is one self-contained HTML document within the published
|
|
673
712
|
size limit.
|
|
674
713
|
- Desktop and mobile interactions were exercised in the sandbox-local preview.
|
|
714
|
+
- Reading text, controls and tables follow the chosen readable type scale;
|
|
715
|
+
mobile and 200% text scaling do not clip or shrink primary content.
|
|
675
716
|
- Exercise live tools and SDK operations locally through `/__opengeni/site-tools/*`
|
|
676
717
|
wherever the attempt's Codemode authority permits. Report remaining
|
|
677
718
|
viewer-only checks explicitly; those do not by themselves block publication.
|
package/src/codex-tool-search.ts
CHANGED
|
@@ -98,6 +98,43 @@ export function isHarnessControlMcpToolName(registryId: string, modelName: strin
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* First-party interaction tools that are only useful together. Disclosing one
|
|
103
|
+
* member through search discloses the whole working set, so an agent that asks
|
|
104
|
+
* for "a browser" gets open, observe, act, screenshot and tab control in one
|
|
105
|
+
* search instead of rediscovering each schema in its own model round trip.
|
|
106
|
+
* Members are matched by their name after the server separator and only ever
|
|
107
|
+
* drawn from the already-authorized searchable pool.
|
|
108
|
+
*/
|
|
109
|
+
const INTERACTION_MCP_SERVER_ID = "interaction";
|
|
110
|
+
const INTERACTION_TOOL_FAMILIES: readonly (readonly string[])[] = [
|
|
111
|
+
["browser_open", "browser_observe", "browser_act", "browser_screenshot", "browser_tabs"],
|
|
112
|
+
["computer_open", "computer_observe", "computer_act"],
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** The interaction tool's name after its server prefix, or null for any other tool. */
|
|
116
|
+
function interactionToolName(name: string): string | null {
|
|
117
|
+
const prefix = `${INTERACTION_MCP_SERVER_ID}${MCP_TOOL_NAME_SEPARATOR}`;
|
|
118
|
+
return name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The other members of every family a disclosed tool belongs to, in family order. */
|
|
122
|
+
function familyCompanions<T extends Tool & { name: string }>(disclosed: T[], pool: T[]): T[] {
|
|
123
|
+
const disclosedShort = new Set(
|
|
124
|
+
disclosed.map((tool) => interactionToolName(tool.name)).filter((name) => name !== null),
|
|
125
|
+
);
|
|
126
|
+
const wanted = INTERACTION_TOOL_FAMILIES.filter((family) =>
|
|
127
|
+
family.some((member) => disclosedShort.has(member)),
|
|
128
|
+
).flat();
|
|
129
|
+
const companions: T[] = [];
|
|
130
|
+
for (const member of wanted) {
|
|
131
|
+
if (disclosedShort.has(member)) continue;
|
|
132
|
+
const tool = pool.find((candidate) => interactionToolName(candidate.name) === member);
|
|
133
|
+
if (tool && !companions.includes(tool)) companions.push(tool);
|
|
134
|
+
}
|
|
135
|
+
return companions;
|
|
136
|
+
}
|
|
137
|
+
|
|
101
138
|
/**
|
|
102
139
|
* True when a prepared MCP function tool is one of the harness control tools
|
|
103
140
|
* served by the authorized first-party `opengeni` server. These stay in the
|
|
@@ -353,5 +390,16 @@ export function searchToolPool(availableTools: Tool[], rawArguments: unknown): T
|
|
|
353
390
|
bounded.push(tool);
|
|
354
391
|
if (bounded.length >= limit) break;
|
|
355
392
|
}
|
|
393
|
+
// Complete any interaction family a result belongs to. Companions come after
|
|
394
|
+
// the ranked results, so they never displace them, and they still respect the
|
|
395
|
+
// per-tool and total disclosure byte budgets (but not the result count limit).
|
|
396
|
+
for (const tool of familyCompanions(bounded as (Tool & { name: string })[], searchable)) {
|
|
397
|
+
if (bounded.includes(tool)) continue;
|
|
398
|
+
const toolBytes = mcpSerializedSizeBytes(tool);
|
|
399
|
+
if (toolBytes > MCP_MAX_TOOL_DEFINITION_BYTES) continue;
|
|
400
|
+
if (disclosedBytes + toolBytes > MCP_MAX_TOOL_SEARCH_DISCLOSURE_BYTES) continue;
|
|
401
|
+
disclosedBytes += toolBytes;
|
|
402
|
+
bounded.push(tool);
|
|
403
|
+
}
|
|
356
404
|
return bounded;
|
|
357
405
|
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
FileResourceRef,
|
|
23
23
|
MODEL_ATTACHMENT_CATALOG_MARKER,
|
|
24
24
|
MODEL_ATTACHMENT_CATALOG_RENDERED_FIELD,
|
|
25
|
+
MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD,
|
|
25
26
|
MODEL_ATTACHMENT_REFS_FIELD,
|
|
26
27
|
collapseRepeatedSessionGoalContexts,
|
|
27
28
|
expandUnchangedSessionGoalContexts,
|
|
@@ -482,13 +483,15 @@ function jsonStringUtf8ByteLength(value: string): number {
|
|
|
482
483
|
}
|
|
483
484
|
|
|
484
485
|
const IMAGE_LOW_DETAIL_TOKENS = 85;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
486
|
+
// Current vision models price an image by the patches it covers at the
|
|
487
|
+
// resolution it is sent. One token per 28 px patch is a safe bound for both
|
|
488
|
+
// families in use: OpenAI's 32 px patches cost about 1.2 tokens each, which is
|
|
489
|
+
// slightly less per pixel. A photo sent at full resolution therefore costs
|
|
490
|
+
// many thousands of tokens, not a few hundred.
|
|
491
|
+
const IMAGE_PATCH_SIZE = 28;
|
|
490
492
|
export const UNKNOWN_IMAGE_TOKENS = 4_096;
|
|
491
|
-
|
|
493
|
+
/** OpenAI rejects images above 30,000 of its patches (about 36,000 tokens). */
|
|
494
|
+
export const MAX_NATIVE_IMAGE_TOKENS = 36_000;
|
|
492
495
|
|
|
493
496
|
export type NativeImageEstimateReason = "dimensions" | "bounded_fallback";
|
|
494
497
|
export type NativeImageTokenEstimate = {
|
|
@@ -701,27 +704,10 @@ function dimensionsFromImageSource(source: unknown): [number, number] | null {
|
|
|
701
704
|
);
|
|
702
705
|
}
|
|
703
706
|
|
|
704
|
-
function
|
|
705
|
-
let scaledWidth = width;
|
|
706
|
-
let scaledHeight = height;
|
|
707
|
-
const longest = Math.max(scaledWidth, scaledHeight);
|
|
708
|
-
if (longest > IMAGE_HIGH_DETAIL_MAX_SIDE) {
|
|
709
|
-
const scale = IMAGE_HIGH_DETAIL_MAX_SIDE / longest;
|
|
710
|
-
scaledWidth *= scale;
|
|
711
|
-
scaledHeight *= scale;
|
|
712
|
-
}
|
|
713
|
-
const shortest = Math.min(scaledWidth, scaledHeight);
|
|
714
|
-
if (shortest > IMAGE_HIGH_DETAIL_TARGET_SHORT_SIDE) {
|
|
715
|
-
const scale = IMAGE_HIGH_DETAIL_TARGET_SHORT_SIDE / shortest;
|
|
716
|
-
scaledWidth *= scale;
|
|
717
|
-
scaledHeight *= scale;
|
|
718
|
-
}
|
|
719
|
-
const tiles =
|
|
720
|
-
Math.ceil(scaledWidth / IMAGE_HIGH_DETAIL_TILE_SIZE) *
|
|
721
|
-
Math.ceil(scaledHeight / IMAGE_HIGH_DETAIL_TILE_SIZE);
|
|
707
|
+
function patchImageTokens(width: number, height: number): number {
|
|
722
708
|
return Math.min(
|
|
723
709
|
MAX_NATIVE_IMAGE_TOKENS,
|
|
724
|
-
|
|
710
|
+
Math.max(1, Math.ceil(width / IMAGE_PATCH_SIZE) * Math.ceil(height / IMAGE_PATCH_SIZE)),
|
|
725
711
|
);
|
|
726
712
|
}
|
|
727
713
|
|
|
@@ -755,7 +741,7 @@ export function estimateNativeImageTokens(input: {
|
|
|
755
741
|
};
|
|
756
742
|
}
|
|
757
743
|
return {
|
|
758
|
-
tokens:
|
|
744
|
+
tokens: patchImageTokens(width, height),
|
|
759
745
|
width,
|
|
760
746
|
height,
|
|
761
747
|
detail,
|
|
@@ -1680,10 +1666,41 @@ function isPortableRetentionCandidate(item: CompactionItem): boolean {
|
|
|
1680
1666
|
return isPortableInputMessage(item) && !isCompactionSummary(item) && !isAttachmentCatalog(item);
|
|
1681
1667
|
}
|
|
1682
1668
|
|
|
1669
|
+
function hasAttachmentRefs(item: CompactionItem): boolean {
|
|
1670
|
+
const raw = item[MODEL_ATTACHMENT_REFS_FIELD];
|
|
1671
|
+
return Array.isArray(raw) && raw.length > 0;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Only the newest user message that carries attachments keeps them inline
|
|
1676
|
+
* after compaction. Older retained messages drop their references, so their
|
|
1677
|
+
* files are listed in the dated attachment catalog instead of being replayed
|
|
1678
|
+
* as images on every later request. Their text is unchanged and they are
|
|
1679
|
+
* charged only for that text.
|
|
1680
|
+
*/
|
|
1681
|
+
function withOnlyNewestAttachmentsInline(items: readonly CompactionItem[]): CompactionItem[] {
|
|
1682
|
+
let newest = -1;
|
|
1683
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
1684
|
+
const item = items[index]!;
|
|
1685
|
+
if (itemRole(item) === "user" && !isAttachmentCatalog(item) && hasAttachmentRefs(item)) {
|
|
1686
|
+
newest = index;
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return items.map((item, index) => {
|
|
1691
|
+
if (index === newest || isAttachmentCatalog(item) || !hasAttachmentRefs(item)) return item;
|
|
1692
|
+
const stripped: CompactionItem = { ...item };
|
|
1693
|
+
delete stripped[MODEL_ATTACHMENT_REFS_FIELD];
|
|
1694
|
+
delete stripped[MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD];
|
|
1695
|
+
return stripped;
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1683
1699
|
/**
|
|
1684
1700
|
* Build the active history after compaction:
|
|
1685
1701
|
* the newest user/system input messages that fit one cumulative, model-bounded budget
|
|
1686
|
-
* (prior summaries excluded
|
|
1702
|
+
* (prior summaries excluded) plus one marked summary item. Only the newest
|
|
1703
|
+
* user message with attachments keeps its images inline.
|
|
1687
1704
|
* Each distinct session goal context is kept once, and answered transient
|
|
1688
1705
|
* machine-status batches are left to the summary.
|
|
1689
1706
|
* Attachments of messages that were not retained are listed in one dated
|
|
@@ -1701,7 +1718,10 @@ export function buildCompactionReplacementHistory(
|
|
|
1701
1718
|
0,
|
|
1702
1719
|
Math.min(COMPACT_USER_MESSAGE_MAX_TOKENS, retainedMessageBudgetTokens),
|
|
1703
1720
|
);
|
|
1704
|
-
const prepared = prepareRetainedInputs(
|
|
1721
|
+
const prepared = prepareRetainedInputs(
|
|
1722
|
+
withOnlyNewestAttachmentsInline(items),
|
|
1723
|
+
isPortableRetentionCandidate,
|
|
1724
|
+
);
|
|
1705
1725
|
for (let index = prepared.charged.length - 1; index >= 0 && remaining > 0; index -= 1) {
|
|
1706
1726
|
const item = prepared.charged[index]!;
|
|
1707
1727
|
if (!isPortableRetentionCandidate(item) || prepared.skip(index)) {
|
|
@@ -1763,8 +1783,8 @@ export function isRetainedRemoteV2Message(item: unknown): boolean {
|
|
|
1763
1783
|
* newest retained user/system/developer messages within the CLI 64k budget plus the
|
|
1764
1784
|
* opaque `{ type: "compaction", encrypted_content }` item.
|
|
1765
1785
|
*
|
|
1766
|
-
* Both modes
|
|
1767
|
-
*
|
|
1786
|
+
* Both modes keep attachments inline only on the newest user message that has
|
|
1787
|
+
* them, and charge its projected image tokens against the budget.
|
|
1768
1788
|
*/
|
|
1769
1789
|
export function buildRemoteV2ReplacementHistory(
|
|
1770
1790
|
items: readonly CompactionItem[],
|
|
@@ -1780,7 +1800,7 @@ export function buildRemoteV2ReplacementHistory(
|
|
|
1780
1800
|
const retainedReversed: CompactionItem[] = [];
|
|
1781
1801
|
let remaining = REMOTE_V2_RETAINED_MESSAGE_TOKEN_BUDGET;
|
|
1782
1802
|
const prepared = prepareRetainedInputs(
|
|
1783
|
-
items,
|
|
1803
|
+
withOnlyNewestAttachmentsInline(items),
|
|
1784
1804
|
(item) => readSkillCatalogContext(item) === null && isRetainedRemoteV2Message(item),
|
|
1785
1805
|
);
|
|
1786
1806
|
for (let index = prepared.charged.length - 1; index >= 0 && remaining > 0; index -= 1) {
|
package/src/history-sanitizer.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
MODEL_ATTACHMENT_CATALOG_MARKER,
|
|
37
37
|
MODEL_ATTACHMENT_CATALOG_RENDERED_FIELD,
|
|
38
|
+
MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD,
|
|
38
39
|
MODEL_ATTACHMENT_REFS_FIELD,
|
|
39
40
|
MODEL_TIMELINE_ANNOTATIONS_FIELD,
|
|
40
41
|
} from "@opengeni/contracts";
|
|
@@ -340,6 +341,7 @@ export function stripInternalModelMetadata<T extends HistoryItem>(item: T): T {
|
|
|
340
341
|
if (
|
|
341
342
|
!(MODEL_ATTACHMENT_CATALOG_MARKER in withoutResume) &&
|
|
342
343
|
!(MODEL_ATTACHMENT_CATALOG_RENDERED_FIELD in withoutResume) &&
|
|
344
|
+
!(MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD in withoutResume) &&
|
|
343
345
|
!(MODEL_ATTACHMENT_REFS_FIELD in withoutResume) &&
|
|
344
346
|
!(MODEL_TIMELINE_ANNOTATIONS_FIELD in withoutResume)
|
|
345
347
|
) {
|
|
@@ -348,6 +350,7 @@ export function stripInternalModelMetadata<T extends HistoryItem>(item: T): T {
|
|
|
348
350
|
const clone = { ...withoutResume } as Record<string, unknown>;
|
|
349
351
|
delete clone[MODEL_ATTACHMENT_CATALOG_MARKER];
|
|
350
352
|
delete clone[MODEL_ATTACHMENT_CATALOG_RENDERED_FIELD];
|
|
353
|
+
delete clone[MODEL_ATTACHMENT_IMAGE_MAX_DIMENSION_FIELD];
|
|
351
354
|
delete clone[MODEL_ATTACHMENT_REFS_FIELD];
|
|
352
355
|
delete clone[MODEL_TIMELINE_ANNOTATIONS_FIELD];
|
|
353
356
|
return clone as T;
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,11 @@ export {
|
|
|
15
15
|
compactionPrefixCuts,
|
|
16
16
|
} from "./anthropic-compaction";
|
|
17
17
|
export { AnthropicProviderRejection } from "./anthropic-messages";
|
|
18
|
+
export {
|
|
19
|
+
createModelImageSizer,
|
|
20
|
+
type ModelImageSizerOptions,
|
|
21
|
+
type SizedImage,
|
|
22
|
+
} from "./model-image-sizing";
|
|
18
23
|
import { runSingleModelCall, type SingleModelCallTarget } from "./single-model-call";
|
|
19
24
|
export {
|
|
20
25
|
runSingleModelCall,
|
|
@@ -33,6 +38,7 @@ import { instrumentedModelFetch } from "./model-provider-client";
|
|
|
33
38
|
import type { ModelProviderApi, ResolvedModelProvider, Settings } from "@opengeni/config";
|
|
34
39
|
import { isRunMcpCredentialError, RunMcpCredentials } from "./mcp-run-credentials";
|
|
35
40
|
import {
|
|
41
|
+
mcpToolRecommendsApproval,
|
|
36
42
|
normalizeCredentialProviderMcpUrl,
|
|
37
43
|
toolPolicyActionName,
|
|
38
44
|
toolReviewContextFromSchema,
|
|
@@ -3584,6 +3590,7 @@ function installMcpApprovalPolicy(
|
|
|
3584
3590
|
inputSchema: unknown;
|
|
3585
3591
|
title?: string;
|
|
3586
3592
|
accountLabel?: string | undefined;
|
|
3593
|
+
recommendsApproval: boolean;
|
|
3587
3594
|
readReview: ToolReviewReader;
|
|
3588
3595
|
}
|
|
3589
3596
|
>();
|
|
@@ -3602,6 +3609,7 @@ function installMcpApprovalPolicy(
|
|
|
3602
3609
|
serverId: server.registryId,
|
|
3603
3610
|
toolName: await server.unprefixedToolName(descriptor.name),
|
|
3604
3611
|
inputSchema: descriptor.inputSchema,
|
|
3612
|
+
recommendsApproval: mcpToolRecommendsApproval(descriptor),
|
|
3605
3613
|
readReview: (name, args) => server.reviewContext(name, args),
|
|
3606
3614
|
...(descriptor.title ? { title: descriptor.title } : {}),
|
|
3607
3615
|
...(accountLabel ? { accountLabel } : {}),
|
|
@@ -3622,8 +3630,12 @@ function installMcpApprovalPolicy(
|
|
|
3622
3630
|
const unprefixed = identity?.toolName ?? tool.name.slice(policy.prefix.length);
|
|
3623
3631
|
const originalNeedsApproval = tool.needsApproval.bind(tool);
|
|
3624
3632
|
const originalInvoke = tool.invoke.bind(tool);
|
|
3625
|
-
|
|
3626
|
-
|
|
3633
|
+
// The recommendation: the session's requireApproval or the tool's own
|
|
3634
|
+
// tools/list `_meta`. Either one asking makes Ask the default.
|
|
3635
|
+
const recommendsApproval =
|
|
3636
|
+
mcpToolRequiresApproval(policy.requireApproval, unprefixed) ||
|
|
3637
|
+
identity?.recommendsApproval === true;
|
|
3638
|
+
const legacyApproval = !policy.connectorBacked && recommendsApproval;
|
|
3627
3639
|
if (!policy.connectorBacked && !legacyApproval && !connectorActionPolicy) {
|
|
3628
3640
|
return tool;
|
|
3629
3641
|
}
|
|
@@ -3645,9 +3657,7 @@ function installMcpApprovalPolicy(
|
|
|
3645
3657
|
}),
|
|
3646
3658
|
actionName: toolPolicyActionName(unprefixed, identity?.inputSchema, args),
|
|
3647
3659
|
...(legacyApproval ? { approvalMode: "session_mcp" as const } : {}),
|
|
3648
|
-
defaultDecision:
|
|
3649
|
-
? "ask"
|
|
3650
|
-
: "allow",
|
|
3660
|
+
defaultDecision: recommendsApproval ? "ask" : "allow",
|
|
3651
3661
|
};
|
|
3652
3662
|
};
|
|
3653
3663
|
return {
|
|
@@ -3659,8 +3669,7 @@ function installMcpApprovalPolicy(
|
|
|
3659
3669
|
) => {
|
|
3660
3670
|
if (!connectorActionPolicy) {
|
|
3661
3671
|
return (
|
|
3662
|
-
|
|
3663
|
-
(await originalNeedsApproval(runContext, parsedInput, callId))
|
|
3672
|
+
recommendsApproval || (await originalNeedsApproval(runContext, parsedInput, callId))
|
|
3664
3673
|
);
|
|
3665
3674
|
}
|
|
3666
3675
|
if (!callId) {
|
|
@@ -3679,7 +3688,7 @@ function installMcpApprovalPolicy(
|
|
|
3679
3688
|
// An explicit Allow is a decision, not an exemption from one of several floors.
|
|
3680
3689
|
const requiresApproval = preparation.managed
|
|
3681
3690
|
? preparation.decision === "ask"
|
|
3682
|
-
:
|
|
3691
|
+
: recommendsApproval;
|
|
3683
3692
|
if (requiresApproval) approvalRequiredCallIds.add(callId);
|
|
3684
3693
|
else approvalRequiredCallIds.delete(callId);
|
|
3685
3694
|
return requiresApproval;
|
|
@@ -3936,7 +3945,9 @@ function installInteractionInterventionPolicy(
|
|
|
3936
3945
|
|
|
3937
3946
|
/**
|
|
3938
3947
|
* Translate the canonical decision into the model SDK interruption protocol.
|
|
3939
|
-
* Legacy requireApproval
|
|
3948
|
+
* Legacy requireApproval and a tool's `opengeni/recommendedPermission` `_meta`
|
|
3949
|
+
* are recommendations only when no explicit choice exists. Every external MCP
|
|
3950
|
+
* server is wrapped so a tool's own recommendation holds even without hooks.
|
|
3940
3951
|
* Standalone runtimes without policy hooks retain their configured behavior;
|
|
3941
3952
|
* they cannot execute an approval-gated call without the durable authority.
|
|
3942
3953
|
* Prepared server/tool identities take precedence over longest-prefix legacy
|
|
@@ -3953,7 +3964,7 @@ function applyMcpApprovalPolicy(
|
|
|
3953
3964
|
.filter(
|
|
3954
3965
|
(server) =>
|
|
3955
3966
|
Boolean(server.connectionRef) ||
|
|
3956
|
-
|
|
3967
|
+
attemptToolSource(server.id) === "mcp" ||
|
|
3957
3968
|
server.requireApproval === true ||
|
|
3958
3969
|
(Array.isArray(server.requireApproval) && server.requireApproval.length > 0),
|
|
3959
3970
|
)
|
|
@@ -5851,10 +5862,7 @@ function installAttemptConnectorActionGatewayLifecycle(
|
|
|
5851
5862
|
definition.inputSchema,
|
|
5852
5863
|
arguments_,
|
|
5853
5864
|
),
|
|
5854
|
-
defaultDecision:
|
|
5855
|
-
attemptToolApproval(config!, definition.identity.toolName) === "human"
|
|
5856
|
-
? "ask"
|
|
5857
|
-
: "allow",
|
|
5865
|
+
defaultDecision: definition.approval === "human" ? "ask" : "allow",
|
|
5858
5866
|
};
|
|
5859
5867
|
}
|
|
5860
5868
|
: (approvalId: string, arguments_: unknown): ConnectorActionToolCall => ({
|
|
@@ -6163,7 +6171,7 @@ async function prepareToolGatewayDefinitionsFromServers(
|
|
|
6163
6171
|
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
6164
6172
|
...(tool.icons ? { icons: tool.icons } : {}),
|
|
6165
6173
|
source: attemptToolSource(server.registryId),
|
|
6166
|
-
approval: attemptToolApproval(config, toolName),
|
|
6174
|
+
approval: attemptToolApproval(config, toolName, tool),
|
|
6167
6175
|
effectAuthorityDigest: digestCanonicalJson({
|
|
6168
6176
|
version: 1,
|
|
6169
6177
|
serverId: server.registryId,
|
|
@@ -6331,10 +6339,12 @@ function attemptToolSource(serverId: string): ToolGatewayDefinition["source"] {
|
|
|
6331
6339
|
function attemptToolApproval(
|
|
6332
6340
|
config: Settings["mcpServers"][number],
|
|
6333
6341
|
toolName: string,
|
|
6342
|
+
tool: object,
|
|
6334
6343
|
): ToolGatewayDefinition["approval"] {
|
|
6335
6344
|
if (
|
|
6336
6345
|
config.requireApproval === true ||
|
|
6337
|
-
(Array.isArray(config.requireApproval) && config.requireApproval.includes(toolName))
|
|
6346
|
+
(Array.isArray(config.requireApproval) && config.requireApproval.includes(toolName)) ||
|
|
6347
|
+
(attemptToolSource(config.id) === "mcp" && mcpToolRecommendsApproval(tool))
|
|
6338
6348
|
) {
|
|
6339
6349
|
return "human";
|
|
6340
6350
|
}
|