@townco/agent 0.1.88 → 0.1.98
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 +49 -0
- package/dist/acp-server/adapter.js +693 -5
- package/dist/acp-server/http.d.ts +7 -0
- package/dist/acp-server/http.js +53 -6
- package/dist/definition/index.d.ts +29 -0
- package/dist/definition/index.js +24 -0
- package/dist/runner/agent-runner.d.ts +16 -1
- package/dist/runner/agent-runner.js +2 -1
- package/dist/runner/e2b-sandbox-manager.d.ts +18 -0
- package/dist/runner/e2b-sandbox-manager.js +99 -0
- package/dist/runner/hooks/executor.d.ts +3 -1
- package/dist/runner/hooks/executor.js +21 -1
- package/dist/runner/hooks/predefined/compaction-tool.js +67 -2
- package/dist/runner/hooks/types.d.ts +5 -0
- package/dist/runner/index.d.ts +11 -0
- package/dist/runner/langchain/index.d.ts +10 -0
- package/dist/runner/langchain/index.js +227 -7
- package/dist/runner/langchain/model-factory.js +28 -1
- package/dist/runner/langchain/tools/artifacts.js +6 -3
- package/dist/runner/langchain/tools/e2b.d.ts +48 -0
- package/dist/runner/langchain/tools/e2b.js +305 -0
- package/dist/runner/langchain/tools/filesystem.js +63 -0
- package/dist/runner/langchain/tools/subagent.d.ts +8 -0
- package/dist/runner/langchain/tools/subagent.js +76 -4
- package/dist/runner/langchain/tools/web_search.d.ts +36 -14
- package/dist/runner/langchain/tools/web_search.js +33 -2
- package/dist/runner/session-context.d.ts +20 -0
- package/dist/runner/session-context.js +54 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
|
@@ -8,13 +8,14 @@ import { z } from "zod";
|
|
|
8
8
|
import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
|
|
9
9
|
import { createLogger } from "../../logger.js";
|
|
10
10
|
import { telemetry } from "../../telemetry/index.js";
|
|
11
|
-
import { bindGeneratorToSessionContext } from "../session-context";
|
|
11
|
+
import { bindGeneratorToAbortSignal, bindGeneratorToSessionContext, getAbortSignal, runWithAbortSignal, } from "../session-context";
|
|
12
12
|
import { loadCustomToolModule, } from "../tool-loader.js";
|
|
13
13
|
import { createModelFromString, detectProvider } from "./model-factory.js";
|
|
14
14
|
import { makeOtelCallbacks } from "./otel-callbacks.js";
|
|
15
15
|
import { makeArtifactsTools } from "./tools/artifacts";
|
|
16
16
|
import { makeBrowserTools } from "./tools/browser";
|
|
17
17
|
import { makeDocumentExtractTool } from "./tools/document_extract";
|
|
18
|
+
import { makeTownE2BTools } from "./tools/e2b";
|
|
18
19
|
import { makeFilesystemTools } from "./tools/filesystem";
|
|
19
20
|
import { makeGenerateImageTool, makeTownGenerateImageTool, } from "./tools/generate_image";
|
|
20
21
|
import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
|
|
@@ -44,6 +45,7 @@ export const TOOL_REGISTRY = {
|
|
|
44
45
|
town_generate_image: () => makeTownGenerateImageTool(),
|
|
45
46
|
browser: () => makeBrowserTools(),
|
|
46
47
|
document_extract: () => makeDocumentExtractTool(),
|
|
48
|
+
town_e2b: () => makeTownE2BTools(),
|
|
47
49
|
};
|
|
48
50
|
// ============================================================================
|
|
49
51
|
// Custom tool loading
|
|
@@ -82,6 +84,12 @@ export class LangchainAgent {
|
|
|
82
84
|
return telemetry.bindGeneratorToContext(sessionAttributes, generator);
|
|
83
85
|
}
|
|
84
86
|
async *invokeInternal(req) {
|
|
87
|
+
// Start with the base generator
|
|
88
|
+
let generator = this.invokeWithContext(req);
|
|
89
|
+
// Bind abort signal if available (for cancellation support)
|
|
90
|
+
if (req.abortSignal) {
|
|
91
|
+
generator = bindGeneratorToAbortSignal(req.abortSignal, generator);
|
|
92
|
+
}
|
|
85
93
|
// Set up session context for session-scoped file storage
|
|
86
94
|
// This allows tools to access getSessionContext() / getToolOutputDir()
|
|
87
95
|
if (req.agentDir && req.sessionId) {
|
|
@@ -95,11 +103,9 @@ export class LangchainAgent {
|
|
|
95
103
|
sessionDir,
|
|
96
104
|
artifactsDir,
|
|
97
105
|
};
|
|
98
|
-
|
|
99
|
-
return yield* boundGenerator;
|
|
106
|
+
generator = bindGeneratorToSessionContext(sessionContext, generator);
|
|
100
107
|
}
|
|
101
|
-
|
|
102
|
-
return yield* this.invokeWithContext(req);
|
|
108
|
+
return yield* generator;
|
|
103
109
|
}
|
|
104
110
|
async *invokeWithContext(req) {
|
|
105
111
|
// Derive the parent OTEL context for this invocation.
|
|
@@ -384,6 +390,10 @@ export class LangchainAgent {
|
|
|
384
390
|
const noSession = req.sessionMeta?.[SUBAGENT_MODE_KEY] === true; // Subagents don't have session storage
|
|
385
391
|
// Track cumulative tool output tokens in this turn for proper context calculation
|
|
386
392
|
let cumulativeToolOutputTokens = 0;
|
|
393
|
+
// Counter for subagent calls - used to create unique source ID ranges
|
|
394
|
+
// Each subagent call gets a unique offset (1000, 2000, 3000, etc.)
|
|
395
|
+
// to ensure sources never conflict with parent's sources (typically < 100)
|
|
396
|
+
let subagentCallCounter = 0;
|
|
387
397
|
let wrappedTools = enabledTools;
|
|
388
398
|
if (hasToolResponseHook && !noSession) {
|
|
389
399
|
const { countToolResultTokens } = await import("../../utils/token-counter.js");
|
|
@@ -426,6 +436,7 @@ export class LangchainAgent {
|
|
|
426
436
|
maxTokens,
|
|
427
437
|
percentage: (currentTokens / maxTokens) * 100,
|
|
428
438
|
model: this.definition.model,
|
|
439
|
+
agent: this.definition,
|
|
429
440
|
toolResponse: {
|
|
430
441
|
toolCallId: "pending",
|
|
431
442
|
toolName: originalTool.name,
|
|
@@ -510,6 +521,65 @@ export class LangchainAgent {
|
|
|
510
521
|
allowedToolNames.has(t.name));
|
|
511
522
|
});
|
|
512
523
|
}
|
|
524
|
+
// Wrap the subagent tool to re-number citation sources
|
|
525
|
+
// This ensures sources from subagents get unique IDs that don't conflict
|
|
526
|
+
// with the parent agent's own sources or other subagent calls
|
|
527
|
+
// Each subagent call gets a unique ID range (1000+, 2000+, etc.)
|
|
528
|
+
finalTools = finalTools.map((t) => {
|
|
529
|
+
if (t.name !== SUBAGENT_TOOL_NAME) {
|
|
530
|
+
return t;
|
|
531
|
+
}
|
|
532
|
+
const wrappedFunc = async (input) => {
|
|
533
|
+
const result = (await t.invoke(input));
|
|
534
|
+
// Check if result has sources to re-number
|
|
535
|
+
if (!result ||
|
|
536
|
+
typeof result !== "object" ||
|
|
537
|
+
!Array.isArray(result.sources) ||
|
|
538
|
+
result.sources.length === 0) {
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
// Increment subagent call counter and calculate base offset
|
|
542
|
+
subagentCallCounter++;
|
|
543
|
+
const baseOffset = subagentCallCounter * 1000;
|
|
544
|
+
let sourceIndex = 0;
|
|
545
|
+
// Create ID mapping and re-number sources with offset
|
|
546
|
+
const idMapping = new Map();
|
|
547
|
+
const renumberedSources = result.sources.map((source) => {
|
|
548
|
+
sourceIndex++;
|
|
549
|
+
const newId = String(baseOffset + sourceIndex);
|
|
550
|
+
idMapping.set(source.id, newId);
|
|
551
|
+
return { ...source, id: newId };
|
|
552
|
+
});
|
|
553
|
+
// Update citation references in the text [[oldId]] -> [[newId]]
|
|
554
|
+
let updatedText = result.text;
|
|
555
|
+
for (const [oldId, newId] of idMapping) {
|
|
556
|
+
const pattern = new RegExp(`\\[\\[${oldId}\\]\\]`, "g");
|
|
557
|
+
updatedText = updatedText.replace(pattern, `[[${newId}]]`);
|
|
558
|
+
}
|
|
559
|
+
_logger.info("Re-numbered subagent citation sources", {
|
|
560
|
+
subagentCall: subagentCallCounter,
|
|
561
|
+
originalCount: result.sources.length,
|
|
562
|
+
idRange: `${baseOffset + 1}-${baseOffset + sourceIndex}`,
|
|
563
|
+
});
|
|
564
|
+
return { text: updatedText, sources: renumberedSources };
|
|
565
|
+
};
|
|
566
|
+
// Create new tool with wrapped function
|
|
567
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to pass function with dynamic signature
|
|
568
|
+
const wrappedTool = tool(wrappedFunc, {
|
|
569
|
+
name: t.name,
|
|
570
|
+
description: t.description,
|
|
571
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accessing internal schema property
|
|
572
|
+
schema: t.schema,
|
|
573
|
+
});
|
|
574
|
+
// Preserve metadata
|
|
575
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
576
|
+
wrappedTool.prettyName = t.prettyName;
|
|
577
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to add custom properties to LangChain tool
|
|
578
|
+
wrappedTool.icon = t.icon;
|
|
579
|
+
// biome-ignore lint/suspicious/noExplicitAny: Need to preserve subagentConfigs for metadata
|
|
580
|
+
wrappedTool.subagentConfigs = t.subagentConfigs;
|
|
581
|
+
return wrappedTool;
|
|
582
|
+
});
|
|
513
583
|
// Create the model instance using the factory
|
|
514
584
|
// This detects the provider from the model string:
|
|
515
585
|
// - "gemini-2.0-flash" → Google Generative AI
|
|
@@ -532,6 +602,32 @@ export class LangchainAgent {
|
|
|
532
602
|
if (hasTodoWrite) {
|
|
533
603
|
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${TODO_WRITE_INSTRUCTIONS}`;
|
|
534
604
|
}
|
|
605
|
+
// Inject citation instructions when web search tools are enabled
|
|
606
|
+
const hasWebSearch = builtInNames.includes("web_search") ||
|
|
607
|
+
builtInNames.includes("town_web_search");
|
|
608
|
+
if (hasWebSearch) {
|
|
609
|
+
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${CITATION_INSTRUCTIONS}`;
|
|
610
|
+
}
|
|
611
|
+
// Inject citation instructions when library/document retrieval tools are enabled
|
|
612
|
+
// These are typically MCP tools for searching and retrieving internal documents
|
|
613
|
+
const hasLibraryTools = finalTools.some((t) => t.name.startsWith("library__") ||
|
|
614
|
+
t.name.includes("get_document") ||
|
|
615
|
+
t.name.includes("retrieve_document") ||
|
|
616
|
+
t.name.includes("search_document"));
|
|
617
|
+
if (hasLibraryTools && !hasWebSearch) {
|
|
618
|
+
// Only add if not already covered by web search instructions
|
|
619
|
+
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${DOCUMENT_CITATION_INSTRUCTIONS}`;
|
|
620
|
+
}
|
|
621
|
+
else if (hasLibraryTools && hasWebSearch) {
|
|
622
|
+
// Append document-specific instructions to existing citation instructions
|
|
623
|
+
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${DOCUMENT_CITATION_ADDENDUM}`;
|
|
624
|
+
}
|
|
625
|
+
// Inject subagent citation instructions when agent has subagent tools
|
|
626
|
+
// This tells the parent agent to preserve citations from subagent responses
|
|
627
|
+
const hasSubagentTool = finalTools.some((t) => t.name === SUBAGENT_TOOL_NAME);
|
|
628
|
+
if (hasSubagentTool) {
|
|
629
|
+
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${SUBAGENT_CITATION_INSTRUCTIONS}`;
|
|
630
|
+
}
|
|
535
631
|
// Process template variables in system prompt and inject current date/time
|
|
536
632
|
if (agentConfig.systemPrompt) {
|
|
537
633
|
const currentDateTime = getCurrentDateTimeString();
|
|
@@ -540,6 +636,18 @@ export class LangchainAgent {
|
|
|
540
636
|
// Replace {{.CurrentDateTime}} template variable (alias)
|
|
541
637
|
agentConfig.systemPrompt = agentConfig.systemPrompt.replace(/\{\{\.CurrentDateTime\}\}/g, currentDateTime);
|
|
542
638
|
}
|
|
639
|
+
// Apply prompt parameters from request (user-selected per-message options)
|
|
640
|
+
if (req.promptParameters && this.definition.promptParameters) {
|
|
641
|
+
for (const param of this.definition.promptParameters) {
|
|
642
|
+
const selectedOptionId = req.promptParameters[param.id];
|
|
643
|
+
if (selectedOptionId) {
|
|
644
|
+
const option = param.options.find((o) => o.id === selectedOptionId);
|
|
645
|
+
if (option?.systemPromptAddendum) {
|
|
646
|
+
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${option.systemPromptAddendum}`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
543
651
|
const agent = createAgent(agentConfig);
|
|
544
652
|
// Build messages from context history if available, otherwise use just the prompt
|
|
545
653
|
// Type includes tool messages for sending tool results
|
|
@@ -687,11 +795,17 @@ export class LangchainAgent {
|
|
|
687
795
|
}
|
|
688
796
|
// Create the stream within the invocation context so AsyncLocalStorage
|
|
689
797
|
// propagates the context to all tool executions and callbacks
|
|
798
|
+
_logger.info("Starting agent.stream", {
|
|
799
|
+
messageCount: messages.length,
|
|
800
|
+
effectiveModel,
|
|
801
|
+
sessionId: req.sessionId,
|
|
802
|
+
});
|
|
690
803
|
const stream = context.with(invocationContext, () => agent.stream({ messages }, {
|
|
691
804
|
streamMode: ["updates", "messages"],
|
|
692
805
|
recursionLimit: 200,
|
|
693
806
|
callbacks: [otelCallbacks],
|
|
694
807
|
}));
|
|
808
|
+
_logger.info("agent.stream created, starting iteration");
|
|
695
809
|
// Use manual iteration with Promise.race to interleave stream events with subagent updates
|
|
696
810
|
const streamIterator = (await stream)[Symbol.asyncIterator]();
|
|
697
811
|
let streamDone = false;
|
|
@@ -961,6 +1075,10 @@ export class LangchainAgent {
|
|
|
961
1075
|
continue; // Skip the rest of the processing for this chunk
|
|
962
1076
|
}
|
|
963
1077
|
if (typeof aiMessage.content === "string") {
|
|
1078
|
+
console.log("[DEBUG] Yielding agent_message_chunk (string)", {
|
|
1079
|
+
content: aiMessage.content.slice(0, 100),
|
|
1080
|
+
hasTokenUsage: !!messageTokenUsage,
|
|
1081
|
+
});
|
|
964
1082
|
const msgToYield = messageTokenUsage
|
|
965
1083
|
? {
|
|
966
1084
|
sessionUpdate: "agent_message_chunk",
|
|
@@ -982,8 +1100,16 @@ export class LangchainAgent {
|
|
|
982
1100
|
yield msgToYield;
|
|
983
1101
|
}
|
|
984
1102
|
else if (Array.isArray(aiMessage.content)) {
|
|
1103
|
+
console.log("[DEBUG] Processing array content", {
|
|
1104
|
+
partCount: aiMessage.content.length,
|
|
1105
|
+
partTypes: aiMessage.content.map((p) => p.type),
|
|
1106
|
+
});
|
|
985
1107
|
for (const part of aiMessage.content) {
|
|
986
1108
|
if (part.type === "text" && typeof part.text === "string") {
|
|
1109
|
+
console.log("[DEBUG] Yielding agent_message_chunk (array text)", {
|
|
1110
|
+
text: part.text.slice(0, 100),
|
|
1111
|
+
hasTokenUsage: !!messageTokenUsage,
|
|
1112
|
+
});
|
|
987
1113
|
const msgToYield = messageTokenUsage
|
|
988
1114
|
? {
|
|
989
1115
|
sessionUpdate: "agent_message_chunk",
|
|
@@ -1280,6 +1406,61 @@ I've found some existing telemetry code. Let me mark the first todo as in_progre
|
|
|
1280
1406
|
[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
|
|
1281
1407
|
</example>
|
|
1282
1408
|
`.trim();
|
|
1409
|
+
// System prompt instructions to inject when WebSearch/WebFetch tools are enabled
|
|
1410
|
+
const CITATION_INSTRUCTIONS = `
|
|
1411
|
+
# Citations
|
|
1412
|
+
When referencing information from web search results or fetched web pages, you MUST cite your sources using double-bracket notation: [[1]], [[2]], etc.
|
|
1413
|
+
|
|
1414
|
+
Each number corresponds to a source from your search results. Always cite when:
|
|
1415
|
+
- Quoting or paraphrasing information from a source
|
|
1416
|
+
- Stating facts or statistics from search results
|
|
1417
|
+
- Referencing specific claims from fetched pages
|
|
1418
|
+
|
|
1419
|
+
Example:
|
|
1420
|
+
"According to recent reports, the company's revenue increased by 15% [[1]]. This growth was primarily driven by their expansion into new markets [[2]]."
|
|
1421
|
+
|
|
1422
|
+
Place citations immediately after the relevant claim or at the end of the sentence. Use multiple citations if information comes from multiple sources.
|
|
1423
|
+
`.trim();
|
|
1424
|
+
// System prompt instructions for agents that use document retrieval tools (library, internal docs, etc.)
|
|
1425
|
+
const DOCUMENT_CITATION_INSTRUCTIONS = `
|
|
1426
|
+
# Citations
|
|
1427
|
+
When referencing information from retrieved documents, you MUST cite your sources using double-bracket notation: [[1]], [[2]], etc.
|
|
1428
|
+
|
|
1429
|
+
The document_id from each retrieved document serves as its citation number. Always cite when:
|
|
1430
|
+
- Quoting or paraphrasing information from a document
|
|
1431
|
+
- Stating facts or data from retrieved documents
|
|
1432
|
+
- Referencing specific claims or findings
|
|
1433
|
+
|
|
1434
|
+
Example:
|
|
1435
|
+
If you retrieve a document with document_id 19448 titled "Q3 Financial Report", cite it as:
|
|
1436
|
+
"Revenue grew by 20% in Q3 [[19448]], exceeding analyst expectations."
|
|
1437
|
+
|
|
1438
|
+
When summarizing multiple documents, cite each source appropriately:
|
|
1439
|
+
"The product roadmap [[19448]] aligns with the strategy outlined in the board memo [[19449]]."
|
|
1440
|
+
`.trim();
|
|
1441
|
+
// Addendum for agents that have both web search and document retrieval tools
|
|
1442
|
+
const DOCUMENT_CITATION_ADDENDUM = `
|
|
1443
|
+
# Document Citations
|
|
1444
|
+
In addition to web search citations, when referencing information from retrieved internal documents, cite using the document_id:
|
|
1445
|
+
- Retrieved documents include a document_id field - use this as the citation number
|
|
1446
|
+
- Example: "According to the internal memo [[19448]], the project is on track."
|
|
1447
|
+
`.trim();
|
|
1448
|
+
// System prompt instructions for agents that use subagents with web search capabilities
|
|
1449
|
+
const SUBAGENT_CITATION_INSTRUCTIONS = `
|
|
1450
|
+
# Preserving Citations from Subagents
|
|
1451
|
+
When subagents return results that include citations (in the format [[N]]), you MUST preserve these citations in your final response. The subagent's output includes a "sources" array that maps citation numbers to their source URLs.
|
|
1452
|
+
|
|
1453
|
+
Important guidelines:
|
|
1454
|
+
- When summarizing or quoting information from a subagent's response, preserve the original citation numbers exactly as they appear (e.g., [[1001]], [[1002]])
|
|
1455
|
+
- These citation numbers link to sources that will be displayed to the user
|
|
1456
|
+
- Do not remove or renumber citations - the numbers are already unique across all subagent responses
|
|
1457
|
+
- If you paraphrase information that had a citation, include that citation with your paraphrase
|
|
1458
|
+
- Facts, statistics, quotes, and specific claims from subagent responses should retain their citations
|
|
1459
|
+
|
|
1460
|
+
Example:
|
|
1461
|
+
If a subagent returns: "Revenue increased 15% [[1001]] driven by cloud growth [[1002]]"
|
|
1462
|
+
Your response should preserve those citations: "The company saw 15% revenue growth [[1001]], primarily from cloud services [[1002]]."
|
|
1463
|
+
`.trim();
|
|
1283
1464
|
/**
|
|
1284
1465
|
* Returns a human-readable string of the current date and time.
|
|
1285
1466
|
* Example: "Monday, December 9, 2024 at 2:30 PM PST"
|
|
@@ -1299,6 +1480,35 @@ const getCurrentDateTimeString = () => {
|
|
|
1299
1480
|
};
|
|
1300
1481
|
// Re-export subagent tool utility
|
|
1301
1482
|
export { makeSubagentsTool } from "./tools/subagent.js";
|
|
1483
|
+
/**
|
|
1484
|
+
* Get metadata for children tools of a built-in tool group.
|
|
1485
|
+
* This dynamically loads the tool factory and extracts metadata from each tool.
|
|
1486
|
+
* Returns undefined if the tool is not a group (returns single tool or not found).
|
|
1487
|
+
*/
|
|
1488
|
+
export function getToolGroupChildren(toolName) {
|
|
1489
|
+
const entry = TOOL_REGISTRY[toolName];
|
|
1490
|
+
if (!entry) {
|
|
1491
|
+
return undefined;
|
|
1492
|
+
}
|
|
1493
|
+
// If it's a function, call it to get the tool(s)
|
|
1494
|
+
if (typeof entry === "function") {
|
|
1495
|
+
const result = entry();
|
|
1496
|
+
// Check if it returns an array (tool group)
|
|
1497
|
+
if (Array.isArray(result)) {
|
|
1498
|
+
return result.map((tool) => ({
|
|
1499
|
+
name: tool.name,
|
|
1500
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accessing custom property on LangChain tool
|
|
1501
|
+
prettyName: tool.prettyName || tool.name,
|
|
1502
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accessing custom property on LangChain tool
|
|
1503
|
+
icon: tool.icon,
|
|
1504
|
+
}));
|
|
1505
|
+
}
|
|
1506
|
+
// Single tool factory - not a group
|
|
1507
|
+
return undefined;
|
|
1508
|
+
}
|
|
1509
|
+
// Direct tool reference - not a group
|
|
1510
|
+
return undefined;
|
|
1511
|
+
}
|
|
1302
1512
|
/**
|
|
1303
1513
|
* Wraps a LangChain tool with OpenTelemetry tracing and session_id injection.
|
|
1304
1514
|
* This ensures the tool executes within its own span context,
|
|
@@ -1318,6 +1528,12 @@ function wrapToolWithTracing(originalTool, getIterationContext, sessionId) {
|
|
|
1318
1528
|
];
|
|
1319
1529
|
const needsSessionId = TOOLS_NEEDING_SESSION_ID.includes(originalTool.name);
|
|
1320
1530
|
const wrappedFunc = async (input) => {
|
|
1531
|
+
// Capture the abort signal from the current context BEFORE LangChain breaks AsyncLocalStorage
|
|
1532
|
+
const abortSignal = getAbortSignal();
|
|
1533
|
+
// Check if already cancelled before starting tool
|
|
1534
|
+
if (abortSignal?.aborted) {
|
|
1535
|
+
throw new Error(`Tool ${originalTool.name} cancelled before execution`);
|
|
1536
|
+
}
|
|
1321
1537
|
// Inject session_id if needed
|
|
1322
1538
|
let finalInput = input;
|
|
1323
1539
|
if (needsSessionId && sessionId) {
|
|
@@ -1347,8 +1563,12 @@ function wrapToolWithTracing(originalTool, getIterationContext, sessionId) {
|
|
|
1347
1563
|
? trace.setSpan(iterationContext, toolSpan)
|
|
1348
1564
|
: iterationContext;
|
|
1349
1565
|
try {
|
|
1350
|
-
// Execute within the tool span's context
|
|
1351
|
-
const
|
|
1566
|
+
// Execute within the tool span's context AND with abort signal context restored
|
|
1567
|
+
const executeWithContext = () => context.with(spanContext, () => originalTool.invoke(finalInput));
|
|
1568
|
+
// Wrap with abort signal context if available
|
|
1569
|
+
const result = abortSignal
|
|
1570
|
+
? await runWithAbortSignal(abortSignal, executeWithContext)
|
|
1571
|
+
: await executeWithContext();
|
|
1352
1572
|
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
1353
1573
|
if (toolSpan) {
|
|
1354
1574
|
telemetry.setSpanAttributes(toolSpan, {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
4
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
3
5
|
import { ChatVertexAI } from "@langchain/google-vertexai";
|
|
@@ -104,11 +106,36 @@ export function createModelFromString(modelString) {
|
|
|
104
106
|
case "openai":
|
|
105
107
|
throw new Error("OpenAI provider is not yet supported. Please install @langchain/openai and add implementation.");
|
|
106
108
|
case "anthropic":
|
|
107
|
-
case "claude":
|
|
109
|
+
case "claude": {
|
|
110
|
+
// Read the expected key from .env file for comparison
|
|
111
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
112
|
+
let expectedKey = "";
|
|
113
|
+
try {
|
|
114
|
+
const envContents = fs.readFileSync(envPath, "utf8");
|
|
115
|
+
const match = envContents.match(/ANTHROPIC_API_KEY="?([^"\n]+)"?/);
|
|
116
|
+
if (match) {
|
|
117
|
+
expectedKey = match[1] || "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
expectedKey = "[could not read .env]";
|
|
122
|
+
}
|
|
123
|
+
const actualKey = process.env.ANTHROPIC_API_KEY || "";
|
|
124
|
+
console.log("[DEBUG] Creating Anthropic model", {
|
|
125
|
+
modelName,
|
|
126
|
+
hasApiKey: !!actualKey,
|
|
127
|
+
keysMatch: actualKey === expectedKey,
|
|
128
|
+
actualKeyLength: actualKey.length,
|
|
129
|
+
expectedKeyLength: expectedKey.length,
|
|
130
|
+
actualKeyPrefix: actualKey.slice(0, 15),
|
|
131
|
+
expectedKeyPrefix: expectedKey.slice(0, 15),
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
});
|
|
108
134
|
return new ChatAnthropic({
|
|
109
135
|
model: modelName,
|
|
110
136
|
// Use default Anthropic settings
|
|
111
137
|
});
|
|
138
|
+
}
|
|
112
139
|
default:
|
|
113
140
|
// Fallback to Anthropic for unknown providers (backward compatibility)
|
|
114
141
|
logger.warn(`Unknown provider "${provider}" in model string "${modelString}". Defaulting to Anthropic.`);
|
|
@@ -31,7 +31,10 @@ import { z } from "zod";
|
|
|
31
31
|
function getArtifactsKeyPrefix() {
|
|
32
32
|
// 1. Explicit override (highest priority)
|
|
33
33
|
const explicitPrefix = process.env.ARTIFACTS_KEY_PREFIX;
|
|
34
|
-
|
|
34
|
+
// Check for valid prefix (not empty, not just quotes from env parsing)
|
|
35
|
+
if (explicitPrefix &&
|
|
36
|
+
explicitPrefix !== '""' &&
|
|
37
|
+
explicitPrefix.trim() !== "") {
|
|
35
38
|
return explicitPrefix.replace(/\/+$/, ""); // Remove trailing slashes
|
|
36
39
|
}
|
|
37
40
|
// 2. Check deployment vs local mode
|
|
@@ -393,8 +396,8 @@ const artifactsUrl = tool(async ({ session_id, path, }) => {
|
|
|
393
396
|
try {
|
|
394
397
|
validateStoragePath(path);
|
|
395
398
|
const storageKey = buildStorageKey(session_id, path);
|
|
396
|
-
const
|
|
397
|
-
return `Signed URL for ${path}`;
|
|
399
|
+
const url = await generateSignedUrl(storageKey);
|
|
400
|
+
return `Signed URL for ${path}:\n${url}`;
|
|
398
401
|
}
|
|
399
402
|
catch (error) {
|
|
400
403
|
return handleStorageError(error, `Failed to generate URL for ${path}`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Create E2B tools using Town proxy authentication.
|
|
4
|
+
* Fetches E2B API key from Town server using user credentials.
|
|
5
|
+
*/
|
|
6
|
+
export declare function makeTownE2BTools(): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
7
|
+
code: z.ZodString;
|
|
8
|
+
language: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
javascript: "javascript";
|
|
10
|
+
python: "python";
|
|
11
|
+
}>>>;
|
|
12
|
+
}, z.core.$strip>, {
|
|
13
|
+
code: string;
|
|
14
|
+
language: "javascript" | "python";
|
|
15
|
+
}, {
|
|
16
|
+
code: string;
|
|
17
|
+
language?: "javascript" | "python" | undefined;
|
|
18
|
+
}, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
19
|
+
command: z.ZodString;
|
|
20
|
+
}, z.core.$strip>, {
|
|
21
|
+
command: string;
|
|
22
|
+
}, {
|
|
23
|
+
command: string;
|
|
24
|
+
}, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
25
|
+
path: z.ZodString;
|
|
26
|
+
}, z.core.$strip>, {
|
|
27
|
+
path: string;
|
|
28
|
+
}, {
|
|
29
|
+
path: string;
|
|
30
|
+
}, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
31
|
+
path: z.ZodString;
|
|
32
|
+
content: z.ZodString;
|
|
33
|
+
}, z.core.$strip>, {
|
|
34
|
+
path: string;
|
|
35
|
+
content: string;
|
|
36
|
+
}, {
|
|
37
|
+
path: string;
|
|
38
|
+
content: string;
|
|
39
|
+
}, string>, import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
40
|
+
sandboxPath: z.ZodString;
|
|
41
|
+
fileName: z.ZodOptional<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>, {
|
|
43
|
+
sandboxPath: string;
|
|
44
|
+
fileName?: string | undefined;
|
|
45
|
+
}, {
|
|
46
|
+
sandboxPath: string;
|
|
47
|
+
fileName?: string | undefined;
|
|
48
|
+
}, string>];
|