@provos/ironcurtain 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 +202 -0
- package/README.md +311 -0
- package/dist/agent/index.d.ts +10 -0
- package/dist/agent/index.js +71 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/prompts.d.ts +5 -0
- package/dist/agent/prompts.js +26 -0
- package/dist/agent/prompts.js.map +1 -0
- package/dist/agent/tools.d.ts +13 -0
- package/dist/agent/tools.js +51 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +78 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/constitution.md +16 -0
- package/dist/config/generated/compiled-policy.json +236 -0
- package/dist/config/generated/test-scenarios.json +765 -0
- package/dist/config/generated/tool-annotations.json +955 -0
- package/dist/config/index.d.ts +25 -0
- package/dist/config/index.js +151 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mcp-servers.json +22 -0
- package/dist/config/model-provider.d.ts +49 -0
- package/dist/config/model-provider.js +78 -0
- package/dist/config/model-provider.js.map +1 -0
- package/dist/config/paths.d.ts +59 -0
- package/dist/config/paths.js +96 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/types.d.ts +89 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/user-config.d.ts +93 -0
- package/dist/config/user-config.js +309 -0
- package/dist/config/user-config.js.map +1 -0
- package/dist/hash.d.ts +17 -0
- package/dist/hash.js +34 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +93 -0
- package/dist/logger.js.map +1 -0
- package/dist/pipeline/annotate.d.ts +9 -0
- package/dist/pipeline/annotate.js +136 -0
- package/dist/pipeline/annotate.js.map +1 -0
- package/dist/pipeline/compile.d.ts +23 -0
- package/dist/pipeline/compile.js +386 -0
- package/dist/pipeline/compile.js.map +1 -0
- package/dist/pipeline/constitution-compiler.d.ts +22 -0
- package/dist/pipeline/constitution-compiler.js +197 -0
- package/dist/pipeline/constitution-compiler.js.map +1 -0
- package/dist/pipeline/generate-with-repair.d.ts +22 -0
- package/dist/pipeline/generate-with-repair.js +64 -0
- package/dist/pipeline/generate-with-repair.js.map +1 -0
- package/dist/pipeline/handwritten-scenarios.d.ts +9 -0
- package/dist/pipeline/handwritten-scenarios.js +321 -0
- package/dist/pipeline/handwritten-scenarios.js.map +1 -0
- package/dist/pipeline/llm-logger.d.ts +42 -0
- package/dist/pipeline/llm-logger.js +78 -0
- package/dist/pipeline/llm-logger.js.map +1 -0
- package/dist/pipeline/pipeline-shared.d.ts +47 -0
- package/dist/pipeline/pipeline-shared.js +145 -0
- package/dist/pipeline/pipeline-shared.js.map +1 -0
- package/dist/pipeline/policy-verifier.d.ts +46 -0
- package/dist/pipeline/policy-verifier.js +277 -0
- package/dist/pipeline/policy-verifier.js.map +1 -0
- package/dist/pipeline/scenario-generator.d.ts +11 -0
- package/dist/pipeline/scenario-generator.js +128 -0
- package/dist/pipeline/scenario-generator.js.map +1 -0
- package/dist/pipeline/tool-annotator.d.ts +24 -0
- package/dist/pipeline/tool-annotator.js +201 -0
- package/dist/pipeline/tool-annotator.js.map +1 -0
- package/dist/pipeline/types.d.ts +122 -0
- package/dist/pipeline/types.js +10 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/sandbox/index.d.ts +39 -0
- package/dist/sandbox/index.js +178 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/session/agent-session.d.ts +83 -0
- package/dist/session/agent-session.js +382 -0
- package/dist/session/agent-session.js.map +1 -0
- package/dist/session/cli-transport.d.ts +61 -0
- package/dist/session/cli-transport.js +320 -0
- package/dist/session/cli-transport.js.map +1 -0
- package/dist/session/errors.d.ts +19 -0
- package/dist/session/errors.js +33 -0
- package/dist/session/errors.js.map +1 -0
- package/dist/session/index.d.ts +29 -0
- package/dist/session/index.js +104 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/message-compactor.d.ts +32 -0
- package/dist/session/message-compactor.js +81 -0
- package/dist/session/message-compactor.js.map +1 -0
- package/dist/session/prompts.d.ts +5 -0
- package/dist/session/prompts.js +62 -0
- package/dist/session/prompts.js.map +1 -0
- package/dist/session/resource-budget-tracker.d.ts +124 -0
- package/dist/session/resource-budget-tracker.js +327 -0
- package/dist/session/resource-budget-tracker.js.map +1 -0
- package/dist/session/step-loop-detector.d.ts +63 -0
- package/dist/session/step-loop-detector.js +136 -0
- package/dist/session/step-loop-detector.js.map +1 -0
- package/dist/session/transport.d.ts +24 -0
- package/dist/session/transport.js +2 -0
- package/dist/session/transport.js.map +1 -0
- package/dist/session/truncate-result.d.ts +35 -0
- package/dist/session/truncate-result.js +71 -0
- package/dist/session/truncate-result.js.map +1 -0
- package/dist/session/types.d.ts +220 -0
- package/dist/session/types.js +6 -0
- package/dist/session/types.js.map +1 -0
- package/dist/trusted-process/audit-log.d.ts +7 -0
- package/dist/trusted-process/audit-log.js +21 -0
- package/dist/trusted-process/audit-log.js.map +1 -0
- package/dist/trusted-process/call-circuit-breaker.d.ts +33 -0
- package/dist/trusted-process/call-circuit-breaker.js +61 -0
- package/dist/trusted-process/call-circuit-breaker.js.map +1 -0
- package/dist/trusted-process/escalation.d.ts +7 -0
- package/dist/trusted-process/escalation.js +38 -0
- package/dist/trusted-process/escalation.js.map +1 -0
- package/dist/trusted-process/index.d.ts +32 -0
- package/dist/trusted-process/index.js +151 -0
- package/dist/trusted-process/index.js.map +1 -0
- package/dist/trusted-process/mcp-client-manager.d.ts +25 -0
- package/dist/trusted-process/mcp-client-manager.js +90 -0
- package/dist/trusted-process/mcp-client-manager.js.map +1 -0
- package/dist/trusted-process/mcp-proxy-server.d.ts +24 -0
- package/dist/trusted-process/mcp-proxy-server.js +451 -0
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -0
- package/dist/trusted-process/path-utils.d.ts +50 -0
- package/dist/trusted-process/path-utils.js +158 -0
- package/dist/trusted-process/path-utils.js.map +1 -0
- package/dist/trusted-process/policy-engine.d.ts +88 -0
- package/dist/trusted-process/policy-engine.js +523 -0
- package/dist/trusted-process/policy-engine.js.map +1 -0
- package/dist/trusted-process/policy-roots.d.ts +50 -0
- package/dist/trusted-process/policy-roots.js +67 -0
- package/dist/trusted-process/policy-roots.js.map +1 -0
- package/dist/trusted-process/policy-types.d.ts +6 -0
- package/dist/trusted-process/policy-types.js +2 -0
- package/dist/trusted-process/policy-types.js.map +1 -0
- package/dist/trusted-process/sandbox-integration.d.ts +92 -0
- package/dist/trusted-process/sandbox-integration.js +184 -0
- package/dist/trusted-process/sandbox-integration.js.map +1 -0
- package/dist/types/argument-roles.d.ts +112 -0
- package/dist/types/argument-roles.js +344 -0
- package/dist/types/argument-roles.js.map +1 -0
- package/dist/types/audit.d.ts +18 -0
- package/dist/types/audit.js +2 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/mcp.d.ts +20 -0
- package/dist/types/mcp.js +2 -0
- package/dist/types/mcp.js.map +1 -0
- package/package.json +83 -0
- package/src/config/constitution.md +16 -0
- package/src/config/generated/compiled-policy.json +236 -0
- package/src/config/generated/test-scenarios.json +765 -0
- package/src/config/generated/tool-annotations.json +955 -0
- package/src/config/mcp-servers.json +22 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox -- Sets up UTCP Code Mode with the MCP proxy server.
|
|
3
|
+
*
|
|
4
|
+
* Code Mode creates a sandboxed TypeScript execution environment where
|
|
5
|
+
* LLM-generated code runs with no direct access to the network, filesystem,
|
|
6
|
+
* or credentials. Instead of connecting to real MCP servers, it connects
|
|
7
|
+
* to our MCP proxy server which evaluates policy on every tool call.
|
|
8
|
+
*/
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import '@utcp/mcp'; // Register MCP call template type with UTCP SDK
|
|
12
|
+
import { CodeModeUtcpClient } from '@utcp/code-mode';
|
|
13
|
+
// Detect compiled (.js in dist/) vs source (.ts in src/) mode.
|
|
14
|
+
// In compiled mode, spawn with `node`; in source mode, spawn with `npx tsx`.
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const isCompiled = __filename.endsWith('.js');
|
|
18
|
+
const proxyServerPath = resolve(__dirname, `../trusted-process/mcp-proxy-server.${isCompiled ? 'js' : 'ts'}`);
|
|
19
|
+
const PROXY_COMMAND = isCompiled ? 'node' : 'npx';
|
|
20
|
+
const PROXY_ARGS = isCompiled ? [proxyServerPath] : ['tsx', proxyServerPath];
|
|
21
|
+
/**
|
|
22
|
+
* Transforms a UTCP tool name (dotted) into the actual callable function name
|
|
23
|
+
* in the sandbox. Mirrors UTCP Code Mode's sanitizeIdentifier() behavior:
|
|
24
|
+
* split on first dot (manual name), join remaining parts with underscores.
|
|
25
|
+
*
|
|
26
|
+
* Example: "filesystem.filesystem.list_directory" → "filesystem.filesystem_list_directory"
|
|
27
|
+
*/
|
|
28
|
+
export function toCallableName(toolName) {
|
|
29
|
+
const sanitize = (s) => s.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^[0-9]/, '_$&');
|
|
30
|
+
if (!toolName.includes('.'))
|
|
31
|
+
return sanitize(toolName);
|
|
32
|
+
const [manual, ...parts] = toolName.split('.');
|
|
33
|
+
return `${sanitize(manual)}.${parts.map(sanitize).join('_')}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extracts required parameter names from a tool's JSON Schema inputs
|
|
37
|
+
* to show inline in the catalog, e.g. "{ path }" or "{ path, content }".
|
|
38
|
+
*/
|
|
39
|
+
function extractRequiredParams(inputs) {
|
|
40
|
+
if (!inputs?.required?.length)
|
|
41
|
+
return '';
|
|
42
|
+
return `{ ${inputs.required.join(', ')} }`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Builds a JavaScript snippet that patches __getToolInterface inside the
|
|
46
|
+
* sandbox isolate so that callable names (with underscores) resolve to
|
|
47
|
+
* the same interface as the raw UTCP tool names (with dots).
|
|
48
|
+
*
|
|
49
|
+
* UTCP Code Mode keys its interface map by raw tool name (e.g.,
|
|
50
|
+
* "tools.git.git_add") but the callable function name the agent sees
|
|
51
|
+
* uses underscores (e.g., "tools.git_git_add"). Without this patch,
|
|
52
|
+
* __getToolInterface(callableName) returns null.
|
|
53
|
+
*/
|
|
54
|
+
function buildInterfacePatchSnippet(callableToRawMap) {
|
|
55
|
+
const mapJson = JSON.stringify(callableToRawMap);
|
|
56
|
+
// The snippet runs inside the V8 isolate before user code.
|
|
57
|
+
// It wraps the existing __getToolInterface to also accept callable names.
|
|
58
|
+
return `
|
|
59
|
+
(function() {
|
|
60
|
+
var _callableToRaw = ${mapJson};
|
|
61
|
+
var _origGetToolInterface = global.__getToolInterface;
|
|
62
|
+
if (_origGetToolInterface) {
|
|
63
|
+
global.__getToolInterface = function(toolName) {
|
|
64
|
+
var result = _origGetToolInterface(toolName);
|
|
65
|
+
if (result) return result;
|
|
66
|
+
var rawName = _callableToRaw[toolName];
|
|
67
|
+
if (rawName) return _origGetToolInterface(rawName);
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
export class Sandbox {
|
|
75
|
+
client = null;
|
|
76
|
+
toolCatalog = '';
|
|
77
|
+
interfacePatchSnippet = '';
|
|
78
|
+
async initialize(config) {
|
|
79
|
+
this.client = await CodeModeUtcpClient.create();
|
|
80
|
+
// Register the MCP proxy server instead of real MCP servers.
|
|
81
|
+
// The proxy is the trusted process boundary -- it evaluates policy
|
|
82
|
+
// on every tool call before forwarding to the real MCP server.
|
|
83
|
+
const proxyEnv = {
|
|
84
|
+
AUDIT_LOG_PATH: config.auditLogPath,
|
|
85
|
+
MCP_SERVERS_CONFIG: JSON.stringify(config.mcpServers),
|
|
86
|
+
GENERATED_DIR: config.generatedDir,
|
|
87
|
+
PROTECTED_PATHS: JSON.stringify(config.protectedPaths),
|
|
88
|
+
ALLOWED_DIRECTORY: config.allowedDirectory,
|
|
89
|
+
};
|
|
90
|
+
// Pass the escalation directory to the proxy when configured.
|
|
91
|
+
// When set, the proxy uses file-based IPC for escalation instead of auto-deny.
|
|
92
|
+
if (config.escalationDir) {
|
|
93
|
+
proxyEnv.ESCALATION_DIR = config.escalationDir;
|
|
94
|
+
}
|
|
95
|
+
// Pass the session log path so the proxy can capture child process stderr.
|
|
96
|
+
if (config.sessionLogPath) {
|
|
97
|
+
proxyEnv.SESSION_LOG_PATH = config.sessionLogPath;
|
|
98
|
+
}
|
|
99
|
+
// Pass escalation timeout to the proxy process and use it for the sandbox timeout.
|
|
100
|
+
// The sandbox timeout must accommodate human escalation approval time.
|
|
101
|
+
proxyEnv.ESCALATION_TIMEOUT_SECONDS = String(config.escalationTimeoutSeconds);
|
|
102
|
+
const timeoutMs = config.escalationTimeoutSeconds * 1000;
|
|
103
|
+
// Pass sandbox availability policy to the proxy process
|
|
104
|
+
proxyEnv.SANDBOX_POLICY = config.sandboxPolicy ?? 'warn';
|
|
105
|
+
// Register one proxy per backend server so UTCP names them cleanly:
|
|
106
|
+
// tools.<serverName>_<toolName>(...)
|
|
107
|
+
// Each proxy gets a SERVER_FILTER env var to connect only to its server.
|
|
108
|
+
const mcpServers = {};
|
|
109
|
+
for (const serverName of Object.keys(config.mcpServers)) {
|
|
110
|
+
mcpServers[serverName] = {
|
|
111
|
+
transport: 'stdio',
|
|
112
|
+
command: PROXY_COMMAND,
|
|
113
|
+
args: [...PROXY_ARGS],
|
|
114
|
+
env: { ...proxyEnv, SERVER_FILTER: serverName },
|
|
115
|
+
timeout: timeoutMs,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
await this.client.registerManual({
|
|
119
|
+
name: 'tools',
|
|
120
|
+
call_template_type: 'mcp',
|
|
121
|
+
config: { mcpServers },
|
|
122
|
+
});
|
|
123
|
+
const { catalog, patchSnippet } = await this.buildToolCatalogAndPatch();
|
|
124
|
+
this.toolCatalog = catalog;
|
|
125
|
+
this.interfacePatchSnippet = patchSnippet;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Builds the tool catalog and the __getToolInterface patch in one pass.
|
|
129
|
+
*
|
|
130
|
+
* The catalog is a compact one-line-per-tool listing using callable names.
|
|
131
|
+
* The patch snippet maps callable names back to raw UTCP names so that
|
|
132
|
+
* __getToolInterface works with either naming convention.
|
|
133
|
+
*/
|
|
134
|
+
async buildToolCatalogAndPatch() {
|
|
135
|
+
if (!this.client)
|
|
136
|
+
throw new Error('Sandbox not initialized');
|
|
137
|
+
const tools = await this.client.getTools();
|
|
138
|
+
if (tools.length === 0)
|
|
139
|
+
return { catalog: 'No tools available', patchSnippet: '' };
|
|
140
|
+
const callableToRaw = {};
|
|
141
|
+
const catalogLines = [];
|
|
142
|
+
for (const t of tools) {
|
|
143
|
+
const callableName = toCallableName(t.name);
|
|
144
|
+
const params = extractRequiredParams(t.inputs);
|
|
145
|
+
catalogLines.push(`- \`${callableName}(${params})\` — ${t.description}`);
|
|
146
|
+
// Only add mapping when the names actually differ
|
|
147
|
+
if (callableName !== t.name) {
|
|
148
|
+
callableToRaw[callableName] = t.name;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const patchSnippet = Object.keys(callableToRaw).length > 0
|
|
152
|
+
? buildInterfacePatchSnippet(callableToRaw)
|
|
153
|
+
: '';
|
|
154
|
+
return { catalog: catalogLines.join('\n'), patchSnippet };
|
|
155
|
+
}
|
|
156
|
+
getToolInterfaces() {
|
|
157
|
+
return this.toolCatalog;
|
|
158
|
+
}
|
|
159
|
+
async executeCode(code, timeoutMs = 300000) {
|
|
160
|
+
if (!this.client)
|
|
161
|
+
throw new Error('Sandbox not initialized');
|
|
162
|
+
// Prepend the interface patch so __getToolInterface accepts callable names.
|
|
163
|
+
// Each callToolChain invocation creates a fresh V8 isolate, so the patch
|
|
164
|
+
// must be re-applied every time.
|
|
165
|
+
const patchedCode = this.interfacePatchSnippet
|
|
166
|
+
? `${this.interfacePatchSnippet}\n${code}`
|
|
167
|
+
: code;
|
|
168
|
+
const { result, logs } = await this.client.callToolChain(patchedCode, timeoutMs);
|
|
169
|
+
return { result, logs: logs ?? [] };
|
|
170
|
+
}
|
|
171
|
+
async shutdown() {
|
|
172
|
+
if (this.client) {
|
|
173
|
+
await this.client.close();
|
|
174
|
+
this.client = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sandbox/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentSession -- concrete Session implementation.
|
|
3
|
+
*
|
|
4
|
+
* Not exported from the public API. Callers use the Session interface
|
|
5
|
+
* via createSession() in index.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { IronCurtainConfig } from '../config/types.js';
|
|
8
|
+
import type { Session, SessionId, SessionInfo, SessionOptions, ConversationTurn, DiagnosticEvent, EscalationRequest, BudgetStatus } from './types.js';
|
|
9
|
+
export declare class AgentSession implements Session {
|
|
10
|
+
private readonly sessionId;
|
|
11
|
+
private status;
|
|
12
|
+
private readonly config;
|
|
13
|
+
private sandbox;
|
|
14
|
+
private readonly createdAt;
|
|
15
|
+
private readonly sandboxFactory;
|
|
16
|
+
private readonly escalationDir;
|
|
17
|
+
/** Raw AI SDK message history. */
|
|
18
|
+
private messages;
|
|
19
|
+
/** Structured turn log exposed through getHistory(). */
|
|
20
|
+
private turns;
|
|
21
|
+
/** Accumulated diagnostic events exposed through getDiagnosticLog(). */
|
|
22
|
+
private diagnosticLog;
|
|
23
|
+
/** System prompt, built once after sandbox initialization. */
|
|
24
|
+
private systemPrompt;
|
|
25
|
+
/** The tool set, built once after sandbox initialization. */
|
|
26
|
+
private tools;
|
|
27
|
+
/** Language model, optionally wrapped with logging middleware. */
|
|
28
|
+
private model;
|
|
29
|
+
/** Currently pending escalation, if any. */
|
|
30
|
+
private pendingEscalation;
|
|
31
|
+
/** Interval handle for polling the escalation directory. */
|
|
32
|
+
private escalationPollInterval;
|
|
33
|
+
/** Escalation IDs already detected, to prevent re-detection after resolution. */
|
|
34
|
+
private seenEscalationIds;
|
|
35
|
+
/** Step-level loop detector for the agent. */
|
|
36
|
+
private loopDetector;
|
|
37
|
+
/** Resource budget tracker for token, step, time, and cost limits. */
|
|
38
|
+
private readonly budgetTracker;
|
|
39
|
+
/** Message compactor for auto-summarizing older messages. */
|
|
40
|
+
private readonly compactor;
|
|
41
|
+
/** Last step's input token count, used by the compactor to detect threshold. */
|
|
42
|
+
private lastStepInputTokens;
|
|
43
|
+
/** Callbacks from SessionOptions. */
|
|
44
|
+
private readonly onEscalation?;
|
|
45
|
+
private readonly onDiagnostic?;
|
|
46
|
+
constructor(config: IronCurtainConfig, sessionId: SessionId, escalationDir: string, options?: SessionOptions);
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the session's sandbox and build the tool set.
|
|
49
|
+
* Called by the factory function, not by external callers.
|
|
50
|
+
*/
|
|
51
|
+
initialize(): Promise<void>;
|
|
52
|
+
getInfo(): SessionInfo;
|
|
53
|
+
sendMessage(userMessage: string): Promise<string>;
|
|
54
|
+
getHistory(): readonly ConversationTurn[];
|
|
55
|
+
getDiagnosticLog(): readonly DiagnosticEvent[];
|
|
56
|
+
getPendingEscalation(): EscalationRequest | undefined;
|
|
57
|
+
getBudgetStatus(): BudgetStatus;
|
|
58
|
+
resolveEscalation(escalationId: string, decision: 'approved' | 'denied'): Promise<void>;
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
private buildTools;
|
|
61
|
+
private buildModel;
|
|
62
|
+
/** Logs a diagnostic event and forwards it to the transport callback. */
|
|
63
|
+
private emitDiagnostic;
|
|
64
|
+
private emitToolCallDiagnostics;
|
|
65
|
+
/** Analyze a step for loop detection and attach a warning if needed. */
|
|
66
|
+
private applyLoopVerdict;
|
|
67
|
+
private emitTruncationDiagnostic;
|
|
68
|
+
private emitLoopDetectionDiagnostic;
|
|
69
|
+
private emitBudgetWarnings;
|
|
70
|
+
private emitBudgetExhaustedDiagnostic;
|
|
71
|
+
private emitTextDiagnostic;
|
|
72
|
+
private recordTurn;
|
|
73
|
+
/**
|
|
74
|
+
* Starts polling the escalation directory for new request files.
|
|
75
|
+
* When a request is detected, sets pendingEscalation and fires
|
|
76
|
+
* the onEscalation callback.
|
|
77
|
+
*/
|
|
78
|
+
private startEscalationWatcher;
|
|
79
|
+
private pollEscalationDirectory;
|
|
80
|
+
/** Extracts the escalation ID from a request filename like "request-abc123.json". */
|
|
81
|
+
private extractEscalationId;
|
|
82
|
+
private stopEscalationWatcher;
|
|
83
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentSession -- concrete Session implementation.
|
|
3
|
+
*
|
|
4
|
+
* Not exported from the public API. Callers use the Session interface
|
|
5
|
+
* via createSession() in index.ts.
|
|
6
|
+
*/
|
|
7
|
+
import { generateText, stepCountIs, tool, wrapLanguageModel, } from 'ai';
|
|
8
|
+
import { createLanguageModel } from '../config/model-provider.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { createLlmLoggingMiddleware } from '../pipeline/llm-logger.js';
|
|
12
|
+
import { resolve } from 'node:path';
|
|
13
|
+
import { buildSystemPrompt } from './prompts.js';
|
|
14
|
+
import { SessionNotReadyError, SessionClosedError, BudgetExhaustedError } from './errors.js';
|
|
15
|
+
import { StepLoopDetector } from './step-loop-detector.js';
|
|
16
|
+
import { ResourceBudgetTracker } from './resource-budget-tracker.js';
|
|
17
|
+
import { MessageCompactor } from './message-compactor.js';
|
|
18
|
+
import { truncateResult, getResultSizeLimit, formatKB } from './truncate-result.js';
|
|
19
|
+
import * as logger from '../logger.js';
|
|
20
|
+
const MAX_AGENT_STEPS = 100;
|
|
21
|
+
const ESCALATION_POLL_INTERVAL_MS = 300;
|
|
22
|
+
/** Default sandbox factory: creates a real UTCP Code Mode sandbox. */
|
|
23
|
+
async function defaultSandboxFactory(config) {
|
|
24
|
+
const { Sandbox: SandboxClass } = await import('../sandbox/index.js');
|
|
25
|
+
const instance = new SandboxClass();
|
|
26
|
+
await instance.initialize(config);
|
|
27
|
+
return instance;
|
|
28
|
+
}
|
|
29
|
+
export class AgentSession {
|
|
30
|
+
sessionId;
|
|
31
|
+
status = 'initializing';
|
|
32
|
+
config;
|
|
33
|
+
sandbox = null;
|
|
34
|
+
createdAt;
|
|
35
|
+
sandboxFactory;
|
|
36
|
+
escalationDir;
|
|
37
|
+
/** Raw AI SDK message history. */
|
|
38
|
+
messages = [];
|
|
39
|
+
/** Structured turn log exposed through getHistory(). */
|
|
40
|
+
turns = [];
|
|
41
|
+
/** Accumulated diagnostic events exposed through getDiagnosticLog(). */
|
|
42
|
+
diagnosticLog = [];
|
|
43
|
+
/** System prompt, built once after sandbox initialization. */
|
|
44
|
+
systemPrompt = '';
|
|
45
|
+
/** The tool set, built once after sandbox initialization. */
|
|
46
|
+
tools = {};
|
|
47
|
+
/** Language model, optionally wrapped with logging middleware. */
|
|
48
|
+
model = null;
|
|
49
|
+
/** Currently pending escalation, if any. */
|
|
50
|
+
pendingEscalation;
|
|
51
|
+
/** Interval handle for polling the escalation directory. */
|
|
52
|
+
escalationPollInterval = null;
|
|
53
|
+
/** Escalation IDs already detected, to prevent re-detection after resolution. */
|
|
54
|
+
seenEscalationIds = new Set();
|
|
55
|
+
/** Step-level loop detector for the agent. */
|
|
56
|
+
loopDetector = new StepLoopDetector();
|
|
57
|
+
/** Resource budget tracker for token, step, time, and cost limits. */
|
|
58
|
+
budgetTracker;
|
|
59
|
+
/** Message compactor for auto-summarizing older messages. */
|
|
60
|
+
compactor;
|
|
61
|
+
/** Last step's input token count, used by the compactor to detect threshold. */
|
|
62
|
+
lastStepInputTokens = 0;
|
|
63
|
+
/** Callbacks from SessionOptions. */
|
|
64
|
+
onEscalation;
|
|
65
|
+
onDiagnostic;
|
|
66
|
+
constructor(config, sessionId, escalationDir, options = {}) {
|
|
67
|
+
this.sessionId = sessionId;
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.escalationDir = escalationDir;
|
|
70
|
+
this.sandboxFactory = options.sandboxFactory ?? defaultSandboxFactory;
|
|
71
|
+
this.onEscalation = options.onEscalation;
|
|
72
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
73
|
+
this.createdAt = new Date().toISOString();
|
|
74
|
+
this.budgetTracker = new ResourceBudgetTracker(config.userConfig.resourceBudget, config.agentModelId);
|
|
75
|
+
this.compactor = new MessageCompactor(config.userConfig.autoCompact);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the session's sandbox and build the tool set.
|
|
79
|
+
* Called by the factory function, not by external callers.
|
|
80
|
+
*/
|
|
81
|
+
async initialize() {
|
|
82
|
+
this.sandbox = await this.sandboxFactory(this.config);
|
|
83
|
+
this.systemPrompt = buildSystemPrompt(this.sandbox.getToolInterfaces(), this.config.allowedDirectory);
|
|
84
|
+
this.tools = this.buildTools();
|
|
85
|
+
this.model = await this.buildModel();
|
|
86
|
+
this.startEscalationWatcher();
|
|
87
|
+
this.status = 'ready';
|
|
88
|
+
}
|
|
89
|
+
getInfo() {
|
|
90
|
+
return {
|
|
91
|
+
id: this.sessionId,
|
|
92
|
+
status: this.status,
|
|
93
|
+
turnCount: this.turns.length,
|
|
94
|
+
createdAt: this.createdAt,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async sendMessage(userMessage) {
|
|
98
|
+
if (this.status === 'closed')
|
|
99
|
+
throw new SessionClosedError();
|
|
100
|
+
if (this.status !== 'ready')
|
|
101
|
+
throw new SessionNotReadyError(this.status);
|
|
102
|
+
this.budgetTracker.startTurn();
|
|
103
|
+
this.status = 'processing';
|
|
104
|
+
const turnStart = new Date().toISOString();
|
|
105
|
+
const messageCountBefore = this.messages.length;
|
|
106
|
+
// Wall-clock abort signal (null when wall-clock budget is disabled)
|
|
107
|
+
const remainingMs = this.budgetTracker.getRemainingWallClockMs();
|
|
108
|
+
const abortController = remainingMs !== null ? new AbortController() : null;
|
|
109
|
+
const abortTimeout = abortController
|
|
110
|
+
? setTimeout(() => abortController.abort(), remainingMs)
|
|
111
|
+
: null;
|
|
112
|
+
try {
|
|
113
|
+
this.messages.push({ role: 'user', content: userMessage });
|
|
114
|
+
// Auto-compact older messages if the previous turn exceeded the threshold
|
|
115
|
+
if (this.compactor.shouldCompact()) {
|
|
116
|
+
const compactionResult = await this.compactor.compact(this.messages, this.config.userConfig);
|
|
117
|
+
if (compactionResult) {
|
|
118
|
+
this.emitDiagnostic({
|
|
119
|
+
kind: 'message_compaction',
|
|
120
|
+
originalMessageCount: compactionResult.originalMessageCount,
|
|
121
|
+
newMessageCount: compactionResult.newMessageCount,
|
|
122
|
+
summaryPreview: compactionResult.summaryPreview,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const result = await generateText({
|
|
127
|
+
model: this.model,
|
|
128
|
+
system: this.systemPrompt,
|
|
129
|
+
messages: this.messages,
|
|
130
|
+
tools: this.tools,
|
|
131
|
+
stopWhen: [
|
|
132
|
+
stepCountIs(MAX_AGENT_STEPS),
|
|
133
|
+
this.budgetTracker.createStopCondition(),
|
|
134
|
+
],
|
|
135
|
+
...(abortController ? { abortSignal: abortController.signal } : {}),
|
|
136
|
+
onStepFinish: (stepResult) => {
|
|
137
|
+
this.lastStepInputTokens = stepResult.usage?.inputTokens ?? 0;
|
|
138
|
+
this.emitToolCallDiagnostics(stepResult.toolCalls);
|
|
139
|
+
this.emitTextDiagnostic(stepResult.text);
|
|
140
|
+
this.emitBudgetWarnings();
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
// Check if budget triggered the stop
|
|
144
|
+
const postCheck = this.budgetTracker.isExhausted();
|
|
145
|
+
if (postCheck) {
|
|
146
|
+
this.emitBudgetExhaustedDiagnostic(postCheck.dimension, postCheck.message);
|
|
147
|
+
}
|
|
148
|
+
this.messages.push(...result.response.messages);
|
|
149
|
+
// Record input tokens for the compactor's next-turn threshold check
|
|
150
|
+
this.compactor.recordInputTokens(this.lastStepInputTokens);
|
|
151
|
+
const turn = this.recordTurn(userMessage, result.text, result.totalUsage, turnStart);
|
|
152
|
+
this.turns.push(turn);
|
|
153
|
+
this.status = 'ready';
|
|
154
|
+
return result.text;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
// Truncate back to the state before this turn, removing the user
|
|
158
|
+
// message and any partial response messages that may have been pushed.
|
|
159
|
+
this.messages.length = messageCountBefore;
|
|
160
|
+
this.status = 'ready';
|
|
161
|
+
// Detect AbortError from wall-clock timeout
|
|
162
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
163
|
+
const exhausted = this.budgetTracker.isExhausted();
|
|
164
|
+
const dimension = exhausted?.dimension ?? 'wall_clock';
|
|
165
|
+
const message = exhausted?.message ?? 'Session aborted: wall-clock time budget exceeded';
|
|
166
|
+
this.emitBudgetExhaustedDiagnostic(dimension, message);
|
|
167
|
+
throw new BudgetExhaustedError(dimension, message);
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (abortTimeout !== null)
|
|
173
|
+
clearTimeout(abortTimeout);
|
|
174
|
+
this.budgetTracker.endTurn();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getHistory() {
|
|
178
|
+
return this.turns;
|
|
179
|
+
}
|
|
180
|
+
getDiagnosticLog() {
|
|
181
|
+
return this.diagnosticLog;
|
|
182
|
+
}
|
|
183
|
+
getPendingEscalation() {
|
|
184
|
+
return this.pendingEscalation;
|
|
185
|
+
}
|
|
186
|
+
getBudgetStatus() {
|
|
187
|
+
return {
|
|
188
|
+
...this.budgetTracker.getSnapshot(),
|
|
189
|
+
limits: this.config.userConfig.resourceBudget,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async resolveEscalation(escalationId, decision) {
|
|
193
|
+
if (!this.pendingEscalation || this.pendingEscalation.escalationId !== escalationId) {
|
|
194
|
+
throw new Error(`No pending escalation with ID: ${escalationId}`);
|
|
195
|
+
}
|
|
196
|
+
const responsePath = resolve(this.escalationDir, `response-${escalationId}.json`);
|
|
197
|
+
writeFileSync(responsePath, JSON.stringify({ decision }));
|
|
198
|
+
this.pendingEscalation = undefined;
|
|
199
|
+
}
|
|
200
|
+
async close() {
|
|
201
|
+
if (this.status === 'closed')
|
|
202
|
+
return;
|
|
203
|
+
this.status = 'closed';
|
|
204
|
+
this.stopEscalationWatcher();
|
|
205
|
+
if (this.sandbox) {
|
|
206
|
+
await this.sandbox.shutdown();
|
|
207
|
+
this.sandbox = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// --- Private helpers ---
|
|
211
|
+
buildTools() {
|
|
212
|
+
return {
|
|
213
|
+
execute_code: tool({
|
|
214
|
+
description: 'Execute TypeScript code in a secure sandbox with access to filesystem tools. ' +
|
|
215
|
+
'Write code that calls tool functions like filesystem.filesystem_read_file({ path }), ' +
|
|
216
|
+
'filesystem.filesystem_list_directory({ path }), etc. ' +
|
|
217
|
+
'Tools are synchronous — no await needed. Use return to provide results. ' +
|
|
218
|
+
'Call __getToolInterface(\'tool.name\') to discover the full type signature of any tool.',
|
|
219
|
+
inputSchema: z.object({
|
|
220
|
+
code: z
|
|
221
|
+
.string()
|
|
222
|
+
.describe('TypeScript code to execute in the sandbox'),
|
|
223
|
+
}),
|
|
224
|
+
execute: async ({ code }) => {
|
|
225
|
+
if (!this.sandbox)
|
|
226
|
+
throw new Error('Sandbox not initialized');
|
|
227
|
+
const budgetBlock = this.budgetTracker.isExhausted();
|
|
228
|
+
if (budgetBlock) {
|
|
229
|
+
return { error: budgetBlock.message };
|
|
230
|
+
}
|
|
231
|
+
const blockCheck = this.loopDetector.isBlocked();
|
|
232
|
+
if (blockCheck) {
|
|
233
|
+
return { error: blockCheck.message };
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const { result, logs } = await this.sandbox.executeCode(code);
|
|
237
|
+
const output = {};
|
|
238
|
+
if (logs.length > 0)
|
|
239
|
+
output.console = logs;
|
|
240
|
+
const truncation = truncateResult(result, getResultSizeLimit());
|
|
241
|
+
output.result = truncation.value;
|
|
242
|
+
if (truncation.truncated) {
|
|
243
|
+
output.warning = `Tool result truncated from ${formatKB(truncation.originalSize)} to ${formatKB(truncation.finalSize)}. Use targeted reads (head/tail parameters) for specific portions.`;
|
|
244
|
+
logger.warn(`[truncation] Result truncated: ${formatKB(truncation.originalSize)} -> ${formatKB(truncation.finalSize)}`);
|
|
245
|
+
this.emitTruncationDiagnostic(truncation.originalSize, truncation.finalSize);
|
|
246
|
+
}
|
|
247
|
+
return this.applyLoopVerdict(code, output);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const output = {
|
|
251
|
+
error: err instanceof Error ? err.message : String(err),
|
|
252
|
+
};
|
|
253
|
+
return this.applyLoopVerdict(code, output);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async buildModel() {
|
|
260
|
+
const baseModel = await createLanguageModel(this.config.agentModelId, this.config.userConfig);
|
|
261
|
+
if (!this.config.llmLogPath)
|
|
262
|
+
return baseModel;
|
|
263
|
+
const logContext = { stepName: 'agent' };
|
|
264
|
+
return wrapLanguageModel({
|
|
265
|
+
model: baseModel,
|
|
266
|
+
middleware: createLlmLoggingMiddleware(this.config.llmLogPath, logContext),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** Logs a diagnostic event and forwards it to the transport callback. */
|
|
270
|
+
emitDiagnostic(event) {
|
|
271
|
+
this.diagnosticLog.push(event);
|
|
272
|
+
this.onDiagnostic?.(event);
|
|
273
|
+
}
|
|
274
|
+
emitToolCallDiagnostics(toolCalls) {
|
|
275
|
+
for (const tc of toolCalls) {
|
|
276
|
+
if (tc.toolName === 'execute_code' && tc.input != null) {
|
|
277
|
+
const input = tc.input;
|
|
278
|
+
const preview = input.code.substring(0, 120).replace(/\n/g, '\\n');
|
|
279
|
+
this.emitDiagnostic({
|
|
280
|
+
kind: 'tool_call',
|
|
281
|
+
toolName: tc.toolName,
|
|
282
|
+
preview: `${preview}${input.code.length > 120 ? '...' : ''}`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Analyze a step for loop detection and attach a warning if needed. */
|
|
288
|
+
applyLoopVerdict(code, output) {
|
|
289
|
+
const verdict = this.loopDetector.analyzeStep(code, output);
|
|
290
|
+
if (verdict.action === 'warn' || verdict.action === 'block') {
|
|
291
|
+
output.warning = output.warning
|
|
292
|
+
? `${output.warning} | ${verdict.message}`
|
|
293
|
+
: verdict.message;
|
|
294
|
+
this.emitLoopDetectionDiagnostic(verdict.action, verdict.category, verdict.message);
|
|
295
|
+
}
|
|
296
|
+
return output;
|
|
297
|
+
}
|
|
298
|
+
emitTruncationDiagnostic(originalSize, finalSize) {
|
|
299
|
+
this.emitDiagnostic({
|
|
300
|
+
kind: 'result_truncation',
|
|
301
|
+
originalKB: +(originalSize / 1024).toFixed(1),
|
|
302
|
+
finalKB: +(finalSize / 1024).toFixed(1),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
emitLoopDetectionDiagnostic(action, category, message) {
|
|
306
|
+
this.emitDiagnostic({ kind: 'loop_detection', action, category, message });
|
|
307
|
+
}
|
|
308
|
+
emitBudgetWarnings() {
|
|
309
|
+
for (const warning of this.budgetTracker.getActiveWarnings()) {
|
|
310
|
+
this.emitDiagnostic({
|
|
311
|
+
kind: 'budget_warning',
|
|
312
|
+
dimension: warning.dimension,
|
|
313
|
+
percentUsed: warning.percentUsed,
|
|
314
|
+
message: warning.message,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
emitBudgetExhaustedDiagnostic(dimension, message) {
|
|
319
|
+
this.emitDiagnostic({ kind: 'budget_exhausted', dimension, message });
|
|
320
|
+
}
|
|
321
|
+
emitTextDiagnostic(text) {
|
|
322
|
+
if (!text)
|
|
323
|
+
return;
|
|
324
|
+
this.emitDiagnostic({
|
|
325
|
+
kind: 'agent_text',
|
|
326
|
+
preview: `${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
recordTurn(userMessage, assistantResponse, usage, timestamp) {
|
|
330
|
+
return {
|
|
331
|
+
turnNumber: this.turns.length + 1,
|
|
332
|
+
userMessage,
|
|
333
|
+
assistantResponse,
|
|
334
|
+
usage: {
|
|
335
|
+
promptTokens: usage.inputTokens ?? 0,
|
|
336
|
+
completionTokens: usage.outputTokens ?? 0,
|
|
337
|
+
totalTokens: usage.totalTokens ?? 0,
|
|
338
|
+
},
|
|
339
|
+
timestamp,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Starts polling the escalation directory for new request files.
|
|
344
|
+
* When a request is detected, sets pendingEscalation and fires
|
|
345
|
+
* the onEscalation callback.
|
|
346
|
+
*/
|
|
347
|
+
startEscalationWatcher() {
|
|
348
|
+
this.escalationPollInterval = setInterval(() => {
|
|
349
|
+
this.pollEscalationDirectory();
|
|
350
|
+
}, ESCALATION_POLL_INTERVAL_MS);
|
|
351
|
+
}
|
|
352
|
+
pollEscalationDirectory() {
|
|
353
|
+
if (this.pendingEscalation)
|
|
354
|
+
return;
|
|
355
|
+
try {
|
|
356
|
+
const files = readdirSync(this.escalationDir);
|
|
357
|
+
const requestFile = files.find(f => f.startsWith('request-') && f.endsWith('.json') &&
|
|
358
|
+
!this.seenEscalationIds.has(this.extractEscalationId(f)));
|
|
359
|
+
if (!requestFile)
|
|
360
|
+
return;
|
|
361
|
+
const requestPath = resolve(this.escalationDir, requestFile);
|
|
362
|
+
const request = JSON.parse(readFileSync(requestPath, 'utf-8'));
|
|
363
|
+
this.seenEscalationIds.add(request.escalationId);
|
|
364
|
+
this.pendingEscalation = request;
|
|
365
|
+
this.onEscalation?.(request);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Directory may not exist yet or be empty -- ignore
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** Extracts the escalation ID from a request filename like "request-abc123.json". */
|
|
372
|
+
extractEscalationId(filename) {
|
|
373
|
+
return filename.replace(/^request-/, '').replace(/\.json$/, '');
|
|
374
|
+
}
|
|
375
|
+
stopEscalationWatcher() {
|
|
376
|
+
if (this.escalationPollInterval) {
|
|
377
|
+
clearInterval(this.escalationPollInterval);
|
|
378
|
+
this.escalationPollInterval = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
//# sourceMappingURL=agent-session.js.map
|