@amodalai/runtime 0.1.15 → 0.1.17
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/src/agent/agent-runner.js +13 -1
- package/dist/src/agent/agent-runner.js.map +1 -1
- package/dist/src/agent/config-watcher.d.ts +2 -2
- package/dist/src/agent/config-watcher.js +28 -15
- package/dist/src/agent/config-watcher.js.map +1 -1
- package/dist/src/agent/config-watcher.test.js +13 -4
- package/dist/src/agent/config-watcher.test.js.map +1 -1
- package/dist/src/agent/eval-store.d.ts +14 -0
- package/dist/src/agent/eval-store.js +44 -0
- package/dist/src/agent/eval-store.js.map +1 -1
- package/dist/src/agent/feedback-store.d.ts +39 -0
- package/dist/src/agent/feedback-store.js +98 -0
- package/dist/src/agent/feedback-store.js.map +1 -0
- package/dist/src/agent/local-server.d.ts +1 -1
- package/dist/src/agent/local-server.js +48 -28
- package/dist/src/agent/local-server.js.map +1 -1
- package/dist/src/agent/local-server.test.js +2 -1
- package/dist/src/agent/local-server.test.js.map +1 -1
- package/dist/src/agent/proactive/proactive-runner.d.ts +3 -3
- package/dist/src/agent/proactive/proactive-runner.js +3 -3
- package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
- package/dist/src/agent/proactive/proactive-runner.test.js +2 -2
- package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
- package/dist/src/agent/routes/admin-chat.d.ts +3 -2
- package/dist/src/agent/routes/admin-chat.js +3 -3
- package/dist/src/agent/routes/admin-chat.js.map +1 -1
- package/dist/src/agent/routes/chat.d.ts +6 -6
- package/dist/src/agent/routes/chat.js +3 -3
- package/dist/src/agent/routes/chat.js.map +1 -1
- package/dist/src/agent/routes/chat.test.js +7 -6
- package/dist/src/agent/routes/chat.test.js.map +1 -1
- package/dist/src/agent/routes/evals.d.ts +2 -2
- package/dist/src/agent/routes/evals.js +143 -68
- package/dist/src/agent/routes/evals.js.map +1 -1
- package/dist/src/agent/routes/feedback.d.ts +11 -0
- package/dist/src/agent/routes/feedback.js +72 -0
- package/dist/src/agent/routes/feedback.js.map +1 -0
- package/dist/src/agent/routes/files.js +118 -12
- package/dist/src/agent/routes/files.js.map +1 -1
- package/dist/src/agent/routes/inspect.d.ts +2 -2
- package/dist/src/agent/routes/inspect.js +44 -22
- package/dist/src/agent/routes/inspect.js.map +1 -1
- package/dist/src/agent/routes/inspect.test.js +11 -16
- package/dist/src/agent/routes/inspect.test.js.map +1 -1
- package/dist/src/agent/routes/task.d.ts +2 -2
- package/dist/src/agent/routes/task.js +4 -4
- package/dist/src/agent/routes/task.js.map +1 -1
- package/dist/src/agent/routes/task.test.js.map +1 -1
- package/dist/src/agent/session-store.d.ts +2 -2
- package/dist/src/agent/session-store.js +9 -6
- package/dist/src/agent/session-store.js.map +1 -1
- package/dist/src/agent/snapshot-server.js +10 -2
- package/dist/src/agent/snapshot-server.js.map +1 -1
- package/dist/src/cron/heartbeat-runner.d.ts +3 -6
- package/dist/src/cron/heartbeat-runner.js +1 -10
- package/dist/src/cron/heartbeat-runner.js.map +1 -1
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +4 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/middleware/auth.d.ts +3 -19
- package/dist/src/middleware/auth.js +0 -118
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/routes/ai-stream.d.ts +8 -7
- package/dist/src/routes/ai-stream.js +3 -16
- package/dist/src/routes/ai-stream.js.map +1 -1
- package/dist/src/routes/chat-stream.d.ts +4 -3
- package/dist/src/routes/chat-stream.js +5 -17
- package/dist/src/routes/chat-stream.js.map +1 -1
- package/dist/src/routes/chat.d.ts +4 -2
- package/dist/src/routes/chat.js +2 -14
- package/dist/src/routes/chat.js.map +1 -1
- package/dist/src/routes/chat.test.js +2 -2
- package/dist/src/routes/chat.test.js.map +1 -1
- package/dist/src/server.d.ts +16 -3
- package/dist/src/server.js +24 -27
- package/dist/src/server.js.map +1 -1
- package/dist/src/server.test.js +37 -6
- package/dist/src/server.test.js.map +1 -1
- package/dist/src/session/admin-file-tools.d.ts +136 -0
- package/dist/src/session/admin-file-tools.js +240 -0
- package/dist/src/session/admin-file-tools.js.map +1 -0
- package/dist/src/session/custom-tool-adapter.d.ts +74 -0
- package/dist/src/session/custom-tool-adapter.js +149 -0
- package/dist/src/session/custom-tool-adapter.js.map +1 -0
- package/dist/src/session/session-manager.d.ts +101 -3
- package/dist/src/session/session-manager.js +467 -31
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/session/session-manager.test.js +93 -58
- package/dist/src/session/session-manager.test.js.map +1 -1
- package/dist/src/session/session-runner.d.ts +29 -13
- package/dist/src/session/session-runner.js +40 -91
- package/dist/src/session/session-runner.js.map +1 -1
- package/dist/src/session/session-runner.test.js +70 -80
- package/dist/src/session/session-runner.test.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/dist/src/agent/agent-runner.test.d.ts +0 -6
- package/dist/src/agent/agent-runner.test.js +0 -552
- package/dist/src/agent/agent-runner.test.js.map +0 -1
- package/dist/src/agent/session-manager.d.ts +0 -88
- package/dist/src/agent/session-manager.js +0 -341
- package/dist/src/agent/session-manager.js.map +0 -1
- package/dist/src/agent/session-manager.test.d.ts +0 -6
- package/dist/src/agent/session-manager.test.js +0 -145
- package/dist/src/agent/session-manager.test.js.map +0 -1
- package/dist/src/audit/audit-client.d.ts +0 -46
- package/dist/src/audit/audit-client.js +0 -83
- package/dist/src/audit/audit-client.js.map +0 -1
- package/dist/src/middleware/auth.test.d.ts +0 -6
- package/dist/src/middleware/auth.test.js +0 -260
- package/dist/src/middleware/auth.test.js.map +0 -1
- package/dist/src/routes/sessions.d.ts +0 -14
- package/dist/src/routes/sessions.js +0 -82
- package/dist/src/routes/sessions.js.map +0 -1
- package/dist/src/utils/jwt-verify.d.ts +0 -19
- package/dist/src/utils/jwt-verify.js +0 -32
- package/dist/src/utils/jwt-verify.js.map +0 -1
- package/dist/src/utils/jwt-verify.test.d.ts +0 -6
- package/dist/src/utils/jwt-verify.test.js +0 -150
- package/dist/src/utils/jwt-verify.test.js.map +0 -1
|
@@ -4,23 +4,59 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { AmodalConfig, Scheduler, ROOT_SCHEDULER_ID, ApprovalMode, PolicyDecision, AgentSDK, buildDefaultPrompt, } from '@amodalai/core';
|
|
7
|
+
import { AmodalConfig, Scheduler, ROOT_SCHEDULER_ID, ApprovalMode, PolicyDecision, AgentSDK, buildDefaultPrompt, resolveScopeLabels, generateFieldGuidance, generateAlternativeLookupGuidance, PlanModeManager, McpManager, ensureAdminAgent, loadAdminAgent, } from '@amodalai/core';
|
|
8
8
|
import { convertSessionMessagesToHistory } from './history-converter.js';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve env: references in a string record.
|
|
11
|
+
* "env:VAR_NAME" → process.env.VAR_NAME value
|
|
12
|
+
*/
|
|
13
|
+
function resolveEnvRefs(record) {
|
|
14
|
+
const resolved = {};
|
|
15
|
+
for (const [key, value] of Object.entries(record)) {
|
|
16
|
+
if (value.startsWith('env:')) {
|
|
17
|
+
const envVar = value.slice(4);
|
|
18
|
+
resolved[key] = process.env[envVar] ?? '';
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
resolved[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
9
26
|
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
10
27
|
const DEFAULT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
28
|
const ASK_USER_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
29
|
+
/**
|
|
30
|
+
* Manages per-request sessions: creates Config + GeminiClient + Scheduler
|
|
31
|
+
* instances, tracks them by ID, and cleans up expired sessions.
|
|
32
|
+
*/
|
|
12
33
|
export class SessionManager {
|
|
13
34
|
sessions = new Map();
|
|
14
35
|
baseParams;
|
|
15
36
|
ttlMs;
|
|
16
37
|
platformApiUrl;
|
|
38
|
+
repo;
|
|
39
|
+
toolExecutor;
|
|
40
|
+
shellExecutor;
|
|
41
|
+
sharedStoreBackend;
|
|
42
|
+
sessionStore;
|
|
17
43
|
cleanupTimer = null;
|
|
18
44
|
/** Deduplicates concurrent hydration requests for the same conversation */
|
|
19
45
|
pendingHydrations = new Map();
|
|
46
|
+
/** Shared MCP manager for all sessions (lazy-initialized, reused) */
|
|
47
|
+
sharedMcpManager;
|
|
48
|
+
/** Persistent MCP manager for inspect operations (lazy-initialized) */
|
|
49
|
+
inspectMcp;
|
|
50
|
+
inspectMcpInitialized = false;
|
|
20
51
|
constructor(options) {
|
|
21
52
|
this.baseParams = options.baseParams;
|
|
22
53
|
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
23
54
|
this.platformApiUrl = options.platformApiUrl;
|
|
55
|
+
this.repo = options.repo;
|
|
56
|
+
this.toolExecutor = options.toolExecutor;
|
|
57
|
+
this.shellExecutor = options.shellExecutor;
|
|
58
|
+
this.sharedStoreBackend = options.storeBackend;
|
|
59
|
+
this.sessionStore = options.sessionStore;
|
|
24
60
|
const cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
|
|
25
61
|
this.cleanupTimer = setInterval(() => void this.cleanup(), cleanupIntervalMs);
|
|
26
62
|
// Don't keep the process alive just for cleanup
|
|
@@ -39,9 +75,8 @@ export class SessionManager {
|
|
|
39
75
|
approvalMode: ApprovalMode.YOLO,
|
|
40
76
|
interactive: false,
|
|
41
77
|
noBrowser: true,
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
coreTools: [],
|
|
78
|
+
// Build coreTools list based on repo config
|
|
79
|
+
coreTools: this.repo ? this.buildCoreToolsList(this.repo) : [],
|
|
45
80
|
policyEngineConfig: {
|
|
46
81
|
approvalMode: ApprovalMode.YOLO,
|
|
47
82
|
defaultDecision: PolicyDecision.ALLOW,
|
|
@@ -55,6 +90,45 @@ export class SessionManager {
|
|
|
55
90
|
if (role) {
|
|
56
91
|
sessionParams.activeRole = role;
|
|
57
92
|
}
|
|
93
|
+
// Inject repo config into session params when in local mode
|
|
94
|
+
if (this.repo) {
|
|
95
|
+
const { buildConnectionsMap } = await import('@amodalai/core');
|
|
96
|
+
const connectionsMap = buildConnectionsMap(this.repo.connections);
|
|
97
|
+
sessionParams.connections = connectionsMap;
|
|
98
|
+
sessionParams.appDocuments = this.repo.knowledge.map((k) => ({
|
|
99
|
+
id: k.name,
|
|
100
|
+
scope_type: 'application',
|
|
101
|
+
scope_id: 'local',
|
|
102
|
+
title: k.title ?? k.name,
|
|
103
|
+
category: 'system_docs',
|
|
104
|
+
body: k.body,
|
|
105
|
+
tags: [],
|
|
106
|
+
status: 'active',
|
|
107
|
+
created_by: 'local',
|
|
108
|
+
created_at: new Date().toISOString(),
|
|
109
|
+
updated_at: new Date().toISOString(),
|
|
110
|
+
}));
|
|
111
|
+
sessionParams.bundleSkills = this.repo.skills.map((s) => ({
|
|
112
|
+
name: s.name,
|
|
113
|
+
description: s.description ?? '',
|
|
114
|
+
body: s.body,
|
|
115
|
+
}));
|
|
116
|
+
sessionParams.basePrompt = this.repo.config.basePrompt;
|
|
117
|
+
sessionParams.agentName = this.repo.config.name;
|
|
118
|
+
sessionParams.agentContext = this.repo.config.userContext ?? this.repo.config.description;
|
|
119
|
+
// Model config from repo
|
|
120
|
+
const mainModel = this.repo.config.models?.main;
|
|
121
|
+
if (mainModel) {
|
|
122
|
+
sessionParams.modelConfig = {
|
|
123
|
+
provider: mainModel.provider ?? 'anthropic',
|
|
124
|
+
model: mainModel.model,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Stores
|
|
128
|
+
if (this.repo.stores.length > 0) {
|
|
129
|
+
sessionParams.stores = this.repo.stores;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
58
132
|
let config;
|
|
59
133
|
// Platform session: use AgentSDK to fetch KB docs, org details, secrets
|
|
60
134
|
if (auth?.token && this.platformApiUrl) {
|
|
@@ -87,7 +161,82 @@ export class SessionManager {
|
|
|
87
161
|
}
|
|
88
162
|
}
|
|
89
163
|
config = new AmodalConfig(sessionParams);
|
|
90
|
-
|
|
164
|
+
// For non-platform sessions, skip the full upstream Config.initialize()
|
|
165
|
+
// which hangs trying to scan files, discover agents, and authenticate
|
|
166
|
+
// with Gemini. Instead, do a minimal init:
|
|
167
|
+
// 1. initializeAuth() — sets up MultiProviderContentGenerator for non-Google
|
|
168
|
+
// 2. registerTools() — registers amodal tools on the upstream registry
|
|
169
|
+
//
|
|
170
|
+
// We need to manually create the tool registry and message bus since
|
|
171
|
+
// the upstream Config only creates them in _initialize().
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
173
|
+
const upstreamRaw = config.getUpstreamConfig();
|
|
174
|
+
// Initialize storage (required by many upstream components)
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
176
|
+
const storage = (upstreamRaw['storage'] ?? upstreamRaw['_storage']);
|
|
177
|
+
if (storage?.initialize) {
|
|
178
|
+
await storage.initialize();
|
|
179
|
+
}
|
|
180
|
+
// Create agent registry and tool registry if not already created
|
|
181
|
+
if (!upstreamRaw['toolRegistry'] && !upstreamRaw['_toolRegistry']) {
|
|
182
|
+
// Agent registry must exist before tool registry (createToolRegistry references it).
|
|
183
|
+
// Use a minimal stub — we register Amodal subagents separately.
|
|
184
|
+
if (!upstreamRaw['agentRegistry']) {
|
|
185
|
+
upstreamRaw['agentRegistry'] = {
|
|
186
|
+
getAllDefinitions: () => [],
|
|
187
|
+
agents: new Map(),
|
|
188
|
+
allDefinitions: new Map(),
|
|
189
|
+
initialize: async () => { },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Prompt registry stub (referenced by some tools)
|
|
193
|
+
if (!upstreamRaw['promptRegistry']) {
|
|
194
|
+
upstreamRaw['promptRegistry'] = { getPrompts: () => [] };
|
|
195
|
+
}
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
197
|
+
const upstreamConfig = config.getUpstreamConfig();
|
|
198
|
+
const registry = await upstreamConfig.createToolRegistry();
|
|
199
|
+
upstreamRaw['_toolRegistry'] = registry;
|
|
200
|
+
}
|
|
201
|
+
// Initialize auth (replaces content generator for non-Google providers)
|
|
202
|
+
await config.initializeAuth();
|
|
203
|
+
// Register amodal tools (request, present, knowledge, stores)
|
|
204
|
+
await config.registerTools();
|
|
205
|
+
// Register custom tools from repo tools/ directory
|
|
206
|
+
if (this.repo && this.repo.tools.length > 0) {
|
|
207
|
+
const { CustomToolAdapter } = await import('./custom-tool-adapter.js');
|
|
208
|
+
const { LocalToolExecutor } = await import('../agent/tool-executor-local.js');
|
|
209
|
+
const executor = this.toolExecutor ?? new LocalToolExecutor();
|
|
210
|
+
const registry = config.getUpstreamConfig().getToolRegistry();
|
|
211
|
+
// Session isn't created yet — we'll set it after session construction
|
|
212
|
+
// For now store the tools to register after session is built
|
|
213
|
+
for (const tool of this.repo.tools) {
|
|
214
|
+
if (tool.confirm === 'never')
|
|
215
|
+
continue; // hidden from LLM
|
|
216
|
+
// Create a placeholder session for the adapter — will be updated below
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- partial session for tool context
|
|
218
|
+
const placeholder = { config, toolExecutor: executor };
|
|
219
|
+
const adapter = new CustomToolAdapter(tool, placeholder, executor);
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- adapter matches upstream tool interface
|
|
221
|
+
registry.registerTool(adapter);
|
|
222
|
+
}
|
|
223
|
+
process.stderr.write(`[SESSION] Registered ${String(this.repo.tools.length)} custom tool(s)\n`);
|
|
224
|
+
}
|
|
225
|
+
// Set model on upstream config so GeminiClient.startChat() can resolve it
|
|
226
|
+
if (sessionParams.modelConfig?.model) {
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
228
|
+
const upstreamForModel = config.getUpstreamConfig();
|
|
229
|
+
if (!upstreamForModel.model && !upstreamForModel._model) {
|
|
230
|
+
upstreamForModel['model'] = sessionParams.modelConfig.model;
|
|
231
|
+
upstreamForModel['_model'] = sessionParams.modelConfig.model;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Initialize the GeminiClient (creates the Chat instance)
|
|
235
|
+
// Must happen after tool registry and content generator are set up.
|
|
236
|
+
const gemClient = config.getGeminiClient();
|
|
237
|
+
if (!gemClient.isInitialized()) {
|
|
238
|
+
await gemClient.initialize();
|
|
239
|
+
}
|
|
91
240
|
}
|
|
92
241
|
// Inject app secrets as process env vars so shell_exec commands can
|
|
93
242
|
// reference them (e.g. $API_BASE_URL in curl commands). The shell execution
|
|
@@ -97,11 +246,6 @@ export class SessionManager {
|
|
|
97
246
|
const connections = config.getConnections();
|
|
98
247
|
const connKeys = Object.keys(connections).filter((k) => k !== '_secrets');
|
|
99
248
|
process.stderr.write(`[SESSION] connections: ${connKeys.join(', ') || '(none)'}\n`);
|
|
100
|
-
if (sessionType === 'onboarding' && !connections['platform-api']) {
|
|
101
|
-
process.stderr.write(`[SESSION] WARNING: onboarding session has no platform-api connection — ` +
|
|
102
|
-
`the request tool cannot create resources. Check that ADMIN_APP_ID is set ` +
|
|
103
|
-
`and the seed has run.\n`);
|
|
104
|
-
}
|
|
105
249
|
// App secrets are available to tools via session-scoped getSessionEnv()
|
|
106
250
|
// (through ToolContext). They are NOT injected into process.env to prevent
|
|
107
251
|
// cross-app secret leakage in multi-session runtimes.
|
|
@@ -139,9 +283,10 @@ export class SessionManager {
|
|
|
139
283
|
// Initialize store backend if stores are configured.
|
|
140
284
|
// Must happen before initializeAuth/tool registration so store tools
|
|
141
285
|
// are available when registerAmodalTools runs.
|
|
286
|
+
// In local dev mode, use the shared store backend from options.
|
|
142
287
|
const stores = config.getStores();
|
|
143
|
-
let storeBackend;
|
|
144
|
-
if (stores.length > 0) {
|
|
288
|
+
let storeBackend = this.sharedStoreBackend;
|
|
289
|
+
if (stores.length > 0 && !storeBackend) {
|
|
145
290
|
try {
|
|
146
291
|
const { PGLiteStoreBackend } = await import('../stores/pglite-store-backend.js');
|
|
147
292
|
const backend = new PGLiteStoreBackend();
|
|
@@ -166,7 +311,36 @@ export class SessionManager {
|
|
|
166
311
|
name: config.getAgentName() ?? 'Amodal Agent',
|
|
167
312
|
description: config.getAgentDescription(),
|
|
168
313
|
agentContext: config.getAgentContext(),
|
|
169
|
-
|
|
314
|
+
agentOverride: this.repo?.agents?.main,
|
|
315
|
+
connections: this.repo?.connections ? Array.from(this.repo.connections.values()).map((conn) => ({
|
|
316
|
+
name: conn.name,
|
|
317
|
+
endpoints: (conn.surface ?? [])
|
|
318
|
+
.filter((ep) => ep.included)
|
|
319
|
+
.map((ep) => ({ method: ep.method, path: ep.path, description: ep.description })),
|
|
320
|
+
entities: conn.entities,
|
|
321
|
+
rules: conn.rules,
|
|
322
|
+
})) : undefined,
|
|
323
|
+
skills: this.repo?.skills?.map((s) => ({
|
|
324
|
+
name: s.name,
|
|
325
|
+
description: s.description ?? '',
|
|
326
|
+
trigger: s.trigger,
|
|
327
|
+
body: s.body,
|
|
328
|
+
})),
|
|
329
|
+
knowledge: this.repo?.knowledge?.map((k) => ({
|
|
330
|
+
name: k.name,
|
|
331
|
+
title: k.title,
|
|
332
|
+
body: k.body,
|
|
333
|
+
})),
|
|
334
|
+
...(this.repo?.connections ? (() => {
|
|
335
|
+
const { scopeLabels } = resolveScopeLabels(this.repo.connections, []);
|
|
336
|
+
const fieldGuidance = generateFieldGuidance(this.repo.connections, []);
|
|
337
|
+
const altLookup = generateAlternativeLookupGuidance(this.repo.connections);
|
|
338
|
+
return {
|
|
339
|
+
fieldGuidance: fieldGuidance || undefined,
|
|
340
|
+
scopeLabels: Object.keys(scopeLabels).length > 0 ? scopeLabels : undefined,
|
|
341
|
+
alternativeLookupGuidance: altLookup || undefined,
|
|
342
|
+
};
|
|
343
|
+
})() : {}),
|
|
170
344
|
});
|
|
171
345
|
try {
|
|
172
346
|
geminiClient.getChat().setSystemInstruction(systemPrompt);
|
|
@@ -283,7 +457,79 @@ export class SessionManager {
|
|
|
283
457
|
model: mc?.model ?? config.getModel(),
|
|
284
458
|
provider: mc?.provider,
|
|
285
459
|
storeBackend,
|
|
460
|
+
planModeManager: new PlanModeManager(),
|
|
461
|
+
toolExecutor: this.toolExecutor,
|
|
462
|
+
shellExecutor: this.shellExecutor,
|
|
463
|
+
appId: auth?.applicationId,
|
|
286
464
|
};
|
|
465
|
+
// Share MCP connection across sessions — connect once, reuse for all
|
|
466
|
+
if (this.repo && !this.sharedMcpManager) {
|
|
467
|
+
await this.initSharedMcp(this.repo);
|
|
468
|
+
}
|
|
469
|
+
if (this.sharedMcpManager) {
|
|
470
|
+
session.mcpManager = this.sharedMcpManager;
|
|
471
|
+
// Register MCP tools on the upstream tool registry so the Gemini client can see them
|
|
472
|
+
try {
|
|
473
|
+
const upstream = config.getUpstreamConfig();
|
|
474
|
+
const toolRegistry = upstream.getToolRegistry();
|
|
475
|
+
const mcpTools = session.mcpManager.getDiscoveredTools();
|
|
476
|
+
for (const mcpTool of mcpTools) {
|
|
477
|
+
// Adapter matching upstream DeclarativeTool interface (build/silentBuild/schema/getSchema)
|
|
478
|
+
const mcpSession = session;
|
|
479
|
+
const adapter = {
|
|
480
|
+
name: mcpTool.name,
|
|
481
|
+
displayName: mcpTool.name,
|
|
482
|
+
description: mcpTool.description,
|
|
483
|
+
kind: 'declarative',
|
|
484
|
+
parameterSchema: mcpTool.parameters,
|
|
485
|
+
get isReadOnly() { return true; },
|
|
486
|
+
get toolAnnotations() { return undefined; },
|
|
487
|
+
get schema() { return this.getSchema(); },
|
|
488
|
+
getSchema() {
|
|
489
|
+
return {
|
|
490
|
+
name: mcpTool.name,
|
|
491
|
+
description: mcpTool.description,
|
|
492
|
+
parametersJsonSchema: mcpTool.parameters,
|
|
493
|
+
};
|
|
494
|
+
},
|
|
495
|
+
build(params) {
|
|
496
|
+
return {
|
|
497
|
+
name: mcpTool.name,
|
|
498
|
+
params,
|
|
499
|
+
execute: async () => adapter.validateBuildAndExecute(params),
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
silentBuild(params) {
|
|
503
|
+
return this.build(params);
|
|
504
|
+
},
|
|
505
|
+
async validateBuildAndExecute(params) {
|
|
506
|
+
try {
|
|
507
|
+
const result = await mcpSession.mcpManager.callTool(mcpTool.name, params);
|
|
508
|
+
const output = result.content
|
|
509
|
+
.map((c) => c.type === 'text' && c.text ? c.text : `[${c.type}]`)
|
|
510
|
+
.join('\n');
|
|
511
|
+
return {
|
|
512
|
+
llmContent: result.isError ? `Error: ${output}` : output,
|
|
513
|
+
returnDisplay: output.slice(0, 200),
|
|
514
|
+
...(result.isError ? { error: { message: output, type: 'EXECUTION_FAILED' } } : {}),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
519
|
+
return { llmContent: `Error: ${msg}`, returnDisplay: msg, error: { message: msg, type: 'EXECUTION_FAILED' } };
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
toolRegistry.registerTool(adapter); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
524
|
+
}
|
|
525
|
+
await geminiClient.setTools();
|
|
526
|
+
process.stderr.write(`[MCP] Registered ${String(mcpTools.length)} MCP tools on tool registry\n`);
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
+
process.stderr.write(`[MCP] Failed to register MCP tools: ${msg}\n`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
287
533
|
this.sessions.set(sessionId, session);
|
|
288
534
|
return session;
|
|
289
535
|
}
|
|
@@ -329,31 +575,20 @@ export class SessionManager {
|
|
|
329
575
|
}
|
|
330
576
|
}
|
|
331
577
|
async doHydrate(conversationId, role, auth, sessionType) {
|
|
332
|
-
// Fetch stored conversation
|
|
333
|
-
|
|
578
|
+
// Fetch stored conversation via pluggable session store
|
|
579
|
+
if (!this.sessionStore || !auth?.applicationId || !auth.token) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
334
582
|
let record;
|
|
335
583
|
try {
|
|
336
|
-
|
|
337
|
-
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
338
|
-
const response = await fetch(url, {
|
|
339
|
-
signal: controller.signal,
|
|
340
|
-
headers: {
|
|
341
|
-
Authorization: `Bearer ${auth.token}`,
|
|
342
|
-
Accept: 'application/json',
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
clearTimeout(timer);
|
|
346
|
-
if (!response.ok) {
|
|
347
|
-
process.stderr.write(`[HYDRATE] Failed to fetch conversation ${conversationId}: HTTP ${response.status}\n`);
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- API response shape
|
|
351
|
-
record = (await response.json());
|
|
584
|
+
record = await this.sessionStore.getSession(auth.applicationId, conversationId, auth.token);
|
|
352
585
|
}
|
|
353
586
|
catch (err) {
|
|
354
587
|
process.stderr.write(`[HYDRATE] Error fetching conversation ${conversationId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
355
588
|
return null;
|
|
356
589
|
}
|
|
590
|
+
if (!record)
|
|
591
|
+
return null;
|
|
357
592
|
// No messages → nothing to hydrate
|
|
358
593
|
if (!record.messages || record.messages.length === 0)
|
|
359
594
|
return null;
|
|
@@ -395,6 +630,195 @@ export class SessionManager {
|
|
|
395
630
|
}
|
|
396
631
|
await session.config.shutdownAudit();
|
|
397
632
|
}
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// Local dev features
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
/**
|
|
637
|
+
* Get the repo (local dev mode).
|
|
638
|
+
*/
|
|
639
|
+
getRepo() {
|
|
640
|
+
return this.repo;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Update the repo for new sessions (hot reload).
|
|
644
|
+
* Existing sessions keep their old config.
|
|
645
|
+
*/
|
|
646
|
+
updateRepo(repo) {
|
|
647
|
+
this.repo = repo;
|
|
648
|
+
// Reset inspect MCP manager so it picks up new connections
|
|
649
|
+
this.inspectMcpInitialized = false;
|
|
650
|
+
this.inspectMcp = undefined;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Re-register a session under a different ID (e.g., restoring original ID on session restore).
|
|
654
|
+
*/
|
|
655
|
+
reregister(session, newId) {
|
|
656
|
+
this.sessions.delete(session.id);
|
|
657
|
+
session.id = newId;
|
|
658
|
+
this.sessions.set(newId, session);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Create an admin session for the config chat.
|
|
662
|
+
* Uses admin agent skills/knowledge but the current repo's connections/stores.
|
|
663
|
+
*
|
|
664
|
+
* Temporarily swaps repo fields so create() builds the prompt with admin
|
|
665
|
+
* content, then restores the original repo. This mirrors the old approach
|
|
666
|
+
* of building an adminRepo overlay.
|
|
667
|
+
*/
|
|
668
|
+
async createAdminSession(getPort) {
|
|
669
|
+
if (!this.repo) {
|
|
670
|
+
throw new Error('Admin sessions require a repo');
|
|
671
|
+
}
|
|
672
|
+
if (this.repo.source !== 'local') {
|
|
673
|
+
throw new Error('Admin sessions are only available for local repos');
|
|
674
|
+
}
|
|
675
|
+
const agentDir = await ensureAdminAgent(this.repo.origin);
|
|
676
|
+
const adminContent = await loadAdminAgent(agentDir);
|
|
677
|
+
// Save original repo fields
|
|
678
|
+
const origSkills = this.repo.skills;
|
|
679
|
+
const origKnowledge = this.repo.knowledge;
|
|
680
|
+
const origAgents = this.repo.agents;
|
|
681
|
+
const origAutomations = this.repo.automations;
|
|
682
|
+
// Swap in admin content so create() builds the prompt correctly
|
|
683
|
+
this.repo.skills = adminContent.skills;
|
|
684
|
+
this.repo.knowledge = adminContent.knowledge;
|
|
685
|
+
this.repo.agents = {
|
|
686
|
+
main: adminContent.agentPrompt ?? undefined,
|
|
687
|
+
simple: undefined,
|
|
688
|
+
subagents: [],
|
|
689
|
+
};
|
|
690
|
+
this.repo.automations = [];
|
|
691
|
+
let session;
|
|
692
|
+
try {
|
|
693
|
+
session = await this.create('admin');
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
// Restore original repo fields
|
|
697
|
+
this.repo.skills = origSkills;
|
|
698
|
+
this.repo.knowledge = origKnowledge;
|
|
699
|
+
this.repo.agents = origAgents;
|
|
700
|
+
this.repo.automations = origAutomations;
|
|
701
|
+
}
|
|
702
|
+
session.appId = 'admin';
|
|
703
|
+
// Register admin file tools (read/write/delete repo files)
|
|
704
|
+
try {
|
|
705
|
+
const { createReadRepoFileTool, createWriteRepoFileTool, createDeleteRepoFileTool } = await import('./admin-file-tools.js');
|
|
706
|
+
const repoRoot = this.repo.origin;
|
|
707
|
+
const upstream = session.config.getUpstreamConfig();
|
|
708
|
+
const toolRegistry = upstream.getToolRegistry();
|
|
709
|
+
toolRegistry.registerTool(createReadRepoFileTool(repoRoot)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
710
|
+
toolRegistry.registerTool(createWriteRepoFileTool(repoRoot)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
711
|
+
toolRegistry.registerTool(createDeleteRepoFileTool(repoRoot)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
712
|
+
// Internal API tool — lets admin query eval results, health, context, etc.
|
|
713
|
+
if (getPort) {
|
|
714
|
+
const { createInternalApiTool } = await import('./admin-file-tools.js');
|
|
715
|
+
toolRegistry.registerTool(createInternalApiTool(getPort)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
|
|
716
|
+
}
|
|
717
|
+
await session.geminiClient.setTools();
|
|
718
|
+
process.stderr.write('[ADMIN] Registered admin tools (file tools + internal_api)\n');
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
722
|
+
process.stderr.write(`[ADMIN] Failed to register file tools: ${msg}\n`);
|
|
723
|
+
}
|
|
724
|
+
return session;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Get a persistent MCP manager for inspect/health operations.
|
|
728
|
+
* Lazy-initialized on first call, reused across requests.
|
|
729
|
+
*/
|
|
730
|
+
async getInspectMcpManager() {
|
|
731
|
+
if (this.inspectMcpInitialized)
|
|
732
|
+
return this.inspectMcp;
|
|
733
|
+
this.inspectMcpInitialized = true;
|
|
734
|
+
if (!this.repo)
|
|
735
|
+
return undefined;
|
|
736
|
+
// Build MCP server configs from repo connections
|
|
737
|
+
const mcpServers = this.buildMcpConfigs(this.repo);
|
|
738
|
+
if (Object.keys(mcpServers).length === 0)
|
|
739
|
+
return undefined;
|
|
740
|
+
const manager = new McpManager();
|
|
741
|
+
try {
|
|
742
|
+
await manager.startServers(mcpServers);
|
|
743
|
+
this.inspectMcp = manager;
|
|
744
|
+
return manager;
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
748
|
+
process.stderr.write(`[INSPECT] MCP initialization failed: ${msg}\n`);
|
|
749
|
+
this.inspectMcp = manager;
|
|
750
|
+
return manager;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Initialize MCP servers for a session from repo connections.
|
|
755
|
+
*/
|
|
756
|
+
/**
|
|
757
|
+
* Initialize the shared MCP manager (once, reused across sessions).
|
|
758
|
+
* Avoids reconnecting MCP servers for every eval/judge/admin session.
|
|
759
|
+
*/
|
|
760
|
+
async initSharedMcp(repo) {
|
|
761
|
+
const mcpServers = this.buildMcpConfigs(repo);
|
|
762
|
+
if (Object.keys(mcpServers).length === 0)
|
|
763
|
+
return;
|
|
764
|
+
const manager = new McpManager();
|
|
765
|
+
try {
|
|
766
|
+
await manager.startServers(mcpServers);
|
|
767
|
+
if (manager.connectedCount > 0) {
|
|
768
|
+
this.sharedMcpManager = manager;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
773
|
+
process.stderr.write(`[SESSION] MCP initialization failed: ${msg}\n`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Build the list of upstream core tools to enable based on repo config.
|
|
778
|
+
* Only tools relevant to the Amodal runtime are included.
|
|
779
|
+
*/
|
|
780
|
+
buildCoreToolsList(repo) {
|
|
781
|
+
const tools = [
|
|
782
|
+
// Always available
|
|
783
|
+
'enter_plan_mode',
|
|
784
|
+
'exit_plan_mode',
|
|
785
|
+
'ask_user',
|
|
786
|
+
];
|
|
787
|
+
// Shell execution (opt-in via config.sandbox.shellExec)
|
|
788
|
+
if (repo.config.sandbox?.shellExec) {
|
|
789
|
+
tools.push('shell');
|
|
790
|
+
}
|
|
791
|
+
return tools;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Build MCP server configs from repo connections.
|
|
795
|
+
*/
|
|
796
|
+
buildMcpConfigs(repo) {
|
|
797
|
+
const mcpServers = {};
|
|
798
|
+
for (const [name, conn] of repo.connections) {
|
|
799
|
+
if (conn.spec.protocol === 'mcp') {
|
|
800
|
+
const resolvedHeaders = conn.spec.headers ? resolveEnvRefs(conn.spec.headers) : undefined;
|
|
801
|
+
const resolvedEnv = conn.spec.env ? resolveEnvRefs(conn.spec.env) : undefined;
|
|
802
|
+
mcpServers[name] = {
|
|
803
|
+
transport: conn.spec.transport ?? 'stdio',
|
|
804
|
+
command: conn.spec.command,
|
|
805
|
+
args: conn.spec.args,
|
|
806
|
+
env: resolvedEnv,
|
|
807
|
+
url: conn.spec.url,
|
|
808
|
+
headers: resolvedHeaders,
|
|
809
|
+
trust: conn.spec.trust,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (repo.mcpServers) {
|
|
814
|
+
for (const [name, config] of Object.entries(repo.mcpServers)) {
|
|
815
|
+
if (!mcpServers[name]) {
|
|
816
|
+
mcpServers[name] = config;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return mcpServers;
|
|
821
|
+
}
|
|
398
822
|
/**
|
|
399
823
|
* Remove sessions that have been idle longer than the TTL.
|
|
400
824
|
*/
|
|
@@ -419,6 +843,18 @@ export class SessionManager {
|
|
|
419
843
|
clearInterval(this.cleanupTimer);
|
|
420
844
|
this.cleanupTimer = null;
|
|
421
845
|
}
|
|
846
|
+
// Shutdown MCP managers for all sessions
|
|
847
|
+
for (const session of this.sessions.values()) {
|
|
848
|
+
if (session.mcpManager) {
|
|
849
|
+
await session.mcpManager.shutdown().catch(() => { });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// Shutdown persistent inspect MCP manager
|
|
853
|
+
if (this.inspectMcp) {
|
|
854
|
+
await this.inspectMcp.shutdown().catch(() => { });
|
|
855
|
+
this.inspectMcp = undefined;
|
|
856
|
+
this.inspectMcpInitialized = false;
|
|
857
|
+
}
|
|
422
858
|
const ids = [...this.sessions.keys()];
|
|
423
859
|
for (const id of ids) {
|
|
424
860
|
await this.destroy(id);
|