@townco/agent 0.1.107 → 0.1.109
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/acp-server/adapter.d.ts +11 -1
- package/dist/acp-server/adapter.js +171 -17
- package/dist/acp-server/http.js +146 -0
- package/dist/acp-server/session-storage.d.ts +11 -0
- package/dist/acp-server/session-storage.js +45 -0
- package/dist/runner/agent-runner.d.ts +13 -4
- package/dist/runner/e2b-sandbox-manager.d.ts +8 -0
- package/dist/runner/e2b-sandbox-manager.js +163 -2
- package/dist/runner/hooks/predefined/compaction-tool.js +52 -39
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/langchain/index.js +43 -38
- package/dist/runner/langchain/tools/e2b.d.ts +10 -0
- package/dist/runner/langchain/tools/e2b.js +323 -32
- package/dist/runner/session-context.d.ts +17 -0
- package/dist/runner/session-context.js +35 -0
- package/dist/runner/tools.d.ts +2 -5
- package/dist/runner/tools.js +1 -15
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/dist/runner/langchain/tools/artifacts.d.ts +0 -68
- package/dist/runner/langchain/tools/artifacts.js +0 -474
- package/dist/runner/langchain/tools/generate_image.d.ts +0 -47
- package/dist/runner/langchain/tools/generate_image.js +0 -175
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import { createLogger } from "../logger.js";
|
|
2
3
|
import { getSessionContext, hasSessionContext } from "./session-context";
|
|
3
4
|
const logger = createLogger("e2b-sandbox-manager");
|
|
@@ -9,16 +10,64 @@ const sandboxActivity = new Map();
|
|
|
9
10
|
const cleanupTimeouts = new Map();
|
|
10
11
|
// Sandbox timeout in milliseconds (default: 15 minutes)
|
|
11
12
|
const SANDBOX_TIMEOUT_MS = 15 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Collect environment variables that should be passed to E2B sandbox
|
|
15
|
+
* for tool usage (image generation, etc.)
|
|
16
|
+
*/
|
|
17
|
+
function collectToolEnvironmentVars() {
|
|
18
|
+
const envs = {};
|
|
19
|
+
// Gemini API key for image generation
|
|
20
|
+
if (process.env.GEMINI_API_KEY) {
|
|
21
|
+
envs.GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
|
22
|
+
logger.debug("GEMINI_API_KEY configured for sandbox", {
|
|
23
|
+
keyLength: process.env.GEMINI_API_KEY.length,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
logger.warn("GEMINI_API_KEY not set - image generation will fail");
|
|
28
|
+
}
|
|
29
|
+
return envs;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get SessionStorage instance for the current session.
|
|
33
|
+
* Returns null if session storage is not available.
|
|
34
|
+
*/
|
|
35
|
+
function getSessionStorage() {
|
|
36
|
+
if (!hasSessionContext()) {
|
|
37
|
+
logger.debug("No session context available for SessionStorage");
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const { sessionDir, sessionId } = getSessionContext();
|
|
41
|
+
// Derive agentDir from sessionDir
|
|
42
|
+
// sessionDir = <agentDir>/.sessions/<sessionId>
|
|
43
|
+
// so agentDir = dirname(dirname(sessionDir))
|
|
44
|
+
const agentDir = path.dirname(path.dirname(sessionDir));
|
|
45
|
+
// Extract agent name from agentDir (last path component)
|
|
46
|
+
const agentName = path.basename(agentDir);
|
|
47
|
+
// Import SessionStorage dynamically to avoid circular dependency
|
|
48
|
+
try {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
50
|
+
const { SessionStorage: SessionStorageClass, } = require("../acp-server/session-storage");
|
|
51
|
+
const storage = new SessionStorageClass(agentDir, agentName);
|
|
52
|
+
logger.debug("Created SessionStorage", { sessionId, agentDir, agentName });
|
|
53
|
+
return storage;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
logger.error("Failed to create SessionStorage", { error, sessionId });
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
12
60
|
/**
|
|
13
61
|
* Get or create an E2B sandbox for the current session.
|
|
14
62
|
* Sandboxes are session-scoped and reused across tool calls.
|
|
63
|
+
* Attempts to reconnect to persisted sandboxes when resuming sessions.
|
|
15
64
|
*/
|
|
16
65
|
export async function getSessionSandbox(apiKey) {
|
|
17
66
|
if (!hasSessionContext()) {
|
|
18
67
|
throw new Error("E2B tools require session context");
|
|
19
68
|
}
|
|
20
69
|
const { sessionId } = getSessionContext();
|
|
21
|
-
// Check for existing sandbox
|
|
70
|
+
// Check for existing in-memory sandbox
|
|
22
71
|
let sandbox = sessionSandboxes.get(sessionId);
|
|
23
72
|
if (sandbox) {
|
|
24
73
|
// Update activity timestamp and reschedule cleanup
|
|
@@ -27,10 +76,91 @@ export async function getSessionSandbox(apiKey) {
|
|
|
27
76
|
logger.debug("Reusing existing sandbox", { sessionId });
|
|
28
77
|
return sandbox;
|
|
29
78
|
}
|
|
79
|
+
// Try to reconnect to persisted sandbox
|
|
80
|
+
const storage = getSessionStorage();
|
|
81
|
+
const persistedSandboxId = storage?.getSandboxId(sessionId);
|
|
82
|
+
if (persistedSandboxId) {
|
|
83
|
+
logger.info("Attempting to reconnect to persisted sandbox", {
|
|
84
|
+
sessionId,
|
|
85
|
+
sandboxId: persistedSandboxId,
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
const { Sandbox: SandboxClass } = await import("@e2b/code-interpreter");
|
|
89
|
+
sandbox = await SandboxClass.connect(persistedSandboxId, { apiKey });
|
|
90
|
+
logger.info("Successfully reconnected to sandbox", {
|
|
91
|
+
sessionId,
|
|
92
|
+
sandboxId: persistedSandboxId,
|
|
93
|
+
});
|
|
94
|
+
// Store in memory and set up activity tracking
|
|
95
|
+
sessionSandboxes.set(sessionId, sandbox);
|
|
96
|
+
sandboxActivity.set(sessionId, Date.now());
|
|
97
|
+
scheduleCleanup(sessionId);
|
|
98
|
+
return sandbox;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.warn("Failed to reconnect to persisted sandbox, will create new one", {
|
|
102
|
+
sessionId,
|
|
103
|
+
sandboxId: persistedSandboxId,
|
|
104
|
+
error,
|
|
105
|
+
});
|
|
106
|
+
// Clear the stale sandboxId from storage
|
|
107
|
+
if (storage) {
|
|
108
|
+
try {
|
|
109
|
+
await storage.updateSandboxId(sessionId, undefined);
|
|
110
|
+
}
|
|
111
|
+
catch (updateError) {
|
|
112
|
+
logger.warn("Failed to clear stale sandboxId", {
|
|
113
|
+
sessionId,
|
|
114
|
+
updateError,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
30
120
|
// Create new sandbox - dynamic import to avoid loading if not used
|
|
31
121
|
logger.info("Creating new E2B sandbox", { sessionId });
|
|
32
122
|
const { Sandbox: SandboxClass } = await import("@e2b/code-interpreter");
|
|
33
|
-
|
|
123
|
+
const envs = collectToolEnvironmentVars();
|
|
124
|
+
// Use custom template with pre-installed packages (google-generativeai, etc.)
|
|
125
|
+
const templateId = process.env.E2B_TEMPLATE_ID;
|
|
126
|
+
const config = {
|
|
127
|
+
apiKey,
|
|
128
|
+
envVars: envs,
|
|
129
|
+
};
|
|
130
|
+
if (templateId) {
|
|
131
|
+
config.template = templateId;
|
|
132
|
+
logger.info("Using custom E2B template", { templateId });
|
|
133
|
+
}
|
|
134
|
+
sandbox = await SandboxClass.create(config);
|
|
135
|
+
logger.info("Created new sandbox", {
|
|
136
|
+
sessionId,
|
|
137
|
+
sandboxId: sandbox.sandboxId,
|
|
138
|
+
});
|
|
139
|
+
// Persist the sandboxId for future reconnection
|
|
140
|
+
if (storage) {
|
|
141
|
+
try {
|
|
142
|
+
await storage.updateSandboxId(sessionId, sandbox.sandboxId);
|
|
143
|
+
logger.info("Persisted sandboxId to storage", {
|
|
144
|
+
sessionId,
|
|
145
|
+
sandboxId: sandbox.sandboxId,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
logger.error("Failed to persist sandboxId", { sessionId, error });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
logger.warn("No storage available to persist sandboxId", { sessionId });
|
|
154
|
+
}
|
|
155
|
+
// Configure Python logging to output to stderr by default
|
|
156
|
+
await sandbox.runCode(`
|
|
157
|
+
import logging
|
|
158
|
+
logging.basicConfig(
|
|
159
|
+
level=logging.INFO,
|
|
160
|
+
format='%(levelname)s - %(name)s - %(message)s',
|
|
161
|
+
stream=__import__('sys').stderr
|
|
162
|
+
)
|
|
163
|
+
`.trim(), { language: "python" });
|
|
34
164
|
sessionSandboxes.set(sessionId, sandbox);
|
|
35
165
|
sandboxActivity.set(sessionId, Date.now());
|
|
36
166
|
// Set up auto-cleanup timeout
|
|
@@ -39,6 +169,7 @@ export async function getSessionSandbox(apiKey) {
|
|
|
39
169
|
}
|
|
40
170
|
/**
|
|
41
171
|
* Explicitly destroy a session's sandbox (called on session end).
|
|
172
|
+
* Also clears the persisted sandboxId from storage.
|
|
42
173
|
*/
|
|
43
174
|
export async function destroySessionSandbox(sessionId) {
|
|
44
175
|
const sandbox = sessionSandboxes.get(sessionId);
|
|
@@ -58,6 +189,22 @@ export async function destroySessionSandbox(sessionId) {
|
|
|
58
189
|
clearTimeout(timeout);
|
|
59
190
|
cleanupTimeouts.delete(sessionId);
|
|
60
191
|
}
|
|
192
|
+
// Clear persisted sandboxId from storage
|
|
193
|
+
// We do this without requiring session context since this can be called
|
|
194
|
+
// from HTTP endpoints that don't have session context
|
|
195
|
+
try {
|
|
196
|
+
// Try to get storage if we have context, but don't fail if we don't
|
|
197
|
+
if (hasSessionContext()) {
|
|
198
|
+
const storage = getSessionStorage();
|
|
199
|
+
if (storage) {
|
|
200
|
+
await storage.updateSandboxId(sessionId, undefined);
|
|
201
|
+
logger.debug("Cleared persisted sandboxId", { sessionId });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
logger.warn("Failed to clear persisted sandboxId", { sessionId, error });
|
|
207
|
+
}
|
|
61
208
|
}
|
|
62
209
|
}
|
|
63
210
|
/**
|
|
@@ -91,6 +238,20 @@ function rescheduleCleanup(sessionId) {
|
|
|
91
238
|
export function hasSessionSandbox(sessionId) {
|
|
92
239
|
return sessionSandboxes.has(sessionId);
|
|
93
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Get an existing sandbox by sessionId without creating a new one.
|
|
243
|
+
* Returns undefined if no sandbox exists for this session.
|
|
244
|
+
* Used by HTTP endpoints that don't have session context.
|
|
245
|
+
*/
|
|
246
|
+
export function getExistingSandbox(sessionId) {
|
|
247
|
+
const sandbox = sessionSandboxes.get(sessionId);
|
|
248
|
+
if (sandbox) {
|
|
249
|
+
// Update activity timestamp and reschedule cleanup
|
|
250
|
+
sandboxActivity.set(sessionId, Date.now());
|
|
251
|
+
rescheduleCleanup(sessionId);
|
|
252
|
+
}
|
|
253
|
+
return sandbox;
|
|
254
|
+
}
|
|
94
255
|
/**
|
|
95
256
|
* Get the number of active sandboxes (for debugging/monitoring).
|
|
96
257
|
*/
|
|
@@ -14,6 +14,7 @@ export const compactionTool = async (ctx) => {
|
|
|
14
14
|
// Read settings from callbackSetting
|
|
15
15
|
const settings = ctx.callbackSetting;
|
|
16
16
|
const threshold = settings?.threshold ?? 80;
|
|
17
|
+
const forceCompact = settings?.forceCompact ?? false;
|
|
17
18
|
// Calculate effective token usage including pending tool response if present
|
|
18
19
|
const toolResponseTokens = ctx.toolResponse?.outputTokens ?? 0;
|
|
19
20
|
const estimatedTokens = ctx.currentTokens + toolResponseTokens;
|
|
@@ -21,7 +22,8 @@ export const compactionTool = async (ctx) => {
|
|
|
21
22
|
const paddedTokens = applyTokenPadding(estimatedTokens);
|
|
22
23
|
const effectivePercentage = (paddedTokens / ctx.maxTokens) * 100;
|
|
23
24
|
// Check if we should run based on context percentage (including tool response)
|
|
24
|
-
if (
|
|
25
|
+
// Skip threshold check if forceCompact is true (used for error recovery)
|
|
26
|
+
if (!forceCompact && effectivePercentage < threshold) {
|
|
25
27
|
logger.debug("Context below threshold, skipping compaction", {
|
|
26
28
|
currentTokens: ctx.currentTokens,
|
|
27
29
|
toolResponseTokens,
|
|
@@ -40,6 +42,15 @@ export const compactionTool = async (ctx) => {
|
|
|
40
42
|
},
|
|
41
43
|
};
|
|
42
44
|
}
|
|
45
|
+
// Log if force compaction was triggered
|
|
46
|
+
if (forceCompact) {
|
|
47
|
+
logger.info("Force compaction triggered (bypassing threshold check)", {
|
|
48
|
+
currentTokens: ctx.currentTokens,
|
|
49
|
+
toolResponseTokens,
|
|
50
|
+
effectivePercentage: `${effectivePercentage.toFixed(2)}%`,
|
|
51
|
+
threshold,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
43
54
|
logger.info("Compaction tool triggered", {
|
|
44
55
|
currentTokens: ctx.currentTokens,
|
|
45
56
|
toolResponseTokens,
|
|
@@ -192,44 +203,46 @@ Please provide your summary based on the conversation above, following this stru
|
|
|
192
203
|
const kgMatch = summaryText.match(/<knowledge_graph_analysis_needed>(yes|no)<\/knowledge_graph_analysis_needed>/i);
|
|
193
204
|
needsKnowledgeGraphAnalysis = kgMatch?.[1]?.toLowerCase() === "yes";
|
|
194
205
|
logger.info(`Knowledge graph analysis decision: needsAnalysis=${needsKnowledgeGraphAnalysis}, matchFound=${!!kgMatch}`);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
206
|
+
// TODO: Re-enable graph analysis dispatch when ready
|
|
207
|
+
// if (needsKnowledgeGraphAnalysis) {
|
|
208
|
+
// const apiUrl = process.env.LIBRARY_API_URL;
|
|
209
|
+
// const apiKey = process.env.LIBRARY_API_KEY;
|
|
210
|
+
// logger.info("Attempting to dispatch to Library API", {
|
|
211
|
+
// hasApiUrl: !!apiUrl,
|
|
212
|
+
// hasApiKey: !!apiKey,
|
|
213
|
+
// });
|
|
214
|
+
// if (apiUrl && apiKey) {
|
|
215
|
+
// const serviceUrl = `${apiUrl}/compaction_analysis`;
|
|
216
|
+
// // Fire-and-forget - don't block compaction on external service
|
|
217
|
+
// fetch(serviceUrl, {
|
|
218
|
+
// method: "POST",
|
|
219
|
+
// headers: {
|
|
220
|
+
// "Content-Type": "application/json",
|
|
221
|
+
// Authorization: `Bearer ${apiKey}`,
|
|
222
|
+
// },
|
|
223
|
+
// body: JSON.stringify({
|
|
224
|
+
// conversation_text: conversationText,
|
|
225
|
+
// timestamp: Date.now(),
|
|
226
|
+
// }),
|
|
227
|
+
// })
|
|
228
|
+
// .then((response) => {
|
|
229
|
+
// logger.info("Library API response received", {
|
|
230
|
+
// status: response.status,
|
|
231
|
+
// ok: response.ok,
|
|
232
|
+
// });
|
|
233
|
+
// })
|
|
234
|
+
// .catch((error) =>
|
|
235
|
+
// logger.error("Failed to send compaction for analysis", {
|
|
236
|
+
// error: error instanceof Error ? error.message : String(error),
|
|
237
|
+
// }),
|
|
238
|
+
// );
|
|
239
|
+
// } else {
|
|
240
|
+
// logger.warn("Skipping Library API dispatch - missing config", {
|
|
241
|
+
// hasApiUrl: !!apiUrl,
|
|
242
|
+
// hasApiKey: !!apiKey,
|
|
243
|
+
// });
|
|
244
|
+
// }
|
|
245
|
+
// }
|
|
233
246
|
}
|
|
234
247
|
return {
|
|
235
248
|
newContextEntry,
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type AgentRunner } from "./agent-runner";
|
|
2
|
-
export type { AgentRunner };
|
|
1
|
+
import { type AgentRunner, type SessionUpdateNotification } from "./agent-runner";
|
|
2
|
+
export type { AgentRunner, SessionUpdateNotification };
|
|
3
3
|
export declare const makeRunnerFromDefinition: (definition: {
|
|
4
4
|
displayName?: string | undefined;
|
|
5
5
|
version?: string | undefined;
|
package/dist/runner/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { zAgentRunnerParams } from "./agent-runner";
|
|
1
|
+
import { zAgentRunnerParams, } from "./agent-runner";
|
|
2
2
|
import { LangchainAgent } from "./langchain";
|
|
3
3
|
export const makeRunnerFromDefinition = (definition) => {
|
|
4
4
|
const agentRunnerParams = zAgentRunnerParams.safeParse(definition);
|
|
@@ -5,21 +5,19 @@ import { context, propagation, trace } from "@opentelemetry/api";
|
|
|
5
5
|
import { ensureAuthenticated } from "@townco/core/auth";
|
|
6
6
|
import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
|
|
8
|
+
import { ContextOverflowError, SUBAGENT_MODE_KEY, } from "../../acp-server/adapter";
|
|
9
9
|
import { createLogger } from "../../logger.js";
|
|
10
10
|
import { telemetry } from "../../telemetry/index.js";
|
|
11
11
|
import { calculateContextSize } from "../../utils/context-size-calculator.js";
|
|
12
12
|
import { getModelContextWindow } from "../hooks/constants.js";
|
|
13
|
-
import {
|
|
13
|
+
import { isContextOverflowError } from "../hooks/predefined/context-validator.js";
|
|
14
|
+
import { bindGeneratorToAbortSignal, bindGeneratorToEmitUpdate, bindGeneratorToSessionContext, getAbortSignal, runWithAbortSignal, } from "../session-context";
|
|
14
15
|
import { loadCustomToolModule, } from "../tool-loader.js";
|
|
15
16
|
import { createModelFromString, detectProvider } from "./model-factory.js";
|
|
16
17
|
import { makeOtelCallbacks } from "./otel-callbacks.js";
|
|
17
|
-
import { makeArtifactsTools } from "./tools/artifacts";
|
|
18
18
|
import { makeBrowserTools } from "./tools/browser";
|
|
19
19
|
import { makeDocumentExtractTool } from "./tools/document_extract";
|
|
20
20
|
import { makeTownE2BTools } from "./tools/e2b";
|
|
21
|
-
import { makeFilesystemTools } from "./tools/filesystem";
|
|
22
|
-
import { makeGenerateImageTool, makeTownGenerateImageTool, } from "./tools/generate_image";
|
|
23
21
|
import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
|
|
24
22
|
import { hashQuery, queryToToolCallId, subagentEvents, } from "./tools/subagent-connections";
|
|
25
23
|
import { makeTodoWriteTool, TODO_WRITE_TOOL_NAME } from "./tools/todo";
|
|
@@ -37,17 +35,13 @@ getWeather.prettyName = "Get Weather";
|
|
|
37
35
|
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
38
36
|
getWeather.icon = "Cloud";
|
|
39
37
|
export const TOOL_REGISTRY = {
|
|
40
|
-
artifacts: () => makeArtifactsTools(),
|
|
41
38
|
todo_write: () => makeTodoWriteTool(), // Factory function to create fresh instance per invocation
|
|
42
39
|
get_weather: getWeather, // TODO: Convert to factory function for full concurrency safety
|
|
43
40
|
web_search: () => makeWebSearchTools(),
|
|
44
41
|
town_web_search: () => makeTownWebSearchTools(),
|
|
45
|
-
filesystem: () => makeFilesystemTools(),
|
|
46
|
-
generate_image: () => makeGenerateImageTool(),
|
|
47
|
-
town_generate_image: () => makeTownGenerateImageTool(),
|
|
48
42
|
browser: () => makeBrowserTools(),
|
|
49
43
|
document_extract: () => makeDocumentExtractTool(),
|
|
50
|
-
code_sandbox:
|
|
44
|
+
code_sandbox: makeTownE2BTools,
|
|
51
45
|
};
|
|
52
46
|
// ============================================================================
|
|
53
47
|
// Custom tool loading
|
|
@@ -92,6 +86,10 @@ export class LangchainAgent {
|
|
|
92
86
|
if (req.abortSignal) {
|
|
93
87
|
generator = bindGeneratorToAbortSignal(req.abortSignal, generator);
|
|
94
88
|
}
|
|
89
|
+
// Bind emitUpdate callback if available (for file change events)
|
|
90
|
+
if (req.emitUpdate) {
|
|
91
|
+
generator = bindGeneratorToEmitUpdate(req.emitUpdate, generator);
|
|
92
|
+
}
|
|
95
93
|
// Set up session context for session-scoped file storage
|
|
96
94
|
// This allows tools to access getSessionContext() / getToolOutputDir()
|
|
97
95
|
if (req.agentDir && req.sessionId) {
|
|
@@ -290,16 +288,6 @@ export class LangchainAgent {
|
|
|
290
288
|
typeof t.modulePath === "string") {
|
|
291
289
|
customToolPaths.push(t.modulePath);
|
|
292
290
|
}
|
|
293
|
-
else if (type === "filesystem") {
|
|
294
|
-
// Note: working_directory is ignored - filesystem tools now use session context
|
|
295
|
-
const fsTools = makeFilesystemTools();
|
|
296
|
-
// Tag filesystem tools with their group name for tool override filtering
|
|
297
|
-
for (const fsTool of fsTools) {
|
|
298
|
-
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom property for tool grouping
|
|
299
|
-
fsTool.__groupName = "filesystem";
|
|
300
|
-
}
|
|
301
|
-
enabledTools.push(...fsTools);
|
|
302
|
-
}
|
|
303
291
|
else if (type === "direct") {
|
|
304
292
|
// Handle direct tool objects (imported in code)
|
|
305
293
|
// biome-ignore lint/suspicious/noExplicitAny: mlai unsure how to best type this
|
|
@@ -320,6 +308,31 @@ export class LangchainAgent {
|
|
|
320
308
|
for (const name of builtInNames) {
|
|
321
309
|
const entry = TOOL_REGISTRY[name];
|
|
322
310
|
if (!entry) {
|
|
311
|
+
// Provide helpful error messages for removed tools
|
|
312
|
+
// Cast to string to allow checking against removed tool names
|
|
313
|
+
const nameStr = name;
|
|
314
|
+
if (nameStr === "filesystem") {
|
|
315
|
+
throw new Error(`The "filesystem" tool has been removed. Use "code_sandbox" instead.\n\n` +
|
|
316
|
+
`Migration:\n` +
|
|
317
|
+
`- Change tools: ["filesystem"] to tools: ["code_sandbox"]\n` +
|
|
318
|
+
`- Read files: Use Sandbox_ReadFile\n` +
|
|
319
|
+
`- Write files: Use Sandbox_WriteFile\n` +
|
|
320
|
+
`- Search files: Use Sandbox_RunBash with grep/find commands`);
|
|
321
|
+
}
|
|
322
|
+
if (nameStr === "artifacts") {
|
|
323
|
+
throw new Error(`The "artifacts" tool has been removed. Use "code_sandbox" with Sandbox_Share instead.\n\n` +
|
|
324
|
+
`Migration:\n` +
|
|
325
|
+
`- Change tools: ["artifacts"] to tools: ["code_sandbox"]\n` +
|
|
326
|
+
`- Share files: Use Sandbox_Share to upload sandbox files to Supabase Storage`);
|
|
327
|
+
}
|
|
328
|
+
if (nameStr === "generate_image" ||
|
|
329
|
+
nameStr === "town_generate_image") {
|
|
330
|
+
throw new Error(`The "${nameStr}" tool has been removed. Use "code_sandbox" with Sandbox_GenerateImage instead.\n\n` +
|
|
331
|
+
`Migration:\n` +
|
|
332
|
+
`- Change tools: ["${nameStr}"] to tools: ["code_sandbox"]\n` +
|
|
333
|
+
`- Generate images: Use Sandbox_GenerateImage (runs in sandbox)\n` +
|
|
334
|
+
`- Share images: Use Sandbox_Share to make images accessible`);
|
|
335
|
+
}
|
|
323
336
|
throw new Error(`Unknown built-in tool "${name}"`);
|
|
324
337
|
}
|
|
325
338
|
const tagTool = (tool) => {
|
|
@@ -1127,10 +1140,6 @@ export class LangchainAgent {
|
|
|
1127
1140
|
continue; // Skip the rest of the processing for this chunk
|
|
1128
1141
|
}
|
|
1129
1142
|
if (typeof aiMessage.content === "string") {
|
|
1130
|
-
console.log("[DEBUG] Yielding agent_message_chunk (string)", {
|
|
1131
|
-
content: aiMessage.content.slice(0, 100),
|
|
1132
|
-
hasTokenUsage: !!messageTokenUsage,
|
|
1133
|
-
});
|
|
1134
1143
|
const msgToYield = messageTokenUsage
|
|
1135
1144
|
? {
|
|
1136
1145
|
sessionUpdate: "agent_message_chunk",
|
|
@@ -1152,16 +1161,8 @@ export class LangchainAgent {
|
|
|
1152
1161
|
yield msgToYield;
|
|
1153
1162
|
}
|
|
1154
1163
|
else if (Array.isArray(aiMessage.content)) {
|
|
1155
|
-
console.log("[DEBUG] Processing array content", {
|
|
1156
|
-
partCount: aiMessage.content.length,
|
|
1157
|
-
partTypes: aiMessage.content.map((p) => p.type),
|
|
1158
|
-
});
|
|
1159
1164
|
for (const part of aiMessage.content) {
|
|
1160
1165
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1161
|
-
console.log("[DEBUG] Yielding agent_message_chunk (array text)", {
|
|
1162
|
-
text: part.text.slice(0, 100),
|
|
1163
|
-
hasTokenUsage: !!messageTokenUsage,
|
|
1164
|
-
});
|
|
1165
1166
|
const msgToYield = messageTokenUsage
|
|
1166
1167
|
? {
|
|
1167
1168
|
sessionUpdate: "agent_message_chunk",
|
|
@@ -1339,6 +1340,15 @@ export class LangchainAgent {
|
|
|
1339
1340
|
subagentEvents.off("messages", onSubagentMessages);
|
|
1340
1341
|
// Clean up any remaining iteration span
|
|
1341
1342
|
otelCallbacks?.cleanup();
|
|
1343
|
+
// Check if this is a context overflow error - wrap it for retry handling
|
|
1344
|
+
if (isContextOverflowError(error)) {
|
|
1345
|
+
telemetry.log("warn", "Context overflow detected - will attempt compaction and retry", {
|
|
1346
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1347
|
+
sessionId: req.sessionId,
|
|
1348
|
+
});
|
|
1349
|
+
telemetry.endSpan(invocationSpan);
|
|
1350
|
+
throw new ContextOverflowError(error instanceof Error ? error : new Error(String(error)));
|
|
1351
|
+
}
|
|
1342
1352
|
// Log error and end span with error status
|
|
1343
1353
|
telemetry.log("error", "Agent invocation failed", {
|
|
1344
1354
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -1572,12 +1582,7 @@ export function getToolGroupChildren(toolName) {
|
|
|
1572
1582
|
*/
|
|
1573
1583
|
function wrapToolWithTracing(originalTool, getIterationContext, sessionId) {
|
|
1574
1584
|
// Check if this tool needs session_id injection
|
|
1575
|
-
const TOOLS_NEEDING_SESSION_ID = [
|
|
1576
|
-
"artifacts_cp",
|
|
1577
|
-
"artifacts_del",
|
|
1578
|
-
"artifacts_ls",
|
|
1579
|
-
"artifacts_url",
|
|
1580
|
-
];
|
|
1585
|
+
const TOOLS_NEEDING_SESSION_ID = [];
|
|
1581
1586
|
const needsSessionId = TOOLS_NEEDING_SESSION_ID.includes(originalTool.name);
|
|
1582
1587
|
const wrappedFunc = async (input) => {
|
|
1583
1588
|
// Capture the abort signal from the current context BEFORE LangChain breaks AsyncLocalStorage
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Get E2B API key from Town proxy (with caching).
|
|
4
|
+
*/
|
|
5
|
+
export declare function getTownE2BApiKey(): Promise<string>;
|
|
2
6
|
/**
|
|
3
7
|
* Create E2B tools using Town proxy authentication.
|
|
4
8
|
* Fetches E2B API key from Town server using user credentials.
|
|
@@ -51,4 +55,10 @@ export declare function makeTownE2BTools(): readonly [import("langchain").Dynami
|
|
|
51
55
|
document_id: string;
|
|
52
56
|
}, {
|
|
53
57
|
document_id: string;
|
|
58
|
+
}, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
59
|
+
prompt: z.ZodString;
|
|
60
|
+
}, z.core.$strip>, {
|
|
61
|
+
prompt: string;
|
|
62
|
+
}, {
|
|
63
|
+
prompt: string;
|
|
54
64
|
}, string>];
|