open-managed-agents 0.1.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.
- package/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
import { AuthStorage, createAgentSession, createSyntheticSourceInfo, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { RuntimeUnsupportedSessionFileResourcesError } from "../../events/types.js";
|
|
3
|
+
import { PiCustomToolBridge, } from "./custom-tools.js";
|
|
4
|
+
import { PiToolPermissionBridge, } from "./tool-permissions.js";
|
|
5
|
+
import { McpConnection } from "./mcp/client.js";
|
|
6
|
+
import { createGuardedMcpFetch } from "./mcp/fetch.js";
|
|
7
|
+
import { createMcpToolDefinitions, } from "./mcp/bridge.js";
|
|
8
|
+
import { resolveSandboxProviderFactory, } from "./sandbox/selection.js";
|
|
9
|
+
const DEFAULT_IDLE_TTL_MS = 15 * 60 * 1000;
|
|
10
|
+
const ALREADY_PROCESSING_MESSAGE = "Agent is already processing";
|
|
11
|
+
// Builds the resource loader that advertises snapshotted skills to the model at
|
|
12
|
+
// their in-container mount paths. Exported so the real-SDK contract test can
|
|
13
|
+
// exercise the exact same construction the runner uses. The loader must be
|
|
14
|
+
// reload()ed before use (see createResourceLoader): a caller-provided loader is
|
|
15
|
+
// used as-is by createAgentSession, and skillsOverride only runs inside reload().
|
|
16
|
+
export function buildSessionSkillsResourceLoader(skills, systemPrompt) {
|
|
17
|
+
return new DefaultResourceLoader({
|
|
18
|
+
cwd: "/",
|
|
19
|
+
agentDir: "/nonexistent-oma-pi-agent",
|
|
20
|
+
noExtensions: true,
|
|
21
|
+
noSkills: true,
|
|
22
|
+
noPromptTemplates: true,
|
|
23
|
+
noThemes: true,
|
|
24
|
+
noContextFiles: true,
|
|
25
|
+
...(systemPrompt === undefined ? {} : { systemPrompt }),
|
|
26
|
+
skillsOverride: () => ({
|
|
27
|
+
diagnostics: [],
|
|
28
|
+
skills: skills.map((skill) => {
|
|
29
|
+
const baseDir = `/workspace/skills/${skill.name}`;
|
|
30
|
+
const filePath = `${baseDir}/SKILL.md`;
|
|
31
|
+
return { name: skill.name, description: skill.description, filePath, baseDir, disableModelInvocation: false, sourceInfo: createSyntheticSourceInfo(filePath, { source: "oma-session-snapshot", scope: "temporary", origin: "top-level", baseDir }) };
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const DEFAULT_MCP_MAX_CONSECUTIVE_FAILURES = 5;
|
|
37
|
+
const MCP_UNAUTHENTICATED_FINGERPRINT = "none";
|
|
38
|
+
function createPrivatePiModelCatalog(provider = "anthropic", modelId = "claude-haiku-4-5") {
|
|
39
|
+
const authStorage = AuthStorage.create();
|
|
40
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
41
|
+
const allowedProviders = new Set([provider]);
|
|
42
|
+
return {
|
|
43
|
+
defaultModel: { provider, id: modelId },
|
|
44
|
+
allowedProviders,
|
|
45
|
+
authStorage,
|
|
46
|
+
modelRegistry,
|
|
47
|
+
securityReport: { warnings: [] },
|
|
48
|
+
// Standalone runners are an internal/test seam without deployment
|
|
49
|
+
// admission policy. Production always receives the shared allowlisted
|
|
50
|
+
// catalog from the composition root.
|
|
51
|
+
resolve: (ref) => modelRegistry.find(ref.provider, ref.id),
|
|
52
|
+
list: (options = {}) => (options.availableOnly ? modelRegistry.getAvailable() : modelRegistry.getAll())
|
|
53
|
+
.filter((model) => allowedProviders.has(model.provider) &&
|
|
54
|
+
(options.provider === undefined || model.provider === options.provider)),
|
|
55
|
+
// Standalone runners do not participate in production admission. Tests
|
|
56
|
+
// and internal callers may supply their own session factory without
|
|
57
|
+
// configuring operator credentials.
|
|
58
|
+
hasConfiguredAuth: () => true,
|
|
59
|
+
providerAuthMetadata: (providerName) => {
|
|
60
|
+
const status = modelRegistry.getProviderAuthStatus(providerName);
|
|
61
|
+
return {
|
|
62
|
+
...(status.source === undefined ? {} : { source: status.source }),
|
|
63
|
+
...(status.label === undefined ? {} : { label: status.label }),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export class PiSessionRunner {
|
|
69
|
+
opts;
|
|
70
|
+
authStorage;
|
|
71
|
+
modelRegistry;
|
|
72
|
+
modelCatalog;
|
|
73
|
+
sessions = new Map();
|
|
74
|
+
pendingSessions = new Map();
|
|
75
|
+
pendingInterrupts = new Map();
|
|
76
|
+
closedSessionIds = new Set();
|
|
77
|
+
customToolBridge;
|
|
78
|
+
toolPermissionBridge;
|
|
79
|
+
preparingSessionAgents = new Map();
|
|
80
|
+
preparingSessionSkills = new Map();
|
|
81
|
+
idleTtlMs;
|
|
82
|
+
now;
|
|
83
|
+
sessionFactory;
|
|
84
|
+
mcpFetch;
|
|
85
|
+
/**
|
|
86
|
+
* Consecutive connect failures per (session, server). Runner-level so the
|
|
87
|
+
* count SURVIVES handle recreation — fresh-handle retry is the mechanism
|
|
88
|
+
* (plan 0122 §4.6). Cleared on closeSession.
|
|
89
|
+
*/
|
|
90
|
+
mcpFailureCounts = new Map();
|
|
91
|
+
closed = false;
|
|
92
|
+
constructor(opts = {}) {
|
|
93
|
+
this.opts = opts;
|
|
94
|
+
const catalog = opts.modelCatalog ?? createPrivatePiModelCatalog(opts.provider, opts.model);
|
|
95
|
+
this.modelCatalog = catalog;
|
|
96
|
+
this.authStorage = catalog.authStorage;
|
|
97
|
+
this.modelRegistry = catalog.modelRegistry;
|
|
98
|
+
this.resolveSandboxProviderFactory();
|
|
99
|
+
this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
100
|
+
this.now = opts.now ?? Date.now;
|
|
101
|
+
this.sessionFactory = opts.sessionFactory;
|
|
102
|
+
this.mcpFetch = opts.mcp?.fetch ?? createGuardedMcpFetch();
|
|
103
|
+
this.customToolBridge = new PiCustomToolBridge({
|
|
104
|
+
customTools: opts.customTools,
|
|
105
|
+
timeoutMs: opts.customToolTimeoutMs,
|
|
106
|
+
});
|
|
107
|
+
this.toolPermissionBridge = new PiToolPermissionBridge({
|
|
108
|
+
access: (workspaceId, sessionId, toolName) => opts.builtinToolAccess?.(workspaceId, sessionId, toolName, {
|
|
109
|
+
agentId: this.preparingSessionAgents.get(sessionId)?.workspaceId === workspaceId
|
|
110
|
+
? this.preparingSessionAgents.get(sessionId)?.agentId
|
|
111
|
+
: undefined,
|
|
112
|
+
agentVersion: this.preparingSessionAgents.get(sessionId)?.workspaceId === workspaceId
|
|
113
|
+
? this.preparingSessionAgents.get(sessionId)?.agentVersion
|
|
114
|
+
: undefined,
|
|
115
|
+
}) ?? { enabled: true, permission: "allow" },
|
|
116
|
+
timeoutMs: opts.toolConfirmationTimeoutMs,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
runUserMessage(workspaceId, sessionId, text, opts = {}) {
|
|
120
|
+
return this.runOnSession(workspaceId, sessionId, text, opts.signal);
|
|
121
|
+
}
|
|
122
|
+
async prepareSession(workspaceId, sessionId, opts = {}) {
|
|
123
|
+
if (opts.agent) {
|
|
124
|
+
this.preparingSessionAgents.set(sessionId, {
|
|
125
|
+
workspaceId,
|
|
126
|
+
agentId: opts.agent.id,
|
|
127
|
+
agentVersion: opts.agent.version,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (opts.skills)
|
|
131
|
+
this.preparingSessionSkills.set(sessionId, opts.skills);
|
|
132
|
+
try {
|
|
133
|
+
const sandboxContext = opts.environmentId === undefined
|
|
134
|
+
? undefined
|
|
135
|
+
: { environmentId: opts.environmentId };
|
|
136
|
+
const handle = await this.getOrCreateHandle(workspaceId, sessionId, opts.fileMounts, sandboxContext, opts.vaultIds === undefined ? undefined : { vaultIds: opts.vaultIds });
|
|
137
|
+
this.touch(sessionId, handle);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
if (opts.agent)
|
|
141
|
+
this.preparingSessionAgents.delete(sessionId);
|
|
142
|
+
if (opts.skills)
|
|
143
|
+
this.preparingSessionSkills.delete(sessionId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
claimCustomToolResult(workspaceId, sessionId, event) {
|
|
147
|
+
return this.customToolBridge.claimResult(workspaceId, sessionId, event);
|
|
148
|
+
}
|
|
149
|
+
claimToolConfirmation(workspaceId, sessionId, event) {
|
|
150
|
+
return this.toolPermissionBridge.claimConfirmation(workspaceId, sessionId, event);
|
|
151
|
+
}
|
|
152
|
+
async interruptSession(_workspaceId, sessionId) {
|
|
153
|
+
const pending = this.pendingInterrupts.get(sessionId);
|
|
154
|
+
if (pending)
|
|
155
|
+
return pending;
|
|
156
|
+
const interrupt = this.interruptSessionInternal(sessionId);
|
|
157
|
+
this.pendingInterrupts.set(sessionId, interrupt);
|
|
158
|
+
try {
|
|
159
|
+
await interrupt;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
if (this.pendingInterrupts.get(sessionId) === interrupt) {
|
|
163
|
+
this.pendingInterrupts.delete(sessionId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async interruptSessionInternal(sessionId) {
|
|
168
|
+
const existing = this.sessions.get(sessionId);
|
|
169
|
+
if (existing) {
|
|
170
|
+
await this.interruptHandle(sessionId, existing);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const pending = this.pendingSessions.get(sessionId);
|
|
174
|
+
if (!pending)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
const handle = await pending;
|
|
178
|
+
await this.interruptHandle(sessionId, handle);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Interrupting a failed/closed pending session is a no-op for callers.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
customToolNames(workspaceId, sessionId) {
|
|
185
|
+
const handle = this.sessions.get(sessionId);
|
|
186
|
+
if (handle)
|
|
187
|
+
return handle.customToolNames;
|
|
188
|
+
return this.customToolBridge.customToolNames(workspaceId, sessionId, this.preparingSessionAgentContext(workspaceId, sessionId));
|
|
189
|
+
}
|
|
190
|
+
publicToolUseIdForPiToolCallId(_workspaceId, sessionId, piToolCallId) {
|
|
191
|
+
return this.toolPermissionBridge.publicToolUseIdForPiToolCallId(sessionId, piToolCallId);
|
|
192
|
+
}
|
|
193
|
+
suppressPiToolUse(_workspaceId, sessionId, piToolCallId) {
|
|
194
|
+
return this.toolPermissionBridge.suppressPiToolUse(sessionId, piToolCallId);
|
|
195
|
+
}
|
|
196
|
+
async collectSessionOutputs(_workspaceId, sessionId) {
|
|
197
|
+
const handle = this.sessions.get(sessionId);
|
|
198
|
+
if (!handle?.sandbox) {
|
|
199
|
+
return { kind: "unsupported", reason: "no_live_sandbox" };
|
|
200
|
+
}
|
|
201
|
+
if (typeof handle.sandbox.collectOutputFiles !== "function") {
|
|
202
|
+
return { kind: "unsupported", reason: "provider_unsupported" };
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: "collected",
|
|
206
|
+
files: await handle.sandbox.collectOutputFiles(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
close() {
|
|
210
|
+
this.closed = true;
|
|
211
|
+
for (const [sessionId, handle] of this.sessions) {
|
|
212
|
+
if (handle.running) {
|
|
213
|
+
handle.closeWhenIdle = true;
|
|
214
|
+
this.scheduleEviction(sessionId, handle);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.evict(sessionId, handle);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async closeSession(workspaceId, sessionId) {
|
|
222
|
+
this.closedSessionIds.add(sessionId);
|
|
223
|
+
this.mcpFailureCounts.delete(sessionId);
|
|
224
|
+
try {
|
|
225
|
+
const existing = this.sessions.get(sessionId);
|
|
226
|
+
if (existing) {
|
|
227
|
+
await this.closeHandle(sessionId, existing);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const pending = this.pendingSessions.get(sessionId);
|
|
231
|
+
if (!pending)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
const handle = await pending;
|
|
235
|
+
await this.closeHandle(sessionId, handle);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// A close-requested pending session disposes itself during materialization.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
this.closedSessionIds.delete(sessionId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async *runOnSession(workspaceId, sessionId, text, signal) {
|
|
246
|
+
// Cross-request interrupt/message ordering: a message that arrives while
|
|
247
|
+
// abort is settling waits and then starts a fresh post-interrupt turn.
|
|
248
|
+
await this.waitForPendingInterrupt(sessionId);
|
|
249
|
+
const handle = await this.getOrCreateHandle(workspaceId, sessionId);
|
|
250
|
+
if (this.closed) {
|
|
251
|
+
this.evict(sessionId, handle);
|
|
252
|
+
throw new Error("PiSessionRunner is closed");
|
|
253
|
+
}
|
|
254
|
+
this.touch(sessionId, handle);
|
|
255
|
+
if (handle.running && !handle.needsFreshPromptAfterInterrupt) {
|
|
256
|
+
try {
|
|
257
|
+
await handle.session.followUp(text);
|
|
258
|
+
this.touch(sessionId, handle);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
this.evict(sessionId, handle);
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
handle.needsFreshPromptAfterInterrupt = false;
|
|
267
|
+
const queue = [];
|
|
268
|
+
const gatedEvents = [];
|
|
269
|
+
const activeSandboxedToolCalls = new Map();
|
|
270
|
+
let done = false;
|
|
271
|
+
let failure;
|
|
272
|
+
let becameFollowUp = false;
|
|
273
|
+
let wake;
|
|
274
|
+
const stop = handle.session.subscribe((event) => {
|
|
275
|
+
updateRunning(handle, event);
|
|
276
|
+
queue.push(event);
|
|
277
|
+
wake?.();
|
|
278
|
+
});
|
|
279
|
+
const previousInternal = handle.emitInternal;
|
|
280
|
+
const installedInternalEmitter = previousInternal === undefined;
|
|
281
|
+
if (installedInternalEmitter) {
|
|
282
|
+
handle.emitInternal = (event) => {
|
|
283
|
+
if (event.type === "oma.tool_permission_use") {
|
|
284
|
+
const match = takeMessageEndForToolCall(handle.sandbox, [gatedEvents, queue], event.piToolCallId);
|
|
285
|
+
if (match !== undefined) {
|
|
286
|
+
queue.push({
|
|
287
|
+
type: "oma.tool_permission_with_model_end",
|
|
288
|
+
messageEnd: match.event,
|
|
289
|
+
permissionUse: event,
|
|
290
|
+
suppressedPiToolCallIds: match.suppressedPiToolCallIds,
|
|
291
|
+
});
|
|
292
|
+
wake?.();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (event.type === "oma.mcp_tool_use") {
|
|
297
|
+
// Opportunistic coalescing, mirroring the permission path: when
|
|
298
|
+
// the message_end carrying this toolCall is still queued, persist
|
|
299
|
+
// it atomically with the use event; otherwise the use event stands
|
|
300
|
+
// alone (both orders produce correct wire output — plan 0122 §4.4).
|
|
301
|
+
const match = takeMessageEndForMcpToolCall(handle.mcpToolNames, [gatedEvents, queue], event.piToolCallId);
|
|
302
|
+
if (match !== undefined) {
|
|
303
|
+
queue.push({
|
|
304
|
+
type: "oma.mcp_tool_with_model_end",
|
|
305
|
+
messageEnd: match.event,
|
|
306
|
+
mcpToolUse: event,
|
|
307
|
+
suppressedPiToolCallIds: match.suppressedPiToolCallIds,
|
|
308
|
+
});
|
|
309
|
+
wake?.();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
queue.push(event);
|
|
314
|
+
wake?.();
|
|
315
|
+
};
|
|
316
|
+
// Connect failures queued at session build flush now — the first
|
|
317
|
+
// moment an event consumer exists (plan 0122 §4.2).
|
|
318
|
+
for (const failure of handle.pendingMcpFailures.splice(0)) {
|
|
319
|
+
queue.push(failure);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const onAbort = () => {
|
|
323
|
+
void handle.session.abort();
|
|
324
|
+
wake?.();
|
|
325
|
+
};
|
|
326
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
327
|
+
const run = handle.session
|
|
328
|
+
.prompt(text)
|
|
329
|
+
.catch(async (error) => {
|
|
330
|
+
if (isAlreadyProcessing(error)) {
|
|
331
|
+
await handle.session.followUp(text);
|
|
332
|
+
// Best effort for the losing prompt race: once Pi confirms this turn
|
|
333
|
+
// is a follow-up, discard overlap events captured by this temporary
|
|
334
|
+
// subscriber. A pre-rejection event can still escape; in practice that
|
|
335
|
+
// should be limited to early status frames, while the winning prompt
|
|
336
|
+
// subscriber owns the full turn and follow-up output.
|
|
337
|
+
becameFollowUp = true;
|
|
338
|
+
queue.length = 0;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
failure = error;
|
|
342
|
+
})
|
|
343
|
+
.finally(() => {
|
|
344
|
+
done = true;
|
|
345
|
+
wake?.();
|
|
346
|
+
});
|
|
347
|
+
try {
|
|
348
|
+
while (!done || queue.length > 0) {
|
|
349
|
+
if (becameFollowUp)
|
|
350
|
+
break;
|
|
351
|
+
if (queue.length === 0) {
|
|
352
|
+
await new Promise((resolve) => {
|
|
353
|
+
wake = resolve;
|
|
354
|
+
if (done || becameFollowUp || queue.length > 0) {
|
|
355
|
+
wake = undefined;
|
|
356
|
+
resolve();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
wake = undefined;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const event = queue.shift();
|
|
363
|
+
if (isInternalRuntimeEvent(event)) {
|
|
364
|
+
yield event;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const sandboxedToolCalls = sandboxedToolCallsInMessage(handle.sandbox, event);
|
|
368
|
+
if (sandboxedToolCalls.length > 0) {
|
|
369
|
+
for (const call of sandboxedToolCalls) {
|
|
370
|
+
activeSandboxedToolCalls.set(call.toolCallId, call.toolName);
|
|
371
|
+
}
|
|
372
|
+
gatedEvents.push(event);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const sandboxedStart = sandboxedToolEvent(handle.sandbox, event, "tool_execution_start");
|
|
376
|
+
if (sandboxedStart) {
|
|
377
|
+
activeSandboxedToolCalls.set(sandboxedStart.toolCallId, sandboxedStart.toolName);
|
|
378
|
+
gatedEvents.push(event);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const sandboxedEnd = sandboxedToolEvent(handle.sandbox, event, "tool_execution_end");
|
|
382
|
+
if (activeSandboxedToolCalls.size > 0) {
|
|
383
|
+
gatedEvents.push(event);
|
|
384
|
+
if (sandboxedEnd) {
|
|
385
|
+
assertSandboxProviderHandledToolCall({
|
|
386
|
+
sandbox: handle.sandbox,
|
|
387
|
+
toolPermissionBridge: this.toolPermissionBridge,
|
|
388
|
+
sessionId,
|
|
389
|
+
toolName: sandboxedEnd.toolName,
|
|
390
|
+
toolCallId: sandboxedEnd.toolCallId,
|
|
391
|
+
isError: sandboxedEnd.isError,
|
|
392
|
+
});
|
|
393
|
+
assertSandboxProviderHealthy(handle.sandbox);
|
|
394
|
+
activeSandboxedToolCalls.delete(sandboxedEnd.toolCallId);
|
|
395
|
+
if (activeSandboxedToolCalls.size === 0) {
|
|
396
|
+
const releasableEvents = gatedEvents.splice(0);
|
|
397
|
+
for (const releasableEvent of releasableEvents) {
|
|
398
|
+
yield releasableEvent;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (sandboxedEnd) {
|
|
405
|
+
assertSandboxProviderHandledToolCall({
|
|
406
|
+
sandbox: handle.sandbox,
|
|
407
|
+
toolPermissionBridge: this.toolPermissionBridge,
|
|
408
|
+
sessionId,
|
|
409
|
+
toolName: sandboxedEnd.toolName,
|
|
410
|
+
toolCallId: sandboxedEnd.toolCallId,
|
|
411
|
+
isError: sandboxedEnd.isError,
|
|
412
|
+
});
|
|
413
|
+
assertSandboxProviderHealthy(handle.sandbox);
|
|
414
|
+
yield event;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
yield event;
|
|
418
|
+
}
|
|
419
|
+
await run;
|
|
420
|
+
if (failure)
|
|
421
|
+
throw failure;
|
|
422
|
+
if (activeSandboxedToolCalls.size > 0 || gatedEvents.length > 0) {
|
|
423
|
+
throw new Error("Sandboxed builtin tool execution ended without validation");
|
|
424
|
+
}
|
|
425
|
+
if (handle.closeWhenIdle) {
|
|
426
|
+
this.evict(sessionId, handle);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
this.touch(sessionId, handle);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
this.evict(sessionId, handle);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
if (installedInternalEmitter) {
|
|
438
|
+
handle.emitInternal = previousInternal;
|
|
439
|
+
}
|
|
440
|
+
stop();
|
|
441
|
+
signal?.removeEventListener("abort", onAbort);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async getOrCreateHandle(workspaceId, sessionId, fileMounts, context, credentialContext) {
|
|
445
|
+
if (this.closed)
|
|
446
|
+
throw new Error("PiSessionRunner is closed");
|
|
447
|
+
if (this.closedSessionIds.has(sessionId)) {
|
|
448
|
+
throw new Error(`Runtime session ${sessionId} is closed`);
|
|
449
|
+
}
|
|
450
|
+
const existing = this.sessions.get(sessionId);
|
|
451
|
+
if (existing) {
|
|
452
|
+
if (existing.closeWhenIdle && !existing.running) {
|
|
453
|
+
this.evict(sessionId, existing);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
return existing;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const pending = this.pendingSessions.get(sessionId);
|
|
460
|
+
if (pending)
|
|
461
|
+
return pending;
|
|
462
|
+
const created = (async () => {
|
|
463
|
+
const customToolContext = this.preparingSessionAgentContext(workspaceId, sessionId);
|
|
464
|
+
const agentRevision = this.opts.agentRevision?.(workspaceId, sessionId, customToolContext);
|
|
465
|
+
const revisionModel = agentRevision === undefined
|
|
466
|
+
? undefined
|
|
467
|
+
: this.modelCatalog.resolve(agentRevision.model);
|
|
468
|
+
if (agentRevision !== undefined && revisionModel === undefined) {
|
|
469
|
+
throw new Error(`Pi model not available: ${agentRevision.model.provider}/${agentRevision.model.id}`);
|
|
470
|
+
}
|
|
471
|
+
if (agentRevision !== undefined &&
|
|
472
|
+
revisionModel !== undefined &&
|
|
473
|
+
!this.modelCatalog.hasConfiguredAuth(revisionModel)) {
|
|
474
|
+
throw new Error(`Credentials for model provider ${agentRevision.model.provider} are not configured on this deployment`);
|
|
475
|
+
}
|
|
476
|
+
const customToolNames = new Set((this.opts.customTools?.(workspaceId, sessionId, customToolContext) ?? []).map((tool) => tool.name));
|
|
477
|
+
const sandboxProviderFactory = this.resolveSandboxProviderFactory();
|
|
478
|
+
let sandbox;
|
|
479
|
+
let session;
|
|
480
|
+
let mcp = EMPTY_MCP;
|
|
481
|
+
try {
|
|
482
|
+
try {
|
|
483
|
+
sandbox = await sandboxProviderFactory?.(workspaceId, sessionId, context);
|
|
484
|
+
}
|
|
485
|
+
catch (providerError) {
|
|
486
|
+
this.opts.onSandboxEvent?.("error");
|
|
487
|
+
throw providerError;
|
|
488
|
+
}
|
|
489
|
+
if (sandbox !== undefined)
|
|
490
|
+
this.opts.onSandboxEvent?.("created");
|
|
491
|
+
const mounts = fileMounts ??
|
|
492
|
+
(await this.opts.fileMountResolver?.(workspaceId, sessionId)) ??
|
|
493
|
+
[];
|
|
494
|
+
if (mounts.length > 0) {
|
|
495
|
+
if (typeof sandbox?.materializeFileResources !== "function") {
|
|
496
|
+
throw new RuntimeUnsupportedSessionFileResourcesError();
|
|
497
|
+
}
|
|
498
|
+
await sandbox.materializeFileResources(mounts);
|
|
499
|
+
}
|
|
500
|
+
// MCP wiring is skipped entirely under a sessionFactory — the
|
|
501
|
+
// factory bypasses createPiSession, so tools could never reach Pi
|
|
502
|
+
// and a green test on this path would test nothing (plan 0122 §5).
|
|
503
|
+
if (this.sessionFactory === undefined) {
|
|
504
|
+
mcp = await this.prepareMcp(workspaceId, sessionId, {
|
|
505
|
+
...customToolContext,
|
|
506
|
+
...(credentialContext === undefined ? {} : credentialContext),
|
|
507
|
+
});
|
|
508
|
+
for (const name of mcp.toolNames) {
|
|
509
|
+
if (customToolNames.has(name)) {
|
|
510
|
+
throw new Error(`MCP tool name collides with a custom tool: ${name}`);
|
|
511
|
+
}
|
|
512
|
+
customToolNames.add(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
assertNoSandboxCustomToolNameCollision(sandbox, customToolNames);
|
|
516
|
+
session =
|
|
517
|
+
this.sessionFactory === undefined
|
|
518
|
+
? await this.createPiSession(workspaceId, sessionId, sandbox, customToolContext, agentRevision, revisionModel, mcp.toolDefinitions)
|
|
519
|
+
: await this.sessionFactory(workspaceId, sessionId);
|
|
520
|
+
assertActiveToolSurface(session, this.enabledSandboxToolNames(workspaceId, sessionId, sandbox), customToolNames);
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
this.disposeSandbox(sandbox);
|
|
524
|
+
this.disposeMcpConnections(mcp.connections);
|
|
525
|
+
session?.dispose();
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
if (!session)
|
|
529
|
+
throw new Error("Pi session was not created");
|
|
530
|
+
const handle = {
|
|
531
|
+
session,
|
|
532
|
+
sandbox,
|
|
533
|
+
running: false,
|
|
534
|
+
needsFreshPromptAfterInterrupt: false,
|
|
535
|
+
// Retrying MCP failures use fresh-handle mechanics: dispose at
|
|
536
|
+
// idle so the NEXT turn re-runs connect/discovery (plan 0122 §4.6).
|
|
537
|
+
// Exhausted failures also close: unchanged fingerprints skip dialing
|
|
538
|
+
// on rebuild, while rotated credentials can reset the budget.
|
|
539
|
+
closeWhenIdle: mcp.pendingFailures.length > 0 || mcp.skippedExhausted,
|
|
540
|
+
lastUsedAt: this.now(),
|
|
541
|
+
timer: undefined,
|
|
542
|
+
customToolNames,
|
|
543
|
+
mcpToolNames: mcp.toolNames,
|
|
544
|
+
mcpConnections: mcp.connections,
|
|
545
|
+
pendingMcpFailures: [...mcp.pendingFailures],
|
|
546
|
+
mcpFailureEmittedServers: new Set(),
|
|
547
|
+
emitInternal: undefined,
|
|
548
|
+
};
|
|
549
|
+
if (this.closed || this.closedSessionIds.has(sessionId)) {
|
|
550
|
+
this.disposeSandbox(sandbox);
|
|
551
|
+
this.disposeMcpConnections(mcp.connections);
|
|
552
|
+
session.dispose();
|
|
553
|
+
this.pendingSessions.delete(sessionId);
|
|
554
|
+
throw new Error(this.closed
|
|
555
|
+
? "PiSessionRunner is closed"
|
|
556
|
+
: `Runtime session ${sessionId} is closed`);
|
|
557
|
+
}
|
|
558
|
+
this.sessions.set(sessionId, handle);
|
|
559
|
+
this.pendingSessions.delete(sessionId);
|
|
560
|
+
this.scheduleEviction(sessionId, handle);
|
|
561
|
+
return handle;
|
|
562
|
+
})()
|
|
563
|
+
.catch((error) => {
|
|
564
|
+
this.pendingSessions.delete(sessionId);
|
|
565
|
+
throw error;
|
|
566
|
+
});
|
|
567
|
+
this.pendingSessions.set(sessionId, created);
|
|
568
|
+
return created;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Connect + discover the agent's declared MCP servers (plan 0122 §4.2).
|
|
572
|
+
* Failures are captured, never propagated: they queue on the handle and
|
|
573
|
+
* flush as session.error events at turn start (emitInternal does not exist
|
|
574
|
+
* yet at build time). Exhausted servers (count >= max, or MCP disabled by
|
|
575
|
+
* deployment) are skipped without dialing and without re-emitting.
|
|
576
|
+
*/
|
|
577
|
+
async prepareMcp(workspaceId, sessionId, context) {
|
|
578
|
+
const mcpOpts = this.opts.mcp;
|
|
579
|
+
if (!mcpOpts)
|
|
580
|
+
return EMPTY_MCP;
|
|
581
|
+
const servers = mcpOpts.servers?.(workspaceId, sessionId, context) ?? [];
|
|
582
|
+
if (servers.length === 0)
|
|
583
|
+
return EMPTY_MCP;
|
|
584
|
+
const maxFailures = mcpOpts.maxConsecutiveFailures ?? DEFAULT_MCP_MAX_CONSECUTIVE_FAILURES;
|
|
585
|
+
let counts = this.mcpFailureCounts.get(sessionId);
|
|
586
|
+
if (!counts) {
|
|
587
|
+
counts = new Map();
|
|
588
|
+
this.mcpFailureCounts.set(sessionId, counts);
|
|
589
|
+
}
|
|
590
|
+
const connections = [];
|
|
591
|
+
const toolDefinitions = [];
|
|
592
|
+
const toolNames = new Set();
|
|
593
|
+
const pendingFailures = [];
|
|
594
|
+
let skippedExhausted = false;
|
|
595
|
+
// Dial concurrently (review 0122-M1: a serial loop let N slow servers
|
|
596
|
+
// stall the turn for N×timeout while pinning the sandbox); outcomes are
|
|
597
|
+
// processed in declaration order so failure events stay deterministic.
|
|
598
|
+
const dialable = [];
|
|
599
|
+
for (const server of servers) {
|
|
600
|
+
if (!mcpOpts.enabled) {
|
|
601
|
+
counts.set(server.name, {
|
|
602
|
+
count: maxFailures,
|
|
603
|
+
fingerprint: MCP_UNAUTHENTICATED_FINGERPRINT,
|
|
604
|
+
}); // emit once
|
|
605
|
+
pendingFailures.push({
|
|
606
|
+
type: "oma.mcp_connection_failed",
|
|
607
|
+
mcpServerName: server.name,
|
|
608
|
+
message: "MCP is disabled by deployment configuration",
|
|
609
|
+
retryStatus: "exhausted",
|
|
610
|
+
});
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
dialable.push(server);
|
|
614
|
+
}
|
|
615
|
+
const dialed = await Promise.all(dialable.map(async (server) => {
|
|
616
|
+
let credential;
|
|
617
|
+
try {
|
|
618
|
+
credential = await mcpOpts.credentials?.(workspaceId, sessionId, server.url, context);
|
|
619
|
+
const fingerprint = credential?.fingerprint ?? MCP_UNAUTHENTICATED_FINGERPRINT;
|
|
620
|
+
const count = currentMcpFailureCount(counts, server.name, fingerprint);
|
|
621
|
+
if (count >= maxFailures) {
|
|
622
|
+
return { server, credential, fingerprint, count, skipped: true };
|
|
623
|
+
}
|
|
624
|
+
const connection = await McpConnection.connect(server, {
|
|
625
|
+
fetch: this.mcpFetch,
|
|
626
|
+
...(credential === undefined
|
|
627
|
+
? {}
|
|
628
|
+
: { credential }),
|
|
629
|
+
...(mcpOpts.operationTimeoutMs === undefined
|
|
630
|
+
? {}
|
|
631
|
+
: { operationTimeoutMs: mcpOpts.operationTimeoutMs }),
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
server,
|
|
635
|
+
credential,
|
|
636
|
+
fingerprint: connection.credential?.fingerprint ?? MCP_UNAUTHENTICATED_FINGERPRINT,
|
|
637
|
+
count,
|
|
638
|
+
connection,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
const finalFingerprint = credential?.fingerprint ?? MCP_UNAUTHENTICATED_FINGERPRINT;
|
|
643
|
+
return {
|
|
644
|
+
server,
|
|
645
|
+
fingerprint: finalFingerprint,
|
|
646
|
+
count: currentMcpFailureCount(counts, server.name, finalFingerprint),
|
|
647
|
+
failure: error instanceof Error ? error.message : String(error),
|
|
648
|
+
errorType: mcpConnectErrorType(error),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}));
|
|
652
|
+
for (const outcome of dialed) {
|
|
653
|
+
const { server, count } = outcome;
|
|
654
|
+
if ("skipped" in outcome && outcome.skipped) {
|
|
655
|
+
skippedExhausted = true;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (!("connection" in outcome) || outcome.connection === undefined) {
|
|
659
|
+
const failures = count + 1;
|
|
660
|
+
counts.set(server.name, { count: failures, fingerprint: outcome.fingerprint });
|
|
661
|
+
const errorType = "errorType" in outcome ? outcome.errorType : undefined;
|
|
662
|
+
mcpOpts.onConnection?.(errorType === "mcp_authentication_failed_error"
|
|
663
|
+
? "auth_failed"
|
|
664
|
+
: "connect_failed");
|
|
665
|
+
pendingFailures.push({
|
|
666
|
+
type: "oma.mcp_connection_failed",
|
|
667
|
+
errorType,
|
|
668
|
+
mcpServerName: server.name,
|
|
669
|
+
message: mcpFailureMessage(errorType),
|
|
670
|
+
retryStatus: failures >= maxFailures ? "exhausted" : "retrying",
|
|
671
|
+
});
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const connection = outcome.connection;
|
|
675
|
+
counts.set(server.name, { count: 0, fingerprint: outcome.fingerprint });
|
|
676
|
+
mcpOpts.onConnection?.("connected");
|
|
677
|
+
connections.push(connection);
|
|
678
|
+
const definitions = createMcpToolDefinitions({
|
|
679
|
+
workspaceId,
|
|
680
|
+
sessionId,
|
|
681
|
+
connection,
|
|
682
|
+
permissionBridge: this.toolPermissionBridge,
|
|
683
|
+
getEmitter: () => this.sessions.get(sessionId)?.emitInternal,
|
|
684
|
+
...(mcpOpts.access === undefined ? {} : { access: mcpOpts.access }),
|
|
685
|
+
...(context === undefined ? {} : { agentContext: context }),
|
|
686
|
+
...(mcpOpts.outputCapBytes === undefined
|
|
687
|
+
? {}
|
|
688
|
+
: { outputCapBytes: mcpOpts.outputCapBytes }),
|
|
689
|
+
...(mcpOpts.onToolCall === undefined
|
|
690
|
+
? {}
|
|
691
|
+
: { onToolCall: mcpOpts.onToolCall }),
|
|
692
|
+
// Mid-call transport failure is connection-class (review 0122-M1,
|
|
693
|
+
// Codex-adv): count it against the retry budget, surface the
|
|
694
|
+
// structured mcp_connection_failed_error (once per server per
|
|
695
|
+
// handle), and reconnect next turn via fresh-handle mechanics.
|
|
696
|
+
onTransportFailure: (failedServerName, error) => {
|
|
697
|
+
const fingerprint = connection.credential?.fingerprint ?? MCP_UNAUTHENTICATED_FINGERPRINT;
|
|
698
|
+
const failures = currentMcpFailureCount(counts, failedServerName, fingerprint) + 1;
|
|
699
|
+
counts.set(failedServerName, { count: failures, fingerprint });
|
|
700
|
+
const handle = this.sessions.get(sessionId);
|
|
701
|
+
if (!handle)
|
|
702
|
+
return;
|
|
703
|
+
handle.closeWhenIdle = true;
|
|
704
|
+
if (handle.mcpFailureEmittedServers.has(failedServerName))
|
|
705
|
+
return;
|
|
706
|
+
handle.mcpFailureEmittedServers.add(failedServerName);
|
|
707
|
+
handle.emitInternal?.({
|
|
708
|
+
type: "oma.mcp_connection_failed",
|
|
709
|
+
errorType: mcpConnectErrorType(error),
|
|
710
|
+
mcpServerName: failedServerName,
|
|
711
|
+
message: mcpFailureMessage(mcpConnectErrorType(error)),
|
|
712
|
+
retryStatus: failures >= maxFailures ? "exhausted" : "retrying",
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
for (const definition of definitions) {
|
|
717
|
+
if (toolNames.has(definition.name)) {
|
|
718
|
+
throw new Error(`MCP tool name collision across servers: ${definition.name}`);
|
|
719
|
+
}
|
|
720
|
+
toolNames.add(definition.name);
|
|
721
|
+
toolDefinitions.push(definition);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
connections,
|
|
726
|
+
toolDefinitions,
|
|
727
|
+
toolNames,
|
|
728
|
+
pendingFailures,
|
|
729
|
+
skippedExhausted,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
disposeMcpConnections(connections) {
|
|
733
|
+
for (const connection of connections) {
|
|
734
|
+
void connection.close();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async createPiSession(workspaceId, sessionId, sandbox, context, revision, revisionModel, mcpTools = []) {
|
|
738
|
+
const provider = revision?.model.provider ?? this.modelCatalog.defaultModel.provider;
|
|
739
|
+
const modelId = revision?.model.id ?? this.modelCatalog.defaultModel.id;
|
|
740
|
+
const model = revisionModel ?? this.modelCatalog.resolve({ provider, id: modelId });
|
|
741
|
+
if (!model) {
|
|
742
|
+
throw new Error(`Pi model not available: ${provider}/${modelId}`);
|
|
743
|
+
}
|
|
744
|
+
const customToolNames = (this.opts.customTools?.(workspaceId, sessionId, context) ?? []).map((tool) => tool.name);
|
|
745
|
+
const sandboxTools = this.enabledSandboxTools(workspaceId, sessionId, sandbox);
|
|
746
|
+
const customTools = [
|
|
747
|
+
...sandboxTools,
|
|
748
|
+
...this.customToolBridge.createTools(workspaceId, sessionId, () => this.sessions.get(sessionId)?.emitInternal, context),
|
|
749
|
+
...mcpTools,
|
|
750
|
+
];
|
|
751
|
+
const mcpToolNames = mcpTools.map((tool) => tool.name);
|
|
752
|
+
const { session } = await createAgentSession({
|
|
753
|
+
model,
|
|
754
|
+
thinkingLevel: this.opts.thinkingLevel ?? "off",
|
|
755
|
+
noTools: "builtin",
|
|
756
|
+
tools: sandbox
|
|
757
|
+
? [
|
|
758
|
+
...sandboxTools.map((tool) => tool.name),
|
|
759
|
+
...customToolNames,
|
|
760
|
+
...mcpToolNames,
|
|
761
|
+
]
|
|
762
|
+
: [...customToolNames, ...mcpToolNames],
|
|
763
|
+
customTools,
|
|
764
|
+
authStorage: this.authStorage,
|
|
765
|
+
modelRegistry: this.modelRegistry,
|
|
766
|
+
sessionManager: SessionManager.inMemory(),
|
|
767
|
+
resourceLoader: await this.createResourceLoader(workspaceId, sessionId, revision?.system ?? undefined),
|
|
768
|
+
});
|
|
769
|
+
return session;
|
|
770
|
+
}
|
|
771
|
+
async createResourceLoader(workspaceId, sessionId, systemPrompt) {
|
|
772
|
+
const skills = this.preparingSessionSkills.get(sessionId) ?? this.opts.skills?.(workspaceId, sessionId) ?? [];
|
|
773
|
+
const loader = buildSessionSkillsResourceLoader(skills, systemPrompt);
|
|
774
|
+
// createAgentSession only reload()s a loader it constructs itself; a
|
|
775
|
+
// caller-provided one is used as-is. skillsOverride runs inside reload(),
|
|
776
|
+
// so without this the model never sees <available_skills> even though the
|
|
777
|
+
// skill files are mounted in the sandbox.
|
|
778
|
+
await loader.reload();
|
|
779
|
+
return loader;
|
|
780
|
+
}
|
|
781
|
+
preparingSessionAgentContext(workspaceId, sessionId) {
|
|
782
|
+
const preparing = this.preparingSessionAgents.get(sessionId);
|
|
783
|
+
if (preparing?.workspaceId !== workspaceId)
|
|
784
|
+
return undefined;
|
|
785
|
+
return {
|
|
786
|
+
agentId: preparing.agentId,
|
|
787
|
+
agentVersion: preparing.agentVersion,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
resolveSandboxProviderFactory() {
|
|
791
|
+
// Direct factories are trusted internal wiring for tests and already-built
|
|
792
|
+
// providers. Never derive this option from public request, agent, or prompt
|
|
793
|
+
// input; untrusted provider choice must go through sandboxProviderSelection.
|
|
794
|
+
if (this.opts.sandboxProviderFactory)
|
|
795
|
+
return this.opts.sandboxProviderFactory;
|
|
796
|
+
return resolveSandboxProviderFactory(this.opts.sandboxProviderSelection, this.opts.sandboxProviderSelectionOptions);
|
|
797
|
+
}
|
|
798
|
+
touch(sessionId, handle) {
|
|
799
|
+
handle.lastUsedAt = this.now();
|
|
800
|
+
this.scheduleEviction(sessionId, handle);
|
|
801
|
+
}
|
|
802
|
+
enabledSandboxTools(workspaceId, sessionId, sandbox) {
|
|
803
|
+
if (!sandbox)
|
|
804
|
+
return [];
|
|
805
|
+
const toolsByName = new Map(sandbox.tools.map((tool) => [tool.name, tool]));
|
|
806
|
+
const out = [];
|
|
807
|
+
for (const toolName of sandbox.toolNames) {
|
|
808
|
+
const access = this.toolPermissionBridge.access(workspaceId, sessionId, toolName);
|
|
809
|
+
if (!access.enabled)
|
|
810
|
+
continue;
|
|
811
|
+
const tool = toolsByName.get(toolName);
|
|
812
|
+
if (!tool)
|
|
813
|
+
continue;
|
|
814
|
+
out.push(this.toolPermissionBridge.wrapTool(workspaceId, sessionId, toolName, tool, () => this.sessions.get(sessionId)?.emitInternal));
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
enabledSandboxToolNames(workspaceId, sessionId, sandbox) {
|
|
819
|
+
if (!sandbox)
|
|
820
|
+
return new Set();
|
|
821
|
+
const out = new Set();
|
|
822
|
+
for (const toolName of sandbox.toolNames) {
|
|
823
|
+
const access = this.toolPermissionBridge.access(workspaceId, sessionId, toolName);
|
|
824
|
+
if (access.enabled)
|
|
825
|
+
out.add(toolName);
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
scheduleEviction(sessionId, handle) {
|
|
830
|
+
if (handle.timer)
|
|
831
|
+
clearTimeout(handle.timer);
|
|
832
|
+
if (this.idleTtlMs <= 0)
|
|
833
|
+
return;
|
|
834
|
+
handle.timer = setTimeout(() => {
|
|
835
|
+
if (handle.running) {
|
|
836
|
+
this.scheduleEviction(sessionId, handle);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (handle.closeWhenIdle) {
|
|
840
|
+
this.evict(sessionId, handle);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (this.now() - handle.lastUsedAt < this.idleTtlMs) {
|
|
844
|
+
this.scheduleEviction(sessionId, handle);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
this.evict(sessionId, handle);
|
|
848
|
+
}, this.idleTtlMs);
|
|
849
|
+
}
|
|
850
|
+
async closeHandle(sessionId, handle) {
|
|
851
|
+
if (handle.running) {
|
|
852
|
+
handle.closeWhenIdle = true;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
await handle.session.abort();
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
// Disposal below is the authoritative cleanup path.
|
|
859
|
+
}
|
|
860
|
+
this.evict(sessionId, handle);
|
|
861
|
+
}
|
|
862
|
+
async interruptHandle(sessionId, handle) {
|
|
863
|
+
handle.session.clearQueue?.();
|
|
864
|
+
await handle.session.abort();
|
|
865
|
+
handle.session.clearQueue?.();
|
|
866
|
+
handle.running = false;
|
|
867
|
+
handle.needsFreshPromptAfterInterrupt = true;
|
|
868
|
+
this.touch(sessionId, handle);
|
|
869
|
+
}
|
|
870
|
+
async waitForPendingInterrupt(sessionId) {
|
|
871
|
+
const interrupt = this.pendingInterrupts.get(sessionId);
|
|
872
|
+
if (!interrupt)
|
|
873
|
+
return;
|
|
874
|
+
try {
|
|
875
|
+
await interrupt;
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
// interruptSession callers observe abort errors; message delivery waits only
|
|
879
|
+
// for the abort window to close before deciding prompt vs follow-up.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
evict(sessionId, handle) {
|
|
883
|
+
if (handle.timer)
|
|
884
|
+
clearTimeout(handle.timer);
|
|
885
|
+
if (this.sessions.get(sessionId) === handle) {
|
|
886
|
+
this.sessions.delete(sessionId);
|
|
887
|
+
}
|
|
888
|
+
this.customToolBridge.rejectSession(sessionId, new Error("Runtime session evicted"));
|
|
889
|
+
this.toolPermissionBridge.rejectSession(sessionId, new Error("Runtime session evicted"));
|
|
890
|
+
this.disposeSandbox(handle.sandbox);
|
|
891
|
+
this.disposeMcpConnections(handle.mcpConnections);
|
|
892
|
+
handle.session.dispose();
|
|
893
|
+
}
|
|
894
|
+
disposeSandbox(sandbox) {
|
|
895
|
+
if (sandbox === undefined)
|
|
896
|
+
return;
|
|
897
|
+
sandbox.dispose();
|
|
898
|
+
this.opts.onSandboxEvent?.("disposed");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function assertActiveToolSurface(session, sandboxToolNames, customToolNames) {
|
|
902
|
+
if (typeof session.getActiveToolNames !== "function") {
|
|
903
|
+
throw new Error("Pi runtime session does not expose active tool names");
|
|
904
|
+
}
|
|
905
|
+
const activeToolNames = session.getActiveToolNames();
|
|
906
|
+
const expectedToolNames = new Set(customToolNames);
|
|
907
|
+
for (const name of sandboxToolNames)
|
|
908
|
+
expectedToolNames.add(name);
|
|
909
|
+
for (const name of activeToolNames) {
|
|
910
|
+
if (expectedToolNames.has(name))
|
|
911
|
+
continue;
|
|
912
|
+
throw new Error(`Unexpected active Pi tool after sandbox setup: ${name}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function assertNoSandboxCustomToolNameCollision(sandbox, customToolNames) {
|
|
916
|
+
if (!sandbox)
|
|
917
|
+
return;
|
|
918
|
+
for (const name of customToolNames) {
|
|
919
|
+
if (!sandbox.toolNames.has(name))
|
|
920
|
+
continue;
|
|
921
|
+
throw new Error(`Custom tool name conflicts with sandbox builtin: ${name}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
function updateRunning(handle, event) {
|
|
925
|
+
if (typeof event !== "object" || event === null)
|
|
926
|
+
return;
|
|
927
|
+
const type = event.type;
|
|
928
|
+
if (type === "agent_start")
|
|
929
|
+
handle.running = true;
|
|
930
|
+
if (type === "agent_end")
|
|
931
|
+
handle.running = false;
|
|
932
|
+
}
|
|
933
|
+
function isAlreadyProcessing(error) {
|
|
934
|
+
return (error instanceof Error &&
|
|
935
|
+
error.message.includes(ALREADY_PROCESSING_MESSAGE));
|
|
936
|
+
}
|
|
937
|
+
function isInternalRuntimeEvent(event) {
|
|
938
|
+
if (typeof event !== "object" || event === null)
|
|
939
|
+
return false;
|
|
940
|
+
const type = event.type;
|
|
941
|
+
return (type === "oma.custom_tool_use" ||
|
|
942
|
+
type === "oma.tool_permission_use" ||
|
|
943
|
+
type === "oma.tool_permission_with_model_end" ||
|
|
944
|
+
type === "oma.mcp_tool_use" ||
|
|
945
|
+
type === "oma.mcp_tool_with_model_end" ||
|
|
946
|
+
type === "oma.mcp_tool_result" ||
|
|
947
|
+
type === "oma.mcp_connection_failed");
|
|
948
|
+
}
|
|
949
|
+
function currentMcpFailureCount(counts, serverName, fingerprint) {
|
|
950
|
+
const existing = counts.get(serverName);
|
|
951
|
+
if (!existing || existing.fingerprint !== fingerprint) {
|
|
952
|
+
counts.set(serverName, { count: 0, fingerprint });
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
return existing.count;
|
|
956
|
+
}
|
|
957
|
+
function mcpConnectErrorType(error) {
|
|
958
|
+
const code = error.code;
|
|
959
|
+
return code === 401 || code === 403
|
|
960
|
+
? "mcp_authentication_failed_error"
|
|
961
|
+
: "mcp_connection_failed_error";
|
|
962
|
+
}
|
|
963
|
+
function mcpFailureMessage(errorType) {
|
|
964
|
+
return errorType === "mcp_authentication_failed_error"
|
|
965
|
+
? "MCP authentication failed"
|
|
966
|
+
: "MCP connection failed";
|
|
967
|
+
}
|
|
968
|
+
const EMPTY_MCP = {
|
|
969
|
+
connections: [],
|
|
970
|
+
toolDefinitions: [],
|
|
971
|
+
toolNames: new Set(),
|
|
972
|
+
pendingFailures: [],
|
|
973
|
+
skippedExhausted: false,
|
|
974
|
+
};
|
|
975
|
+
/** message_end toolCall blocks whose pi-name is an MCP tool of this handle. Exported for direct contract tests (review 0122-M1, Sonnet 1). */
|
|
976
|
+
export function mcpToolCallsInMessage(mcpToolNames, event) {
|
|
977
|
+
if (mcpToolNames.size === 0)
|
|
978
|
+
return [];
|
|
979
|
+
if (typeof event !== "object" || event === null)
|
|
980
|
+
return [];
|
|
981
|
+
const typed = event;
|
|
982
|
+
if (typed.type !== "message_end")
|
|
983
|
+
return [];
|
|
984
|
+
if (typeof typed.message !== "object" || typed.message === null)
|
|
985
|
+
return [];
|
|
986
|
+
const message = typed.message;
|
|
987
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
988
|
+
return [];
|
|
989
|
+
const out = [];
|
|
990
|
+
for (const block of message.content) {
|
|
991
|
+
if (typeof block !== "object" || block === null)
|
|
992
|
+
continue;
|
|
993
|
+
const toolCall = block;
|
|
994
|
+
if (toolCall.type !== "toolCall")
|
|
995
|
+
continue;
|
|
996
|
+
if (typeof toolCall.id !== "string" || typeof toolCall.name !== "string") {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (!mcpToolNames.has(toolCall.name))
|
|
1000
|
+
continue;
|
|
1001
|
+
out.push({ toolCallId: toolCall.id });
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
/** MCP-aware variant of takeMessageEndForToolCall (plan 0122 §4.4 step 2). Exported for direct contract tests. */
|
|
1006
|
+
export function takeMessageEndForMcpToolCall(mcpToolNames, eventQueues, piToolCallId) {
|
|
1007
|
+
for (const events of eventQueues) {
|
|
1008
|
+
let suppressedPiToolCallIds = [];
|
|
1009
|
+
const index = events.findIndex((event) => {
|
|
1010
|
+
const calls = mcpToolCallsInMessage(mcpToolNames, event);
|
|
1011
|
+
if (!calls.some((call) => call.toolCallId === piToolCallId))
|
|
1012
|
+
return false;
|
|
1013
|
+
suppressedPiToolCallIds = calls.map((call) => call.toolCallId);
|
|
1014
|
+
return true;
|
|
1015
|
+
});
|
|
1016
|
+
if (index === -1)
|
|
1017
|
+
continue;
|
|
1018
|
+
const [event] = events.splice(index, 1);
|
|
1019
|
+
return { event, suppressedPiToolCallIds };
|
|
1020
|
+
}
|
|
1021
|
+
return undefined;
|
|
1022
|
+
}
|
|
1023
|
+
function sandboxedToolEvent(sandbox, event, eventType) {
|
|
1024
|
+
if (!sandbox || typeof event !== "object" || event === null)
|
|
1025
|
+
return undefined;
|
|
1026
|
+
const typed = event;
|
|
1027
|
+
if (typed.type !== eventType)
|
|
1028
|
+
return undefined;
|
|
1029
|
+
if (typeof typed.toolName !== "string")
|
|
1030
|
+
return undefined;
|
|
1031
|
+
if (typeof typed.toolCallId !== "string")
|
|
1032
|
+
return undefined;
|
|
1033
|
+
if (!sandbox.toolNames.has(typed.toolName))
|
|
1034
|
+
return undefined;
|
|
1035
|
+
return {
|
|
1036
|
+
toolName: typed.toolName,
|
|
1037
|
+
toolCallId: typed.toolCallId,
|
|
1038
|
+
isError: typed.isError === true,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function sandboxedToolCallsInMessage(sandbox, event) {
|
|
1042
|
+
if (!sandbox || typeof event !== "object" || event === null)
|
|
1043
|
+
return [];
|
|
1044
|
+
const typed = event;
|
|
1045
|
+
if (typed.type !== "message_end")
|
|
1046
|
+
return [];
|
|
1047
|
+
if (typeof typed.message !== "object" || typed.message === null)
|
|
1048
|
+
return [];
|
|
1049
|
+
const message = typed.message;
|
|
1050
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
1051
|
+
return [];
|
|
1052
|
+
const out = [];
|
|
1053
|
+
for (const block of message.content) {
|
|
1054
|
+
if (typeof block !== "object" || block === null)
|
|
1055
|
+
continue;
|
|
1056
|
+
const toolCall = block;
|
|
1057
|
+
if (toolCall.type !== "toolCall")
|
|
1058
|
+
continue;
|
|
1059
|
+
if (typeof toolCall.id !== "string" || typeof toolCall.name !== "string") {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (!sandbox.toolNames.has(toolCall.name))
|
|
1063
|
+
continue;
|
|
1064
|
+
out.push({
|
|
1065
|
+
toolName: toolCall.name,
|
|
1066
|
+
toolCallId: toolCall.id,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return out;
|
|
1070
|
+
}
|
|
1071
|
+
function takeMessageEndForToolCall(sandbox, eventQueues, piToolCallId) {
|
|
1072
|
+
for (const events of eventQueues) {
|
|
1073
|
+
let suppressedPiToolCallIds = [];
|
|
1074
|
+
const index = events.findIndex((event) => {
|
|
1075
|
+
const calls = sandboxedToolCallsInMessage(sandbox, event);
|
|
1076
|
+
if (!calls.some((call) => call.toolCallId === piToolCallId))
|
|
1077
|
+
return false;
|
|
1078
|
+
suppressedPiToolCallIds = calls.map((call) => call.toolCallId);
|
|
1079
|
+
return true;
|
|
1080
|
+
});
|
|
1081
|
+
if (index === -1)
|
|
1082
|
+
continue;
|
|
1083
|
+
const [event] = events.splice(index, 1);
|
|
1084
|
+
return { event, suppressedPiToolCallIds };
|
|
1085
|
+
}
|
|
1086
|
+
return undefined;
|
|
1087
|
+
}
|
|
1088
|
+
function assertSandboxProviderHandledToolCall(opts) {
|
|
1089
|
+
if (!opts.sandbox)
|
|
1090
|
+
return;
|
|
1091
|
+
if (opts.sandbox.invocations.toolCallIds[opts.toolName].has(opts.toolCallId)) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (opts.isError &&
|
|
1095
|
+
opts.toolPermissionBridge.permissionDenied(opts.sessionId, opts.toolCallId)) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
throw new Error(`Sandboxed builtin tool ${opts.toolName} executed without invoking the sandbox provider`);
|
|
1099
|
+
}
|
|
1100
|
+
function assertSandboxProviderHealthy(sandbox) {
|
|
1101
|
+
if (sandbox?.isPoisoned?.() !== true)
|
|
1102
|
+
return;
|
|
1103
|
+
throw new Error("Sandbox provider was poisoned during tool execution");
|
|
1104
|
+
}
|