@opengeni/runtime 3.0.1 → 4.0.0-canary.35585322809001
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 -0
- package/dist/chunk-4A4XTKKX.js +1 -0
- package/dist/{chunk-RIJ6DCYS.js → chunk-HCGLD7NH.js} +9817 -9494
- package/dist/chunk-HCGLD7NH.js.map +1 -0
- package/dist/{chunk-FHGLYLBL.js → chunk-SPT3PLMX.js} +1 -87
- package/dist/chunk-SPT3PLMX.js.map +1 -0
- package/dist/{chunk-GB763TSV.js → chunk-TE27J65Q.js} +1 -1
- package/dist/chunk-TE27J65Q.js.map +1 -0
- package/dist/{chunk-46BFUGFL.js → chunk-WIHICNWS.js} +1660 -388
- package/dist/chunk-WIHICNWS.js.map +1 -0
- package/dist/{chunk-UU6EJR52.js → chunk-YU4VRTR4.js} +2 -2
- package/dist/chunk-YU4VRTR4.js.map +1 -0
- package/dist/chunk-ZG2YH25H.js +94 -0
- package/dist/chunk-ZG2YH25H.js.map +1 -0
- package/dist/codex-tool-search.d.ts +5 -4
- package/dist/context-compaction.d.ts +19 -0
- package/dist/gmail-rest-mcp.js +1 -1
- package/dist/index.d.ts +30 -22
- package/dist/index.js +25 -5
- package/dist/lazy-manifest-OEYEHNVY.js +45 -0
- package/dist/lazy-manifest-OEYEHNVY.js.map +1 -0
- package/dist/lazy-manifest.d.ts +6 -0
- package/dist/lazy-tool-transport.d.ts +3 -2
- package/dist/{localSources-PVFCAPN2.js → localSources-TWHTEKGO.js} +3 -2
- package/dist/localSources-TWHTEKGO.js.map +1 -0
- package/dist/metrics.d.ts +6 -0
- package/dist/operational-instructions.d.ts +1 -1
- package/dist/prepared-compaction-request.d.ts +8 -0
- package/dist/provider-history-adapter.d.ts +2 -2
- package/dist/runtime-skills.d.ts +3 -10
- package/dist/sandbox/codemode-client.d.ts +17 -0
- package/dist/sandbox/codemode-token.d.ts +3 -3
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.js +8 -2
- package/dist/sandbox/materialization-verification-error.d.ts +27 -0
- package/dist/sandbox/provider-command-session.d.ts +44 -2
- package/dist/sandbox/providers/modal-command-control.d.ts +32 -29
- package/dist/sandbox/providers/modal-command-router-wire.d.ts +49 -0
- package/dist/sandbox/providers/modal-command-session.d.ts +2 -1
- package/dist/sandbox/providers/modal-legacy-command-control.d.ts +50 -0
- package/dist/sandbox/providers/modal-materialization-verification.d.ts +7 -0
- package/dist/sandbox/providers/modal.d.ts +0 -8
- package/dist/sandbox/routing/routing-session.d.ts +21 -0
- package/dist/sandbox/turn-tool-cancellation.d.ts +1 -0
- package/dist/skill-library.d.ts +0 -6
- package/dist/skill-library.js +1 -1
- package/dist/workspace-tool-gateway.js +7 -5
- package/package.json +13 -11
- package/src/bundled_artifact_skills/opengeni-documents/SKILL.md +19 -0
- package/src/bundled_artifact_skills/opengeni-documents/references/api.md +22 -0
- package/src/bundled_default_skills/opengeni-client/SKILL.md +349 -0
- package/src/bundled_default_skills/opengeni-client/agents/openai.yaml +4 -0
- package/src/bundled_default_skills/opengeni-client/references/api-workflows.md +256 -0
- package/src/bundled_default_skills/opengeni-client/references/compatibility-and-troubleshooting.md +84 -0
- package/src/bundled_default_skills/opengeni-client/references/customer-skill-template.md +96 -0
- package/src/bundled_default_skills/opengeni-client/references/data-tools-and-credentials.md +99 -0
- package/src/bundled_default_skills/opengeni-client/references/discovery-and-autonomy.md +81 -0
- package/src/bundled_default_skills/opengeni-client/references/external-users-and-connect.md +457 -0
- package/src/bundled_default_skills/opengeni-client/references/isolation-and-authorization.md +77 -0
- package/src/bundled_default_skills/opengeni-client/references/product-integration-shapes.md +232 -0
- package/src/bundled_default_skills/opengeni-client/references/product-shapes-and-ui.md +116 -0
- package/src/bundled_default_skills/opengeni-client/references/runtime-profile-and-verification.md +157 -0
- package/src/bundled_default_skills/opengeni-help/SKILL.md +101 -0
- package/src/bundled_management_skills/opengeni-skills/SKILL.md +33 -3
- package/src/codex-tool-search.ts +13 -5
- package/src/context-compaction.ts +87 -4
- package/src/gmail-rest-mcp.ts +1 -2
- package/src/index.ts +363 -131
- package/src/lazy-manifest.ts +55 -0
- package/src/lazy-tool-transport.ts +15 -2
- package/src/metrics.ts +6 -0
- package/src/model-request-capture.ts +3 -0
- package/src/operational-instructions.ts +11 -2
- package/src/prepared-compaction-request.ts +51 -0
- package/src/provider-history-adapter.ts +12 -3
- package/src/runtime-skills.ts +3 -17
- package/src/sandbox/codemode-client.ts +204 -0
- package/src/sandbox/codemode-token.ts +7 -2
- package/src/sandbox/index.ts +7 -0
- package/src/sandbox/materialization-verification-error.ts +65 -0
- package/src/sandbox/provider-command-session.ts +70 -2
- package/src/sandbox/providers/modal-command-control.ts +386 -242
- package/src/sandbox/providers/modal-command-router-wire.ts +248 -0
- package/src/sandbox/providers/modal-command-session.ts +177 -4
- package/src/sandbox/providers/modal-legacy-command-control.ts +351 -0
- package/src/sandbox/providers/modal-materialization-verification.ts +122 -0
- package/src/sandbox/providers/modal.ts +1 -9
- package/src/sandbox/routing/routing-session.ts +417 -79
- package/src/sandbox/turn-tool-cancellation.ts +38 -2
- package/src/skill-catalog.ts +1 -0
- package/src/skill-library.ts +0 -6
- package/src/workspace-governance.ts +0 -1
- package/src/workspace-skills.ts +114 -74
- package/dist/chunk-46BFUGFL.js.map +0 -1
- package/dist/chunk-FHGLYLBL.js.map +0 -1
- package/dist/chunk-GB763TSV.js.map +0 -1
- package/dist/chunk-RIJ6DCYS.js.map +0 -1
- package/dist/chunk-UU6EJR52.js.map +0 -1
- /package/dist/{localSources-PVFCAPN2.js.map → chunk-4A4XTKKX.js.map} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Manifest, type SandboxSessionLike } from "@openai/agents/sandbox";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
|
|
4
|
+
/** Record already-materialized directory placeholders after lazy setup. This is
|
|
5
|
+
* not manifest application: never change environment, permissions, grants,
|
|
6
|
+
* mounts, files, or existing entries. Non-placeholder deltas remain visible to
|
|
7
|
+
* the SDK's normal validation/invalidation path. */
|
|
8
|
+
export async function recordLazyMaterializedDirectories(
|
|
9
|
+
session: SandboxSessionLike,
|
|
10
|
+
target: Manifest,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const current = session.state.manifest;
|
|
13
|
+
const { entries: currentEntries, ...currentMetadata } = current;
|
|
14
|
+
const { entries: targetEntries, ...targetMetadata } = target;
|
|
15
|
+
if (!isDeepStrictEqual(currentMetadata, targetMetadata) || !session.listDir) return;
|
|
16
|
+
const additions = Object.entries(targetEntries).filter(([path]) => !(path in currentEntries));
|
|
17
|
+
if (
|
|
18
|
+
Object.entries(currentEntries).some(
|
|
19
|
+
([path, entry]) => path in targetEntries && !isDeepStrictEqual(entry, targetEntries[path]),
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
return;
|
|
23
|
+
// A plain directory is the logical placeholder for a repository cloned by
|
|
24
|
+
// setup. Rich directories, sources and permissions require SDK materialization.
|
|
25
|
+
const emptyDirectory = new Manifest({ entries: { placeholder: { type: "dir" } } }).entries
|
|
26
|
+
.placeholder;
|
|
27
|
+
if (additions.some(([, entry]) => !isDeepStrictEqual(entry, emptyDirectory))) return;
|
|
28
|
+
// iterEntries validates every logical path before any live filesystem access.
|
|
29
|
+
const paths = new Map([...target.iterEntries()].map((entry) => [entry.logicalPath, entry]));
|
|
30
|
+
const directories = new Map<
|
|
31
|
+
string,
|
|
32
|
+
Awaited<ReturnType<NonNullable<SandboxSessionLike["listDir"]>>>
|
|
33
|
+
>();
|
|
34
|
+
for (const [path] of additions) {
|
|
35
|
+
let directory = target.root;
|
|
36
|
+
// Listing the target itself follows symlinks on some adapters. Walk every
|
|
37
|
+
// ancestor through its parent's typed listing instead, exactly like the
|
|
38
|
+
// repository reader. Cache only these invocation-local parent listings.
|
|
39
|
+
for (const name of paths.get(path)!.logicalPath.split("/")) {
|
|
40
|
+
let entries = directories.get(directory);
|
|
41
|
+
if (!entries) {
|
|
42
|
+
entries = await session.listDir({ path: directory });
|
|
43
|
+
directories.set(directory, entries);
|
|
44
|
+
}
|
|
45
|
+
if (!entries.some((entry) => entry.name === name && entry.type === "dir")) return;
|
|
46
|
+
directory = `${directory.replace(/\/$/, "")}/${name}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Do not overwrite a concurrent manifest update.
|
|
50
|
+
if (session.state.manifest !== current || additions.length === 0) return;
|
|
51
|
+
session.state.manifest = new Manifest({
|
|
52
|
+
...current,
|
|
53
|
+
entries: { ...currentEntries, ...targetEntries },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rememberPreparedModelRequest } from "./prepared-compaction-request";
|
|
1
2
|
import {
|
|
2
3
|
tool as agentTool,
|
|
3
4
|
toolSearchTool,
|
|
@@ -185,6 +186,7 @@ export class LazyToolRuntime {
|
|
|
185
186
|
private readonly toolPreparationReady?: Promise<void>,
|
|
186
187
|
private readonly deferredMcpServerIds: ReadonlySet<string> = mcpServerIds,
|
|
187
188
|
private readonly preparationIndependentToolNames: ReadonlySet<string> = new Set(),
|
|
189
|
+
private readonly modelMcpServerIds: () => ReadonlyMap<string, string> = () => new Map(),
|
|
188
190
|
) {
|
|
189
191
|
this.controlTools =
|
|
190
192
|
transport !== "generic_dispatch"
|
|
@@ -289,6 +291,7 @@ export class LazyToolRuntime {
|
|
|
289
291
|
this.currentTools = tools;
|
|
290
292
|
this.functionTools.clear();
|
|
291
293
|
this.searchableToolNames.clear();
|
|
294
|
+
const modelServerIds = this.modelMcpServerIds();
|
|
292
295
|
for (const tool of tools) {
|
|
293
296
|
if (!isFunctionTool(tool)) continue;
|
|
294
297
|
this.functionTools.set(tool.name, tool);
|
|
@@ -297,8 +300,8 @@ export class LazyToolRuntime {
|
|
|
297
300
|
// Origin, not transport: deferred MCP plus every non-MCP function tool
|
|
298
301
|
// outside the base set. ToolRef.eager still decides the MCP arm.
|
|
299
302
|
const lazy =
|
|
300
|
-
isSearchableMcpFunctionTool(tool, this.deferredMcpServerIds) ||
|
|
301
|
-
!isSearchableMcpFunctionTool(tool, this.mcpServerIds);
|
|
303
|
+
isSearchableMcpFunctionTool(tool, this.deferredMcpServerIds, modelServerIds) ||
|
|
304
|
+
!isSearchableMcpFunctionTool(tool, this.mcpServerIds, modelServerIds);
|
|
302
305
|
if (lazy) {
|
|
303
306
|
// After the preparation fence, deferred tools stay off the Agent
|
|
304
307
|
// list. Search teaches names; a remembered raw name binds later
|
|
@@ -489,6 +492,12 @@ export class LazyToolRuntime {
|
|
|
489
492
|
tools: descriptors,
|
|
490
493
|
total: tools.length,
|
|
491
494
|
nextCursor: index < tools.length ? descriptors.at(-1)!.name : null,
|
|
495
|
+
...(prefix && tools.length === 0
|
|
496
|
+
? {
|
|
497
|
+
message:
|
|
498
|
+
"No tool names match this literal prefix. Retry tool_list without namePrefix to browse the authorized catalog; tool names can include a server namespace.",
|
|
499
|
+
}
|
|
500
|
+
: {}),
|
|
492
501
|
});
|
|
493
502
|
},
|
|
494
503
|
}) as unknown as Tool;
|
|
@@ -581,6 +590,7 @@ export function installLazyToolRuntime(
|
|
|
581
590
|
toolPreparationReady?: Promise<void>,
|
|
582
591
|
deferredMcpServerIds: ReadonlySet<string> = mcpServerIds,
|
|
583
592
|
preparationIndependentToolNames: ReadonlySet<string> = new Set(),
|
|
593
|
+
modelMcpServerIds?: () => ReadonlyMap<string, string>,
|
|
584
594
|
): LazyToolRuntime {
|
|
585
595
|
const runtime = new LazyToolRuntime(
|
|
586
596
|
transport,
|
|
@@ -588,6 +598,7 @@ export function installLazyToolRuntime(
|
|
|
588
598
|
toolPreparationReady,
|
|
589
599
|
deferredMcpServerIds,
|
|
590
600
|
preparationIndependentToolNames,
|
|
601
|
+
modelMcpServerIds,
|
|
591
602
|
);
|
|
592
603
|
installLazyToolRuntimeOnAgent(agent, runtime);
|
|
593
604
|
return runtime;
|
|
@@ -815,6 +826,7 @@ class LazyToolModel implements Model {
|
|
|
815
826
|
|
|
816
827
|
async getResponse(request: ModelRequest): Promise<ModelResponse> {
|
|
817
828
|
const prepared = prepareLazyToolRequest(request, this.runtime);
|
|
829
|
+
rememberPreparedModelRequest(prepared);
|
|
818
830
|
void notifyModelRequestCapture(prepared);
|
|
819
831
|
const response = await this.inner.getResponse(prepared);
|
|
820
832
|
if (responseRequiresToolPreparation(response, this.runtime)) {
|
|
@@ -827,6 +839,7 @@ class LazyToolModel implements Model {
|
|
|
827
839
|
|
|
828
840
|
async *getStreamedResponse(request: ModelRequest): AsyncIterable<StreamEvent> {
|
|
829
841
|
const prepared = prepareLazyToolRequest(request, this.runtime);
|
|
842
|
+
rememberPreparedModelRequest(prepared);
|
|
830
843
|
void notifyModelRequestCapture(prepared);
|
|
831
844
|
for await (const event of this.inner.getStreamedResponse(prepared)) {
|
|
832
845
|
if (event.type === "response_done") {
|
package/src/metrics.ts
CHANGED
|
@@ -48,6 +48,12 @@ export type RuntimeMetricsHooks = {
|
|
|
48
48
|
outcome: "put" | "put_failed" | "deleted_unpublished";
|
|
49
49
|
backend: string;
|
|
50
50
|
}) => void;
|
|
51
|
+
/** Physical capture plus publication; emitted at settlement, not caller timeout. */
|
|
52
|
+
onWorkspaceCapture?: (input: {
|
|
53
|
+
backend: string;
|
|
54
|
+
outcome: "completed" | "failed";
|
|
55
|
+
durationSeconds: number;
|
|
56
|
+
}) => void;
|
|
51
57
|
/**
|
|
52
58
|
* One physical MCP tools/call invocation. The closed outcome enum deliberately
|
|
53
59
|
* excludes server, tool, tenant, request, and error-content labels.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rememberPreparedModelRequest } from "./prepared-compaction-request";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import type { Model, ModelProvider, ModelRequest, StreamEvent } from "@openai/agents";
|
|
3
4
|
|
|
@@ -132,11 +133,13 @@ export class ModelRequestCaptureModel implements Model {
|
|
|
132
133
|
constructor(private readonly inner: Model) {}
|
|
133
134
|
|
|
134
135
|
async getResponse(request: ModelRequest) {
|
|
136
|
+
rememberPreparedModelRequest(request);
|
|
135
137
|
void notifyModelRequestCapture(request);
|
|
136
138
|
return this.inner.getResponse(request);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
async *getStreamedResponse(request: ModelRequest): AsyncIterable<StreamEvent> {
|
|
142
|
+
rememberPreparedModelRequest(request);
|
|
140
143
|
void notifyModelRequestCapture(request);
|
|
141
144
|
yield* this.inner.getStreamedResponse(request);
|
|
142
145
|
}
|
|
@@ -60,9 +60,12 @@ In your final answer back to the user, focus on the most important information.
|
|
|
60
60
|
Your answer is being rendered by an application for the user. Follow these guidelines to make sure your answer is rendered correctly:
|
|
61
61
|
|
|
62
62
|
- You may format with GitHub-flavored Markdown.
|
|
63
|
-
-
|
|
63
|
+
- User-facing reports are durable document Artifacts by default, including audit or summary reports produced while doing another task. Read the opengeni-documents Skill and create the native document artifact before authoring the report. Do not write a sandbox Markdown/DOCX report first or treat publishing a file as native document creation.
|
|
64
|
+
- Declare report deliverables through the available goal tools before authoring, including reports discovered after a goal was created. Inspect the relevant final artifact head after the last edit, supply its verified delivery evidence at goal completion, and give the user the artifact reference returned by the tools. A sandbox path, a raw file ID, or an assertion that a report exists is not completed report delivery.
|
|
65
|
+
- If artifact creation, inspection, access, or delivery tooling is unavailable or fails, report the concrete blocker and leave report delivery incomplete. Do not silently fall back to a sandbox link, invent an artifact reference, or claim success. Ordinary in-chat answers, brief progress updates, internal worker findings, source-code navigation, and explicitly requested local-file workflows do not become report deliverables merely because they contain Markdown or a file link.
|
|
66
|
+
- When referencing a real local source file or an explicitly requested local file, prefer a clickable markdown link.
|
|
64
67
|
* Clickable file links should look like [app.py](sandbox:/workspace/app.py:12): plain label, sandbox:/workspace/... target, with optional line number after the path.
|
|
65
|
-
* If a file path has spaces, wrap the target in angle brackets: [My
|
|
68
|
+
* If a file path has spaces, wrap the target in angle brackets: [My Component.ts](<sandbox:/workspace/My Project/My Component.ts:3>).
|
|
66
69
|
* Use the active workspace path exactly as exposed to you. Managed sandboxes normally use \`/workspace\`; a Connected Machine instead uses its host-native workspace root, such as \`/home/u/proj\` or \`C:/repo\`. Both are valid inside a \`sandbox:\` link when they are the active workspace.
|
|
67
70
|
* Connected Machine examples are [app.py](sandbox:/home/u/proj/app.py:12) on POSIX and [app.ts](<sandbox:C:/repo/app.ts:12>) on Windows.
|
|
68
71
|
* On a Connected Machine, absolute file links may point outside the working directory (including sibling worktrees and temporary files); use the real path on the selected machine.
|
|
@@ -147,12 +150,18 @@ Skills are reusable instructions supplied dynamically for the current session. W
|
|
|
147
150
|
- The user's instructions take precedence over skill guidance.
|
|
148
151
|
- If a named skill is unavailable or cannot be read, say so briefly and continue with the best fallback.
|
|
149
152
|
|
|
153
|
+
# Integration setup
|
|
154
|
+
|
|
155
|
+
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. 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.
|
|
156
|
+
|
|
150
157
|
# Session coordination
|
|
151
158
|
|
|
152
159
|
Use \`session_events\` for conversation history: its default returns user and completed assistant messages, not execution noise. Cursors only paginate. Request \`results\` for final outcomes, \`tools\` for tool receipts, or \`debug\` for explicit diagnostics; request large tool bodies only when needed. Use the returned continuation cursor rather than rereading whole pages. Audit reads do not acknowledge command completion.
|
|
153
160
|
|
|
154
161
|
For a yielded command, use \`command_read\` to read available output and status, or \`command_wait\` to wait briefly using the same command interface. Keep the command ID and output cursor. A terminal read suppresses any still-pending completion notification; a running read does not. Earlier tool results and delivered messages never change. Use \`command_input\` only to send input where supported, not to poll output. An unsupported input capability does not imply output is unavailable. Give foreground commands a realistic requested wait; default to 10 seconds (yield_time_ms: 10000). An internal polling slice is not a reason to return a background handle.
|
|
155
162
|
|
|
163
|
+
Command completion alone resumes you only while you have an explicit \`wait_for_input\` registered. If remaining work depends on background commands, register that session-level wait before ending your turn. If you finish normally, command results remain retained and can accompany later input, but do not start another turn by themselves. No per-command dismissal is required.
|
|
164
|
+
|
|
156
165
|
If the user asks to create, inspect, continue, pause, resume, steer, rename, or otherwise manage a session, use the corresponding session tool. Pause affects the selected workstream and its descendants: pausing an ancestor also stops you, so you cannot then Resume yourself. Coordinate disjoint edits through messages instead of ancestor Pause.
|
|
157
166
|
|
|
158
167
|
Create a child worker only for a concrete, bounded subtask that can run independently and whose result has a clear integration point in the current request. Before spawning, decide what output you need and keep the parent's concurrent work disjoint. Do not delegate a scope that you will also perform yourself. If no useful independent work remains, continue in this session. Do not repurpose or direct an unrelated existing session unless the user explicitly asks. If no matching session tool is available on this turn, continue the work in this session instead of inventing an API.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { CallModelInputFilter, ModelRequest } from "@openai/agents";
|
|
3
|
+
import { findCompactionNeededError } from "./context-compaction";
|
|
4
|
+
|
|
5
|
+
/** One owner per run; never share a prefix between sessions or rebuilt agents. */
|
|
6
|
+
const requests = new WeakMap<object, Omit<ModelRequest, "input">>();
|
|
7
|
+
const dispatch = new AsyncLocalStorage<{ agent: object; pending?: unknown }>();
|
|
8
|
+
const queued = new WeakMap<object, unknown>();
|
|
9
|
+
|
|
10
|
+
export function queuePreparedCompaction(agent: object, error: unknown): void {
|
|
11
|
+
queued.set(agent, error);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function withPreparedCompactionRequest<T>(agent: object, run: () => T): T {
|
|
15
|
+
const pending = queued.get(agent);
|
|
16
|
+
queued.delete(agent);
|
|
17
|
+
return dispatch.run({ agent, pending }, run);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Called after SDK sandbox preparation AND lazy-tool filtering, before transport. */
|
|
21
|
+
export function rememberPreparedModelRequest(request: ModelRequest): void {
|
|
22
|
+
const scope = dispatch.getStore();
|
|
23
|
+
if (!scope) return;
|
|
24
|
+
const { input: _input, ...prefix } = request;
|
|
25
|
+
requests.set(scope.agent, {
|
|
26
|
+
...prefix,
|
|
27
|
+
tools: structuredClone(request.tools),
|
|
28
|
+
modelSettings: structuredClone(request.modelSettings),
|
|
29
|
+
});
|
|
30
|
+
if (scope.pending) throw scope.pending;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function preparedCompactionRequest(agent: object): Omit<ModelRequest, "input"> {
|
|
34
|
+
const request = requests.get(agent);
|
|
35
|
+
if (!request) throw new Error("Compaction requires a prepared model request");
|
|
36
|
+
return request;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Finish normal preparation, but never send ordinary inference once compaction is due. */
|
|
40
|
+
export function deferCompactionToModelBoundary(filter: CallModelInputFilter): CallModelInputFilter {
|
|
41
|
+
return async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
return await filter(args);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const scope = dispatch.getStore();
|
|
46
|
+
if (!scope || !findCompactionNeededError(error)) throw error;
|
|
47
|
+
scope.pending = error;
|
|
48
|
+
return args.modelData;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -73,15 +73,24 @@ function isChatIncompatibleHostedToolCall(item: Record<string, unknown>): boolea
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Build the one attempt-local history view required by the target wire API.
|
|
76
|
-
* Canonical history remains untouched.
|
|
77
|
-
*
|
|
76
|
+
* Canonical history remains untouched. SDK-unsupported developer messages use
|
|
77
|
+
* the Responses pass-through item. Other history is returned by reference when every
|
|
78
78
|
* item is already representable by the SDK's Chat Completions converter.
|
|
79
79
|
*/
|
|
80
80
|
export function projectHistoryForProvider(
|
|
81
81
|
items: Array<Record<string, unknown>>,
|
|
82
82
|
providerApi: HistoryProviderApi,
|
|
83
83
|
): Array<Record<string, unknown>> {
|
|
84
|
-
if (providerApi === "responses")
|
|
84
|
+
if (providerApi === "responses") {
|
|
85
|
+
if (!items.some((item) => item.type === "message" && item.role === "developer")) return items;
|
|
86
|
+
// agents-js 0.14's message converter supports system/user/assistant only.
|
|
87
|
+
// The Responses API itself supports developer; use the SDK's raw-item adapter.
|
|
88
|
+
return items.map((item) =>
|
|
89
|
+
item.type === "message" && item.role === "developer"
|
|
90
|
+
? { type: "unknown", providerData: item }
|
|
91
|
+
: item,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
85
94
|
|
|
86
95
|
const incompatibleCallIds = new Set<string>();
|
|
87
96
|
for (const item of items) {
|
package/src/runtime-skills.ts
CHANGED
|
@@ -19,8 +19,7 @@ export type RuntimeSkillArtifactFile = Readonly<{
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* One immutable Skill artifact that may be activated by an installation, a
|
|
22
|
-
*
|
|
23
|
-
* acquisition-neutral: Packs do not own a second Skill representation.
|
|
22
|
+
* session selection. The artifact itself is acquisition-neutral.
|
|
24
23
|
*/
|
|
25
24
|
export type RuntimeSkillArtifact = Readonly<{
|
|
26
25
|
name: string;
|
|
@@ -37,13 +36,6 @@ export type InstalledSkillActivation = Readonly<{
|
|
|
37
36
|
reason: string;
|
|
38
37
|
}>;
|
|
39
38
|
|
|
40
|
-
export type PackSkillActivation = Readonly<{
|
|
41
|
-
source: "pack";
|
|
42
|
-
id: string;
|
|
43
|
-
artifact: RuntimeSkillArtifact;
|
|
44
|
-
reason: string;
|
|
45
|
-
}>;
|
|
46
|
-
|
|
47
39
|
export type SessionSkillActivation = Readonly<{
|
|
48
40
|
source: "session";
|
|
49
41
|
id: string;
|
|
@@ -51,10 +43,7 @@ export type SessionSkillActivation = Readonly<{
|
|
|
51
43
|
reason: string;
|
|
52
44
|
}>;
|
|
53
45
|
|
|
54
|
-
export type RuntimeSkillActivation =
|
|
55
|
-
| InstalledSkillActivation
|
|
56
|
-
| PackSkillActivation
|
|
57
|
-
| SessionSkillActivation;
|
|
46
|
+
export type RuntimeSkillActivation = InstalledSkillActivation | SessionSkillActivation;
|
|
58
47
|
|
|
59
48
|
export type NativeToolSkillSet = Readonly<{
|
|
60
49
|
/** False when an embedding host owns the complete Skill catalog and reader. */
|
|
@@ -67,7 +56,7 @@ export type NativeToolSkillSet = Readonly<{
|
|
|
67
56
|
export type EffectiveSkillSelection = Readonly<{
|
|
68
57
|
id: string;
|
|
69
58
|
name: string;
|
|
70
|
-
source: "installation" | "
|
|
59
|
+
source: "installation" | "session" | "native_tool";
|
|
71
60
|
version: string | null;
|
|
72
61
|
contentSha256: string | null;
|
|
73
62
|
reason: string;
|
|
@@ -321,8 +310,6 @@ function activationPrecedence(source: RuntimeSkillActivation["source"]): number
|
|
|
321
310
|
switch (source) {
|
|
322
311
|
case "installation":
|
|
323
312
|
return 1;
|
|
324
|
-
case "pack":
|
|
325
|
-
return 2;
|
|
326
313
|
case "session":
|
|
327
314
|
return 3;
|
|
328
315
|
}
|
|
@@ -369,7 +356,6 @@ function selectionForActivation({
|
|
|
369
356
|
contentSha256,
|
|
370
357
|
reason: activation.reason,
|
|
371
358
|
});
|
|
372
|
-
case "pack":
|
|
373
359
|
case "session":
|
|
374
360
|
return Object.freeze({
|
|
375
361
|
id: activation.id,
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, posix, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type { SandboxSessionLike } from "@openai/agents/sandbox";
|
|
6
|
+
import { sandboxCommandExitCode } from "./command-result";
|
|
7
|
+
|
|
8
|
+
/** Data shipped with the worker, never fetched from a package registry at turn time. */
|
|
9
|
+
export type ManagedCodemodeClient = {
|
|
10
|
+
version: 1;
|
|
11
|
+
files: { ogtool: string; "client.mjs": string; "package.json": string };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function managedCodemodeClientDigest(client: ManagedCodemodeClient): string {
|
|
15
|
+
return createHash("sha256").update(JSON.stringify(client)).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function managedCodemodeClientDirectory(
|
|
19
|
+
client: ManagedCodemodeClient,
|
|
20
|
+
workspaceRoot = "/workspace",
|
|
21
|
+
): string {
|
|
22
|
+
if (!posix.isAbsolute(workspaceRoot) || workspaceRoot.includes("\0")) {
|
|
23
|
+
throw new Error("Managed Codemode client workspace root must be absolute");
|
|
24
|
+
}
|
|
25
|
+
return posix.join(
|
|
26
|
+
workspaceRoot,
|
|
27
|
+
".opengeni/codemode-clients",
|
|
28
|
+
managedCodemodeClientDigest(client),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function buildManagedCodemodeClient(root: string): Promise<ManagedCodemodeClient> {
|
|
33
|
+
const bundle = async (entry: string, format: "esm" | "cjs", banner?: string) => {
|
|
34
|
+
const result = await Bun.build({
|
|
35
|
+
entrypoints: [join(root, entry)],
|
|
36
|
+
target: "node",
|
|
37
|
+
format,
|
|
38
|
+
minify: true,
|
|
39
|
+
sourcemap: "none",
|
|
40
|
+
...(banner ? { banner } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (!result.success || result.outputs.length !== 1) {
|
|
43
|
+
throw new Error(`Managed Codemode client build failed: ${entry}`);
|
|
44
|
+
}
|
|
45
|
+
return await result.outputs[0]!.text();
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
version: 1,
|
|
49
|
+
files: {
|
|
50
|
+
ogtool: await bundle("packages/ogtool/src/cli.ts", "cjs", "#!/usr/bin/env node"),
|
|
51
|
+
"client.mjs": await bundle("packages/codemode/src/index.ts", "esm"),
|
|
52
|
+
// The extensionless executable must not inherit a user's ESM workspace.
|
|
53
|
+
"package.json": '{"type":"commonjs"}\n',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let releaseClient: Promise<ManagedCodemodeClient> | undefined;
|
|
59
|
+
export function loadManagedCodemodeClient(): Promise<ManagedCodemodeClient> {
|
|
60
|
+
return (releaseClient ??= (async () => {
|
|
61
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
// A checkout must not reuse stale dist assets after source changes.
|
|
63
|
+
const root = resolve(moduleDir, "../../../..");
|
|
64
|
+
if (
|
|
65
|
+
import.meta.url.endsWith("/src/sandbox/codemode-client.ts") &&
|
|
66
|
+
existsSync(join(root, "packages/ogtool/src/cli.ts"))
|
|
67
|
+
) {
|
|
68
|
+
return await buildManagedCodemodeClient(root);
|
|
69
|
+
}
|
|
70
|
+
const candidates = [
|
|
71
|
+
join(moduleDir, "assets/codemode-client.json"),
|
|
72
|
+
join(moduleDir, "../assets/codemode-client.json"),
|
|
73
|
+
join(moduleDir, "../../dist/assets/codemode-client.json"),
|
|
74
|
+
];
|
|
75
|
+
for (const path of candidates) {
|
|
76
|
+
if (!existsSync(path)) continue;
|
|
77
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as ManagedCodemodeClient;
|
|
78
|
+
if (
|
|
79
|
+
!value ||
|
|
80
|
+
typeof value !== "object" ||
|
|
81
|
+
value.version !== 1 ||
|
|
82
|
+
Object.keys(value).sort().join(",") !== "files,version" ||
|
|
83
|
+
typeof value.files?.ogtool !== "string" ||
|
|
84
|
+
typeof value.files?.["client.mjs"] !== "string" ||
|
|
85
|
+
value.files?.["package.json"] !== '{"type":"commonjs"}\n' ||
|
|
86
|
+
Object.keys(value.files).sort().join(",") !== "client.mjs,ogtool,package.json"
|
|
87
|
+
) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Invalid deployment-owned managed Codemode client asset; rebuild the worker release",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
// Published runtimes must carry their own exact bytes.
|
|
95
|
+
throw new Error(
|
|
96
|
+
"Managed Codemode client asset missing; rebuild and deploy the matching worker/runtime package (do not install latest or replace the sandbox)",
|
|
97
|
+
);
|
|
98
|
+
})());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function quote(value: string): string {
|
|
102
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Immutable per-release paths permit old/new workers to share a warm box. */
|
|
106
|
+
export async function installManagedCodemodeClient(
|
|
107
|
+
session: SandboxSessionLike,
|
|
108
|
+
client: ManagedCodemodeClient,
|
|
109
|
+
run: (cmd: string) => Promise<unknown>,
|
|
110
|
+
runAs?: string,
|
|
111
|
+
workspaceRoot = "/workspace",
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const directory = managedCodemodeClientDirectory(client, workspaceRoot);
|
|
114
|
+
const digest = managedCodemodeClientDigest(client);
|
|
115
|
+
const verify = `
|
|
116
|
+
const fs = require('node:fs'), crypto = require('node:crypto');
|
|
117
|
+
const root = ${JSON.stringify(directory)};
|
|
118
|
+
if (!fs.lstatSync(root).isDirectory()) process.exit(1);
|
|
119
|
+
for (const name of ['ogtool', 'client.mjs', 'package.json']) {
|
|
120
|
+
if (!fs.lstatSync(root + '/' + name).isFile()) process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
fs.accessSync(root + '/ogtool', fs.constants.X_OK);
|
|
123
|
+
const files = {ogtool: fs.readFileSync(root + '/ogtool', 'utf8'), 'client.mjs': fs.readFileSync(root + '/client.mjs', 'utf8'), 'package.json': fs.readFileSync(root + '/package.json', 'utf8')};
|
|
124
|
+
if (crypto.createHash('sha256').update(JSON.stringify({version: 1, files})).digest('hex') !== ${JSON.stringify(digest)}) process.exit(1);
|
|
125
|
+
`;
|
|
126
|
+
if (sandboxCommandExitCode(await run(`node -e ${quote(verify)} 2>/dev/null`)) === 0) return;
|
|
127
|
+
const editor = session.createEditor?.(runAs);
|
|
128
|
+
if (!editor)
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Managed Codemode client delivery requires sandbox file ingress; update the sandbox provider adapter",
|
|
131
|
+
);
|
|
132
|
+
const stage = `${posix.dirname(directory)}/stage-${randomUUID()}.json`;
|
|
133
|
+
let installationFailure: { error: unknown } | undefined;
|
|
134
|
+
try {
|
|
135
|
+
await editor.createFile({
|
|
136
|
+
type: "create_file",
|
|
137
|
+
path: stage,
|
|
138
|
+
diff: `+${JSON.stringify(client)}`,
|
|
139
|
+
});
|
|
140
|
+
const install = `
|
|
141
|
+
const fs = require('node:fs'), crypto = require('node:crypto');
|
|
142
|
+
const raw = fs.readFileSync(${JSON.stringify(stage)}, 'utf8').trim();
|
|
143
|
+
if (crypto.createHash('sha256').update(raw).digest('hex') !== ${JSON.stringify(digest)}) throw Error('Codemode client transfer integrity failure');
|
|
144
|
+
const data = JSON.parse(raw), root = ${JSON.stringify(directory)};
|
|
145
|
+
fs.mkdirSync(root, {recursive: true});
|
|
146
|
+
if (!fs.lstatSync(root).isDirectory()) throw Error('Codemode client directory must not be a symlink');
|
|
147
|
+
for (const name of ['package.json', 'ogtool', 'client.mjs']) {
|
|
148
|
+
const tmp = root + '/' + name + '.' + crypto.randomUUID();
|
|
149
|
+
fs.writeFileSync(tmp, data.files[name], {mode: 0o755, flag: 'wx'});
|
|
150
|
+
fs.renameSync(tmp, root + '/' + name);
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
const result = await run(`node -e ${quote(install)} && node -e ${quote(verify)}`);
|
|
154
|
+
if (sandboxCommandExitCode(result) !== 0) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"Managed Codemode client delivery failed; verify Node and sandbox file ingress, then retry with the same worker release",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
installationFailure = { error };
|
|
161
|
+
}
|
|
162
|
+
let cleanupFailure: Error | undefined;
|
|
163
|
+
try {
|
|
164
|
+
await editor.deleteFile({ type: "delete_file", path: stage });
|
|
165
|
+
} catch (editorFailure) {
|
|
166
|
+
// A provider can support ingress but lose its delete reply. Remove only
|
|
167
|
+
// this exact attempt's staging file, through the same command fence.
|
|
168
|
+
try {
|
|
169
|
+
const cleanup = await run(
|
|
170
|
+
`node -e ${quote(`require('node:fs').rmSync(${JSON.stringify(stage)}, {force: true})`)}`,
|
|
171
|
+
);
|
|
172
|
+
if (sandboxCommandExitCode(cleanup) !== 0) {
|
|
173
|
+
throw new Error("Staging file removal failed", { cause: editorFailure });
|
|
174
|
+
}
|
|
175
|
+
} catch (commandFailure) {
|
|
176
|
+
cleanupFailure = new AggregateError(
|
|
177
|
+
[editorFailure, commandFailure],
|
|
178
|
+
"Both cleanup paths failed",
|
|
179
|
+
{ cause: commandFailure },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (cleanupFailure) {
|
|
184
|
+
throw new AggregateError(
|
|
185
|
+
installationFailure ? [installationFailure.error, cleanupFailure] : [cleanupFailure],
|
|
186
|
+
"Managed Codemode client staging cleanup failed; restore sandbox file ingress/command execution before retrying",
|
|
187
|
+
{ cause: installationFailure?.error ?? cleanupFailure },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (installationFailure) throw installationFailure.error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function managedCodemodeClientEnvironment(directory: string): string[] {
|
|
194
|
+
return [
|
|
195
|
+
// A routing proxy can switch to a Connected Machine during the attempt.
|
|
196
|
+
// Its native client is projected by the connection-bound exec transport.
|
|
197
|
+
'if [ -z "${OPENGENI_CODEMODE_NATIVE_CLIENT:-}" ]; then',
|
|
198
|
+
` export PATH=${quote(directory)}:"$PATH"`,
|
|
199
|
+
` export OPENGENI_CODEMODE_CLIENT_MODULE=${quote(`${directory}/client.mjs`)}`,
|
|
200
|
+
"else",
|
|
201
|
+
" unset OPENGENI_CODEMODE_CLIENT_MODULE",
|
|
202
|
+
"fi",
|
|
203
|
+
];
|
|
204
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { posix as posixPath } from "node:path";
|
|
3
|
+
import { managedCodemodeClientEnvironment } from "./codemode-client";
|
|
3
4
|
|
|
4
5
|
import type {
|
|
5
6
|
ExecCommandArgs,
|
|
@@ -86,10 +87,12 @@ export function withCodemodeTokenEnvironment(
|
|
|
86
87
|
cmd: string,
|
|
87
88
|
tokenFile: string,
|
|
88
89
|
codemodeUrl?: string,
|
|
90
|
+
clientDirectory?: string,
|
|
89
91
|
): string {
|
|
90
92
|
return [
|
|
91
93
|
`export OPENGENI_CODEMODE_TOKEN_FILE=${shellCodemodePath(tokenFile)}`,
|
|
92
94
|
...(codemodeUrl ? [`export OPENGENI_CODEMODE_URL=${shellQuote(codemodeUrl)}`] : []),
|
|
95
|
+
...(clientDirectory ? managedCodemodeClientEnvironment(clientDirectory) : []),
|
|
93
96
|
cmd,
|
|
94
97
|
].join("\n");
|
|
95
98
|
}
|
|
@@ -99,6 +102,7 @@ export function withCodemodeTokenSession<T extends object>(
|
|
|
99
102
|
session: T,
|
|
100
103
|
tokenFile: string,
|
|
101
104
|
codemodeUrl?: string,
|
|
105
|
+
clientDirectory?: string,
|
|
102
106
|
): T {
|
|
103
107
|
return new Proxy(session, {
|
|
104
108
|
get(target, property, receiver) {
|
|
@@ -110,7 +114,7 @@ export function withCodemodeTokenSession<T extends object>(
|
|
|
110
114
|
return async (args: ExecCommandArgs) =>
|
|
111
115
|
await command.call(target, {
|
|
112
116
|
...args,
|
|
113
|
-
cmd: withCodemodeTokenEnvironment(args.cmd, tokenFile, codemodeUrl),
|
|
117
|
+
cmd: withCodemodeTokenEnvironment(args.cmd, tokenFile, codemodeUrl, clientDirectory),
|
|
114
118
|
});
|
|
115
119
|
}
|
|
116
120
|
const value = Reflect.get(target, property, receiver) as unknown;
|
|
@@ -124,12 +128,13 @@ export function withCodemodeTokenClient(
|
|
|
124
128
|
client: SandboxClient,
|
|
125
129
|
tokenFile: string,
|
|
126
130
|
codemodeUrl?: string,
|
|
131
|
+
clientDirectory?: string,
|
|
127
132
|
): SandboxClient {
|
|
128
133
|
const decorated = new WeakMap<object, SandboxSessionLike>();
|
|
129
134
|
const wrap = <T extends SandboxSessionLike>(session: T): T => {
|
|
130
135
|
const existing = decorated.get(session);
|
|
131
136
|
if (existing) return existing as T;
|
|
132
|
-
const wrapped = withCodemodeTokenSession(session, tokenFile, codemodeUrl);
|
|
137
|
+
const wrapped = withCodemodeTokenSession(session, tokenFile, codemodeUrl, clientDirectory);
|
|
133
138
|
decorated.set(session, wrapped);
|
|
134
139
|
return wrapped;
|
|
135
140
|
};
|
package/src/sandbox/index.ts
CHANGED
|
@@ -86,6 +86,12 @@ import type { ExposedPortEndpoint } from "./stream-port";
|
|
|
86
86
|
// a single agent-loop-free entrypoint. They physically live in @opengeni/config
|
|
87
87
|
// (moving them into runtime would create a config→runtime cycle — ledger CR8).
|
|
88
88
|
export { collectSandboxEnvironment, parseExposedPorts } from "@opengeni/config";
|
|
89
|
+
export {
|
|
90
|
+
SandboxMaterializationVerificationError,
|
|
91
|
+
materializationVerificationDiagnostic,
|
|
92
|
+
type MaterializationFailureReason,
|
|
93
|
+
type MaterializationVerificationDiagnostic,
|
|
94
|
+
} from "./materialization-verification-error";
|
|
89
95
|
export { sendCommandInput, type CommandInputSession } from "./command-input";
|
|
90
96
|
export { OpStreamExecClient, type OpStreamOutputFrame } from "./selfhosted/op-stream";
|
|
91
97
|
|
|
@@ -593,6 +599,7 @@ export {
|
|
|
593
599
|
type RoutingSandboxFirstOperationPhaseObservation,
|
|
594
600
|
type RoutingSandboxOperationObservation,
|
|
595
601
|
type RoutingSandboxOperationObserver,
|
|
602
|
+
type RoutingSandboxCaptureWaitObserver,
|
|
596
603
|
type RoutingSandboxPhaseOutcome,
|
|
597
604
|
type RoutingSandboxWaitObservation,
|
|
598
605
|
type RoutingRetainedProcess,
|