@townco/agent 0.1.72 → 0.1.74
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.js +98 -5
- package/dist/acp-server/http.js +71 -14
- package/dist/acp-server/session-storage.d.ts +40 -0
- package/dist/acp-server/session-storage.js +26 -0
- package/dist/runner/agent-runner.d.ts +11 -1
- package/dist/runner/hooks/predefined/tool-response-compactor.js +73 -19
- package/dist/runner/langchain/index.js +104 -29
- package/dist/runner/langchain/model-factory.js +6 -9
- package/dist/runner/langchain/otel-callbacks.d.ts +7 -1
- package/dist/runner/langchain/otel-callbacks.js +80 -20
- package/dist/runner/langchain/tools/filesystem.js +15 -0
- package/dist/runner/langchain/tools/subagent-connections.d.ts +34 -0
- package/dist/runner/langchain/tools/subagent-connections.js +32 -1
- package/dist/runner/langchain/tools/subagent.js +66 -8
- package/dist/runner/langchain/tools/todo.js +4 -0
- package/dist/runner/langchain/tools/web_search.d.ts +24 -0
- package/dist/runner/langchain/tools/web_search.js +42 -11
- package/dist/runner/tool-loader.d.ts +10 -0
- package/dist/runner/tool-loader.js +1 -0
- package/dist/runner/tools.d.ts +2 -2
- package/dist/runner/tools.js +1 -0
- package/dist/telemetry/index.d.ts +5 -0
- package/dist/telemetry/index.js +8 -0
- package/dist/telemetry/setup.js +10 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/tool.d.ts +5 -0
- package/dist/utils/tool.js +1 -0
- package/package.json +6 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
2
2
|
import { context, propagation, trace } from "@opentelemetry/api";
|
|
3
|
-
import {
|
|
3
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
4
4
|
import { AIMessageChunk, createAgent, ToolMessage, tool, } from "langchain";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { SUBAGENT_MODE_KEY } from "../../acp-server/adapter";
|
|
@@ -15,7 +15,7 @@ import { makeGenerateImageTool } from "./tools/generate_image";
|
|
|
15
15
|
import { SUBAGENT_TOOL_NAME } from "./tools/subagent";
|
|
16
16
|
import { hashQuery, queryToToolCallId, subagentEvents, } from "./tools/subagent-connections";
|
|
17
17
|
import { TODO_WRITE_TOOL_NAME, todoWrite } from "./tools/todo";
|
|
18
|
-
import { makeWebSearchTools } from "./tools/web_search";
|
|
18
|
+
import { makeTownWebSearchTools, makeWebSearchTools } from "./tools/web_search";
|
|
19
19
|
const _logger = createLogger("agent-runner");
|
|
20
20
|
const getWeather = tool(({ city }) => `It's always sunny in ${city}!`, {
|
|
21
21
|
name: "get_weather",
|
|
@@ -30,6 +30,7 @@ export const TOOL_REGISTRY = {
|
|
|
30
30
|
todo_write: todoWrite,
|
|
31
31
|
get_weather: getWeather,
|
|
32
32
|
web_search: () => makeWebSearchTools(),
|
|
33
|
+
town_web_search: () => makeTownWebSearchTools(),
|
|
33
34
|
filesystem: () => makeFilesystemTools(process.cwd()),
|
|
34
35
|
generate_image: () => makeGenerateImageTool(),
|
|
35
36
|
browser: () => makeBrowserTools(),
|
|
@@ -96,12 +97,9 @@ export class LangchainAgent {
|
|
|
96
97
|
// Clear the buffer after flushing
|
|
97
98
|
pendingToolCallNotifications.length = 0;
|
|
98
99
|
}
|
|
99
|
-
// Set session_id as a base attribute so all spans in this invocation include it
|
|
100
|
-
telemetry.setBaseAttributes({
|
|
101
|
-
"agent.session_id": req.sessionId,
|
|
102
|
-
});
|
|
103
100
|
const subagentUpdateQueue = [];
|
|
104
101
|
let subagentUpdateResolver = null;
|
|
102
|
+
const subagentMessagesQueue = [];
|
|
105
103
|
// Listen for subagent connection events - resolve any waiting promise immediately
|
|
106
104
|
const onSubagentConnection = (event) => {
|
|
107
105
|
_logger.info("Received subagent connection event", {
|
|
@@ -121,6 +119,15 @@ export class LangchainAgent {
|
|
|
121
119
|
}
|
|
122
120
|
};
|
|
123
121
|
subagentEvents.on("connection", onSubagentConnection);
|
|
122
|
+
// Listen for subagent messages events (for session storage)
|
|
123
|
+
const onSubagentMessages = (event) => {
|
|
124
|
+
_logger.info("Received subagent messages event", {
|
|
125
|
+
toolCallId: event.toolCallId,
|
|
126
|
+
messageCount: event.messages.length,
|
|
127
|
+
});
|
|
128
|
+
subagentMessagesQueue.push(event);
|
|
129
|
+
};
|
|
130
|
+
subagentEvents.on("messages", onSubagentMessages);
|
|
124
131
|
// Helper to get next subagent update (returns immediately if queued, otherwise waits)
|
|
125
132
|
const waitForSubagentUpdate = () => {
|
|
126
133
|
if (subagentUpdateQueue.length > 0) {
|
|
@@ -149,12 +156,34 @@ export class LangchainAgent {
|
|
|
149
156
|
},
|
|
150
157
|
};
|
|
151
158
|
}
|
|
159
|
+
// Also yield any pending messages updates
|
|
160
|
+
while (subagentMessagesQueue.length > 0) {
|
|
161
|
+
const messagesUpdate = subagentMessagesQueue.shift();
|
|
162
|
+
_logger.info("Yielding queued subagent messages update", {
|
|
163
|
+
toolCallId: messagesUpdate.toolCallId,
|
|
164
|
+
messageCount: messagesUpdate.messages.length,
|
|
165
|
+
});
|
|
166
|
+
yield {
|
|
167
|
+
sessionUpdate: "tool_call_update",
|
|
168
|
+
toolCallId: messagesUpdate.toolCallId,
|
|
169
|
+
_meta: {
|
|
170
|
+
messageId: req.messageId,
|
|
171
|
+
subagentMessages: messagesUpdate.messages,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
152
175
|
}
|
|
176
|
+
// Add agent.session_id as a base attribute so it propagates to all child spans
|
|
177
|
+
// We'll clear this in a finally block to prevent cross-contamination
|
|
178
|
+
telemetry.setBaseAttributes({
|
|
179
|
+
"agent.session_id": req.sessionId,
|
|
180
|
+
});
|
|
153
181
|
// Start telemetry span for entire invocation
|
|
154
182
|
const invocationSpan = telemetry.startSpan("agent.invoke", {
|
|
155
183
|
"agent.model": this.definition.model,
|
|
156
184
|
"agent.subagent": meta?.[SUBAGENT_MODE_KEY] === true,
|
|
157
185
|
"agent.message_id": req.messageId,
|
|
186
|
+
"agent.session_id": req.sessionId,
|
|
158
187
|
}, parentContext);
|
|
159
188
|
// Create a context with the invocation span as active
|
|
160
189
|
// This will be used when creating child spans (tool calls)
|
|
@@ -166,7 +195,29 @@ export class LangchainAgent {
|
|
|
166
195
|
sessionId: req.sessionId,
|
|
167
196
|
messageId: req.messageId,
|
|
168
197
|
});
|
|
198
|
+
// Yield the invocation span to the adapter so it can use it for parenting hook spans
|
|
199
|
+
if (invocationSpan) {
|
|
200
|
+
yield {
|
|
201
|
+
sessionUpdate: "__invocation_span",
|
|
202
|
+
invocationSpan,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Declare otelCallbacks outside try block so it's accessible in catch
|
|
206
|
+
let otelCallbacks = null;
|
|
169
207
|
try {
|
|
208
|
+
// Determine effective model early so we can detect provider for callbacks
|
|
209
|
+
// Use override model if provided (Town Hall comparison feature)
|
|
210
|
+
const effectiveModel = req.configOverrides?.model ?? this.definition.model;
|
|
211
|
+
const provider = detectProvider(effectiveModel);
|
|
212
|
+
// Create OTEL callbacks for instrumentation early so we can use them during tool wrapping
|
|
213
|
+
// Track iteration index across LLM calls in this invocation
|
|
214
|
+
const iterationIndexRef = { current: 0 };
|
|
215
|
+
otelCallbacks = makeOtelCallbacks({
|
|
216
|
+
provider,
|
|
217
|
+
model: effectiveModel,
|
|
218
|
+
parentContext: invocationContext,
|
|
219
|
+
iterationIndexRef,
|
|
220
|
+
});
|
|
170
221
|
// Track todo_write tool call IDs to suppress their tool_call notifications
|
|
171
222
|
const todoWriteToolCallIds = new Set();
|
|
172
223
|
// --------------------------------------------------------------------------
|
|
@@ -365,19 +416,34 @@ export class LangchainAgent {
|
|
|
365
416
|
: wrappedTools;
|
|
366
417
|
// Wrap tools with tracing so each tool executes within its own span context.
|
|
367
418
|
// This ensures subagent spans are children of the Task tool span.
|
|
368
|
-
|
|
419
|
+
// Pass the context getter so tools can nest under the current iteration span.
|
|
420
|
+
let finalTools = filteredTools.map((t) => wrapToolWithTracing(t, otelCallbacks?.getCurrentIterationContext ??
|
|
421
|
+
(() => invocationContext)));
|
|
422
|
+
// Apply tool overrides if provided (Town Hall comparison feature)
|
|
423
|
+
if (req.configOverrides?.tools && req.configOverrides.tools.length > 0) {
|
|
424
|
+
const allowedToolNames = new Set(req.configOverrides.tools);
|
|
425
|
+
finalTools = finalTools.filter((t) => allowedToolNames.has(t.name));
|
|
426
|
+
_logger.debug("Applied tool override filter", {
|
|
427
|
+
requested: req.configOverrides.tools,
|
|
428
|
+
filtered: finalTools.map((t) => t.name),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
369
431
|
// Create the model instance using the factory
|
|
370
432
|
// This detects the provider from the model string:
|
|
371
433
|
// - "gemini-2.0-flash" → Google Generative AI
|
|
372
434
|
// - "vertex-gemini-2.0-flash" → Vertex AI (strips prefix)
|
|
373
435
|
// - "claude-sonnet-4-5-20250929" → Anthropic
|
|
374
|
-
const model = createModelFromString(
|
|
436
|
+
const model = createModelFromString(effectiveModel);
|
|
375
437
|
const agentConfig = {
|
|
376
438
|
model,
|
|
377
439
|
tools: finalTools,
|
|
378
440
|
};
|
|
379
|
-
if (
|
|
380
|
-
|
|
441
|
+
// Use override system prompt if provided (Town Hall comparison feature)
|
|
442
|
+
const effectiveSystemPrompt = req.configOverrides?.systemPrompt !== undefined
|
|
443
|
+
? req.configOverrides.systemPrompt
|
|
444
|
+
: this.definition.systemPrompt;
|
|
445
|
+
if (effectiveSystemPrompt) {
|
|
446
|
+
agentConfig.systemPrompt = effectiveSystemPrompt;
|
|
381
447
|
}
|
|
382
448
|
// Inject system prompt with optional TodoWrite instructions
|
|
383
449
|
const hasTodoWrite = builtInNames.includes("todo_write");
|
|
@@ -385,8 +451,6 @@ export class LangchainAgent {
|
|
|
385
451
|
agentConfig.systemPrompt = `${agentConfig.systemPrompt ?? ""}\n\n${TODO_WRITE_INSTRUCTIONS}`;
|
|
386
452
|
}
|
|
387
453
|
const agent = createAgent(agentConfig);
|
|
388
|
-
// Add logging callbacks for model requests
|
|
389
|
-
const provider = detectProvider(this.definition.model);
|
|
390
454
|
// Build messages from context history if available, otherwise use just the prompt
|
|
391
455
|
let messages;
|
|
392
456
|
// Helper to convert content blocks to LangChain format
|
|
@@ -477,12 +541,6 @@ export class LangchainAgent {
|
|
|
477
541
|
},
|
|
478
542
|
];
|
|
479
543
|
}
|
|
480
|
-
// Create OTEL callbacks for instrumentation
|
|
481
|
-
const otelCallbacks = makeOtelCallbacks({
|
|
482
|
-
provider,
|
|
483
|
-
model: this.definition.model,
|
|
484
|
-
parentContext: invocationContext,
|
|
485
|
-
});
|
|
486
544
|
// Create the stream within the invocation context so AsyncLocalStorage
|
|
487
545
|
// propagates the context to all tool executions and callbacks
|
|
488
546
|
const stream = context.with(invocationContext, () => agent.stream({ messages }, {
|
|
@@ -613,6 +671,7 @@ export class LangchainAgent {
|
|
|
613
671
|
const matchingTool = finalTools.find((t) => t.name === toolCall.name);
|
|
614
672
|
let prettyName = matchingTool?.prettyName;
|
|
615
673
|
const icon = matchingTool?.icon;
|
|
674
|
+
const verbiage = matchingTool?.verbiage;
|
|
616
675
|
// For the Task tool, use the displayName (or agentName as fallback) as the prettyName
|
|
617
676
|
if (toolCall.name === SUBAGENT_TOOL_NAME &&
|
|
618
677
|
toolCall.args &&
|
|
@@ -654,6 +713,7 @@ export class LangchainAgent {
|
|
|
654
713
|
messageId: req.messageId,
|
|
655
714
|
...(prettyName ? { prettyName } : {}),
|
|
656
715
|
...(icon ? { icon } : {}),
|
|
716
|
+
...(verbiage ? { verbiage } : {}),
|
|
657
717
|
...(batchId ? { batchId } : {}),
|
|
658
718
|
},
|
|
659
719
|
});
|
|
@@ -672,6 +732,7 @@ export class LangchainAgent {
|
|
|
672
732
|
messageId: req.messageId,
|
|
673
733
|
...(prettyName ? { prettyName } : {}),
|
|
674
734
|
...(icon ? { icon } : {}),
|
|
735
|
+
...(verbiage ? { verbiage } : {}),
|
|
675
736
|
...(batchId ? { batchId } : {}),
|
|
676
737
|
},
|
|
677
738
|
});
|
|
@@ -888,12 +949,15 @@ export class LangchainAgent {
|
|
|
888
949
|
yield* yieldPendingSubagentUpdates();
|
|
889
950
|
// Now that content streaming is complete, yield all buffered tool call notifications
|
|
890
951
|
yield* flushPendingToolCalls();
|
|
891
|
-
// Clean up subagent
|
|
952
|
+
// Clean up subagent event listeners
|
|
892
953
|
subagentEvents.off("connection", onSubagentConnection);
|
|
954
|
+
subagentEvents.off("messages", onSubagentMessages);
|
|
893
955
|
// Cancel any pending wait
|
|
894
956
|
if (subagentUpdateResolver) {
|
|
895
957
|
subagentUpdateResolver = null;
|
|
896
958
|
}
|
|
959
|
+
// Clean up any remaining iteration span
|
|
960
|
+
otelCallbacks?.cleanup();
|
|
897
961
|
// Log successful completion
|
|
898
962
|
telemetry.log("info", "Agent invocation completed", {
|
|
899
963
|
sessionId: req.sessionId,
|
|
@@ -907,8 +971,11 @@ export class LangchainAgent {
|
|
|
907
971
|
};
|
|
908
972
|
}
|
|
909
973
|
catch (error) {
|
|
910
|
-
// Clean up subagent
|
|
974
|
+
// Clean up subagent event listeners on error
|
|
911
975
|
subagentEvents.off("connection", onSubagentConnection);
|
|
976
|
+
subagentEvents.off("messages", onSubagentMessages);
|
|
977
|
+
// Clean up any remaining iteration span
|
|
978
|
+
otelCallbacks?.cleanup();
|
|
912
979
|
// Log error and end span with error status
|
|
913
980
|
telemetry.log("error", "Agent invocation failed", {
|
|
914
981
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -917,6 +984,10 @@ export class LangchainAgent {
|
|
|
917
984
|
telemetry.endSpan(invocationSpan, error instanceof Error ? error : new Error(String(error)));
|
|
918
985
|
throw error;
|
|
919
986
|
}
|
|
987
|
+
finally {
|
|
988
|
+
// Clear agent.session_id from base attributes to prevent cross-contamination
|
|
989
|
+
telemetry.clearBaseAttribute("agent.session_id");
|
|
990
|
+
}
|
|
920
991
|
}
|
|
921
992
|
}
|
|
922
993
|
const modelRequestSchema = z.object({
|
|
@@ -928,17 +999,17 @@ const makeMcpToolsClient = (mcpConfigs) => {
|
|
|
928
999
|
const mcpServers = mcpConfigs?.map((config) => {
|
|
929
1000
|
if (typeof config === "string") {
|
|
930
1001
|
// String configs use the centralized MCP proxy with auth
|
|
931
|
-
const
|
|
932
|
-
if (!
|
|
933
|
-
throw new Error("Not logged in. Run 'town login'
|
|
1002
|
+
const shedAuth = getShedAuth();
|
|
1003
|
+
if (!shedAuth) {
|
|
1004
|
+
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY to use cloud MCP servers.");
|
|
934
1005
|
}
|
|
935
|
-
const proxyUrl = process.env.MCP_PROXY_URL ?? `${
|
|
1006
|
+
const proxyUrl = process.env.MCP_PROXY_URL ?? `${shedAuth.shedUrl}/mcp_proxy`;
|
|
936
1007
|
return [
|
|
937
1008
|
config,
|
|
938
1009
|
{
|
|
939
1010
|
url: `${proxyUrl}?server=${config}`,
|
|
940
1011
|
headers: {
|
|
941
|
-
Authorization: `Bearer ${
|
|
1012
|
+
Authorization: `Bearer ${shedAuth.accessToken}`,
|
|
942
1013
|
},
|
|
943
1014
|
},
|
|
944
1015
|
];
|
|
@@ -1031,18 +1102,22 @@ export { makeSubagentsTool } from "./tools/subagent.js";
|
|
|
1031
1102
|
* This ensures the tool executes within its own span context,
|
|
1032
1103
|
* so any child operations (like subagent spawning) become children
|
|
1033
1104
|
* of the tool span rather than the parent invocation span.
|
|
1105
|
+
* @param originalTool The tool to wrap
|
|
1106
|
+
* @param getIterationContext Function that returns the current iteration context
|
|
1034
1107
|
*/
|
|
1035
|
-
function wrapToolWithTracing(originalTool) {
|
|
1108
|
+
function wrapToolWithTracing(originalTool, getIterationContext) {
|
|
1036
1109
|
const wrappedFunc = async (input) => {
|
|
1037
1110
|
const toolInputJson = JSON.stringify(input);
|
|
1111
|
+
// Get the current iteration context so the tool span is created as a child
|
|
1112
|
+
const iterationContext = getIterationContext();
|
|
1038
1113
|
const toolSpan = telemetry.startSpan("agent.tool_call", {
|
|
1039
1114
|
"tool.name": originalTool.name,
|
|
1040
1115
|
"tool.input": toolInputJson,
|
|
1041
|
-
});
|
|
1116
|
+
}, iterationContext);
|
|
1042
1117
|
// Create a context with the tool span as active
|
|
1043
1118
|
const spanContext = toolSpan
|
|
1044
|
-
? trace.setSpan(
|
|
1045
|
-
:
|
|
1119
|
+
? trace.setSpan(iterationContext, toolSpan)
|
|
1120
|
+
: iterationContext;
|
|
1046
1121
|
try {
|
|
1047
1122
|
// Execute within the tool span's context
|
|
1048
1123
|
const result = await context.with(spanContext, () => originalTool.invoke(input));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
2
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
3
3
|
import { ChatVertexAI } from "@langchain/google-vertexai";
|
|
4
|
-
import {
|
|
4
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
5
5
|
import { createLogger } from "../../logger.js";
|
|
6
6
|
const logger = createLogger("model-factory");
|
|
7
7
|
/**
|
|
@@ -24,17 +24,14 @@ export function createModelFromString(modelString) {
|
|
|
24
24
|
// Check for town- prefix for proxied models via shed
|
|
25
25
|
if (modelString.startsWith("town-")) {
|
|
26
26
|
const actualModel = modelString.slice(5); // strip "town-"
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
throw new Error("Not logged in. Run 'town login'
|
|
27
|
+
const shedAuth = getShedAuth();
|
|
28
|
+
if (!shedAuth) {
|
|
29
|
+
throw new Error("Not logged in. Run 'town login' or set SHED_API_KEY.");
|
|
30
30
|
}
|
|
31
|
-
const shedUrl = credentials.shed_url ??
|
|
32
|
-
process.env.TOWN_SHED_URL ??
|
|
33
|
-
"http://localhost:3000";
|
|
34
31
|
return new ChatAnthropic({
|
|
35
32
|
model: actualModel,
|
|
36
|
-
anthropicApiUrl: `${shedUrl}/api/anthropic`,
|
|
37
|
-
apiKey:
|
|
33
|
+
anthropicApiUrl: `${shedAuth.shedUrl}/api/anthropic`,
|
|
34
|
+
apiKey: shedAuth.accessToken,
|
|
38
35
|
});
|
|
39
36
|
}
|
|
40
37
|
// Check if the model string uses provider prefix format
|
|
@@ -4,6 +4,9 @@ export interface OtelCallbackOptions {
|
|
|
4
4
|
provider: string;
|
|
5
5
|
model: string;
|
|
6
6
|
parentContext: Context;
|
|
7
|
+
iterationIndexRef: {
|
|
8
|
+
current: number;
|
|
9
|
+
};
|
|
7
10
|
}
|
|
8
11
|
/**
|
|
9
12
|
* Creates OpenTelemetry callback handlers for LangChain LLM calls.
|
|
@@ -15,4 +18,7 @@ export interface OtelCallbackOptions {
|
|
|
15
18
|
* @param opts.parentContext - The parent OTEL context to create child spans under
|
|
16
19
|
* @returns CallbackHandlerMethods object that can be passed to LangChain
|
|
17
20
|
*/
|
|
18
|
-
export declare function makeOtelCallbacks(opts: OtelCallbackOptions): CallbackHandlerMethods
|
|
21
|
+
export declare function makeOtelCallbacks(opts: OtelCallbackOptions): CallbackHandlerMethods & {
|
|
22
|
+
cleanup: () => void;
|
|
23
|
+
getCurrentIterationContext: () => Context;
|
|
24
|
+
};
|
|
@@ -5,11 +5,15 @@ import { telemetry } from "../../telemetry/index.js";
|
|
|
5
5
|
* Creates spans for each LLM request to track model invocations and token usage.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* Map to store active spans by their LangChain run ID
|
|
8
|
+
* Map to store active chat spans by their LangChain run ID
|
|
9
9
|
*
|
|
10
10
|
* There's a memory leak opportunity here, but we are OK with that for now.
|
|
11
11
|
*/
|
|
12
12
|
const spansByRunId = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Map to store active iteration spans by their LangChain run ID
|
|
15
|
+
*/
|
|
16
|
+
const iterationSpansByRunId = new Map();
|
|
13
17
|
/**
|
|
14
18
|
* Serializes LangChain messages to a JSON string for span attributes.
|
|
15
19
|
* Extracts role and content from each message.
|
|
@@ -88,19 +92,41 @@ function serializeOutput(output) {
|
|
|
88
92
|
* @returns CallbackHandlerMethods object that can be passed to LangChain
|
|
89
93
|
*/
|
|
90
94
|
export function makeOtelCallbacks(opts) {
|
|
95
|
+
// Track the iteration span for this specific invocation
|
|
96
|
+
let localIterationSpan = null;
|
|
97
|
+
let currentIterationContext = opts.parentContext;
|
|
91
98
|
return {
|
|
92
99
|
/**
|
|
93
100
|
* Called when a chat model/LLM request starts.
|
|
94
|
-
* Creates
|
|
101
|
+
* Creates iteration span first, then chat span as its child.
|
|
95
102
|
*/
|
|
96
103
|
async handleChatModelStart(_llm, messages, runId, _parentRunId, _extraParams, tags, _metadata) {
|
|
97
104
|
// Extract system prompt and serialize messages
|
|
98
105
|
const systemPrompt = extractSystemPrompt(messages);
|
|
99
106
|
const serializedMessages = serializeMessages(messages);
|
|
100
|
-
//
|
|
107
|
+
// Close previous iteration span if it exists (tools from previous iteration are done)
|
|
108
|
+
if (localIterationSpan) {
|
|
109
|
+
telemetry.endSpan(localIterationSpan);
|
|
110
|
+
localIterationSpan = null;
|
|
111
|
+
}
|
|
112
|
+
// Create iteration span within the parent context (invocation span)
|
|
113
|
+
const iterationIndex = opts.iterationIndexRef.current;
|
|
114
|
+
const iterationSpan = context.with(opts.parentContext, () => telemetry.startSpan("agent.iteration", {
|
|
115
|
+
"agent.iteration.index": iterationIndex,
|
|
116
|
+
"langchain.run_id": runId,
|
|
117
|
+
}));
|
|
118
|
+
// Track as current iteration span (will be closed when next iteration starts or invocation ends)
|
|
119
|
+
localIterationSpan = iterationSpan;
|
|
120
|
+
// Create context with iteration span as active
|
|
121
|
+
const iterationContext = iterationSpan
|
|
122
|
+
? trace.setSpan(opts.parentContext, iterationSpan)
|
|
123
|
+
: opts.parentContext;
|
|
124
|
+
// Store current iteration context so tools can use it
|
|
125
|
+
currentIterationContext = iterationContext;
|
|
126
|
+
// Create chat span as child of iteration span
|
|
101
127
|
// Following OpenTelemetry GenAI semantic conventions:
|
|
102
128
|
// https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
|
|
103
|
-
const
|
|
129
|
+
const chatSpan = context.with(iterationContext, () => telemetry.startSpan(`chat ${opts.model}`, {
|
|
104
130
|
"gen_ai.operation.name": "chat",
|
|
105
131
|
"gen_ai.provider.name": opts.provider,
|
|
106
132
|
"gen_ai.request.model": opts.model,
|
|
@@ -114,10 +140,14 @@ export function makeOtelCallbacks(opts) {
|
|
|
114
140
|
? { "langchain.tags": tags.join(",") }
|
|
115
141
|
: {}),
|
|
116
142
|
}));
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
// Store both spans
|
|
144
|
+
if (iterationSpan) {
|
|
145
|
+
iterationSpansByRunId.set(runId, iterationSpan);
|
|
146
|
+
}
|
|
147
|
+
if (chatSpan) {
|
|
148
|
+
spansByRunId.set(runId, chatSpan);
|
|
119
149
|
// Emit log for LLM request with trace context
|
|
120
|
-
const spanContext =
|
|
150
|
+
const spanContext = chatSpan.spanContext();
|
|
121
151
|
telemetry.log("info", "LLM Request", {
|
|
122
152
|
"gen_ai.operation.name": "chat",
|
|
123
153
|
"gen_ai.provider.name": opts.provider,
|
|
@@ -129,14 +159,16 @@ export function makeOtelCallbacks(opts) {
|
|
|
129
159
|
span_id: spanContext.spanId,
|
|
130
160
|
});
|
|
131
161
|
}
|
|
162
|
+
// Increment iteration index for next LLM call
|
|
163
|
+
opts.iterationIndexRef.current++;
|
|
132
164
|
},
|
|
133
165
|
/**
|
|
134
166
|
* Called when an LLM request completes successfully.
|
|
135
|
-
* Records token usage and ends
|
|
167
|
+
* Records token usage and ends both chat and iteration spans.
|
|
136
168
|
*/
|
|
137
169
|
async handleLLMEnd(output, runId) {
|
|
138
|
-
const
|
|
139
|
-
if (!
|
|
170
|
+
const chatSpan = spansByRunId.get(runId);
|
|
171
|
+
if (!chatSpan)
|
|
140
172
|
return;
|
|
141
173
|
// Extract token usage from LLM output
|
|
142
174
|
// The structure varies by provider but LangChain normalizes it
|
|
@@ -147,15 +179,15 @@ export function makeOtelCallbacks(opts) {
|
|
|
147
179
|
(tokenUsage.totalTokens != null
|
|
148
180
|
? tokenUsage.totalTokens - inputTokens
|
|
149
181
|
: 0);
|
|
150
|
-
telemetry.recordTokenUsage(inputTokens, outputTokens,
|
|
182
|
+
telemetry.recordTokenUsage(inputTokens, outputTokens, chatSpan);
|
|
151
183
|
}
|
|
152
184
|
// Serialize output and attach to span
|
|
153
185
|
const serializedOutput = serializeOutput(output);
|
|
154
|
-
telemetry.setSpanAttributes(
|
|
186
|
+
telemetry.setSpanAttributes(chatSpan, {
|
|
155
187
|
"gen_ai.output.messages": serializedOutput,
|
|
156
188
|
});
|
|
157
189
|
// Emit log for LLM response with trace context
|
|
158
|
-
const spanContext =
|
|
190
|
+
const spanContext = chatSpan.spanContext();
|
|
159
191
|
telemetry.log("info", "LLM Response", {
|
|
160
192
|
"gen_ai.operation.name": "chat",
|
|
161
193
|
"gen_ai.output.messages": serializedOutput,
|
|
@@ -171,19 +203,47 @@ export function makeOtelCallbacks(opts) {
|
|
|
171
203
|
trace_id: spanContext.traceId,
|
|
172
204
|
span_id: spanContext.spanId,
|
|
173
205
|
});
|
|
174
|
-
|
|
206
|
+
// End chat span
|
|
207
|
+
telemetry.endSpan(chatSpan);
|
|
175
208
|
spansByRunId.delete(runId);
|
|
209
|
+
// DON'T close iteration span here - tools will execute after this
|
|
210
|
+
// The iteration span will be closed when:
|
|
211
|
+
// 1. Next iteration starts (in handleChatModelStart)
|
|
212
|
+
// 2. Invocation ends (caller must close remaining span)
|
|
213
|
+
iterationSpansByRunId.delete(runId);
|
|
176
214
|
},
|
|
177
215
|
/**
|
|
178
216
|
* Called when an LLM request fails with an error.
|
|
179
|
-
* Records the error and ends
|
|
217
|
+
* Records the error and ends both chat and iteration spans.
|
|
180
218
|
*/
|
|
181
219
|
async handleLLMError(error, runId) {
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
220
|
+
const chatSpan = spansByRunId.get(runId);
|
|
221
|
+
if (chatSpan) {
|
|
222
|
+
telemetry.endSpan(chatSpan, error instanceof Error ? error : new Error(String(error)));
|
|
223
|
+
spansByRunId.delete(runId);
|
|
224
|
+
}
|
|
225
|
+
// Close iteration span on error
|
|
226
|
+
if (localIterationSpan) {
|
|
227
|
+
telemetry.endSpan(localIterationSpan, error instanceof Error ? error : new Error(String(error)));
|
|
228
|
+
localIterationSpan = null;
|
|
229
|
+
}
|
|
230
|
+
iterationSpansByRunId.delete(runId);
|
|
231
|
+
},
|
|
232
|
+
/**
|
|
233
|
+
* Get the current iteration context for tool span creation
|
|
234
|
+
*/
|
|
235
|
+
getCurrentIterationContext() {
|
|
236
|
+
return currentIterationContext;
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* Cleanup function to close any remaining iteration span
|
|
240
|
+
* Should be called when invocation completes
|
|
241
|
+
*/
|
|
242
|
+
cleanup() {
|
|
243
|
+
if (localIterationSpan) {
|
|
244
|
+
telemetry.endSpan(localIterationSpan);
|
|
245
|
+
localIterationSpan = null;
|
|
246
|
+
}
|
|
187
247
|
},
|
|
188
248
|
};
|
|
189
249
|
}
|
|
@@ -190,6 +190,11 @@ export function makeFilesystemTools(workingDirectory) {
|
|
|
190
190
|
});
|
|
191
191
|
grep.prettyName = "Codebase Search";
|
|
192
192
|
grep.icon = "Search";
|
|
193
|
+
grep.verbiage = {
|
|
194
|
+
active: "Searching for {query}",
|
|
195
|
+
past: "Searched for {query}",
|
|
196
|
+
paramKey: "pattern",
|
|
197
|
+
};
|
|
193
198
|
const read = tool(async ({ file_path, offset, limit }) => {
|
|
194
199
|
await ensureSandbox(resolvedWd);
|
|
195
200
|
assertAbsolutePath(file_path, "file_path");
|
|
@@ -234,6 +239,11 @@ export function makeFilesystemTools(workingDirectory) {
|
|
|
234
239
|
});
|
|
235
240
|
read.prettyName = "Read File";
|
|
236
241
|
read.icon = "FileText";
|
|
242
|
+
read.verbiage = {
|
|
243
|
+
active: "Reading {file}",
|
|
244
|
+
past: "Read {file}",
|
|
245
|
+
paramKey: "file_path",
|
|
246
|
+
};
|
|
237
247
|
const write = tool(async ({ file_path, content }) => {
|
|
238
248
|
await ensureSandbox(resolvedWd);
|
|
239
249
|
assertAbsolutePath(file_path, "file_path");
|
|
@@ -263,5 +273,10 @@ export function makeFilesystemTools(workingDirectory) {
|
|
|
263
273
|
});
|
|
264
274
|
write.prettyName = "Write File";
|
|
265
275
|
write.icon = "Edit";
|
|
276
|
+
write.verbiage = {
|
|
277
|
+
active: "Writing {file}",
|
|
278
|
+
past: "Wrote {file}",
|
|
279
|
+
paramKey: "file_path",
|
|
280
|
+
};
|
|
266
281
|
return [grep, read, write];
|
|
267
282
|
}
|
|
@@ -7,6 +7,35 @@ export interface SubagentConnectionInfo {
|
|
|
7
7
|
port: number;
|
|
8
8
|
sessionId: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Sub-agent tool call tracked during streaming
|
|
12
|
+
*/
|
|
13
|
+
export interface SubagentToolCall {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
prettyName?: string | undefined;
|
|
17
|
+
icon?: string | undefined;
|
|
18
|
+
status: "pending" | "in_progress" | "completed" | "failed";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Content block for sub-agent messages
|
|
22
|
+
*/
|
|
23
|
+
export type SubagentContentBlock = {
|
|
24
|
+
type: "text";
|
|
25
|
+
text: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: "tool_call";
|
|
28
|
+
toolCall: SubagentToolCall;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Sub-agent message accumulated during streaming
|
|
32
|
+
*/
|
|
33
|
+
export interface SubagentMessage {
|
|
34
|
+
id: string;
|
|
35
|
+
content: string;
|
|
36
|
+
contentBlocks: SubagentContentBlock[];
|
|
37
|
+
toolCalls: SubagentToolCall[];
|
|
38
|
+
}
|
|
10
39
|
/**
|
|
11
40
|
* Event emitter for subagent connection events.
|
|
12
41
|
* The runner listens to these events and emits tool_call_update.
|
|
@@ -26,3 +55,8 @@ export declare function hashQuery(query: string): string;
|
|
|
26
55
|
* Emits an event that the runner can listen to.
|
|
27
56
|
*/
|
|
28
57
|
export declare function emitSubagentConnection(queryHash: string, connectionInfo: SubagentConnectionInfo): void;
|
|
58
|
+
/**
|
|
59
|
+
* Called by the subagent tool when it completes with accumulated messages.
|
|
60
|
+
* Emits an event with the messages for session storage.
|
|
61
|
+
*/
|
|
62
|
+
export declare function emitSubagentMessages(queryHash: string, messages: SubagentMessage[]): void;
|
|
@@ -12,6 +12,11 @@ export const subagentEvents = new EventEmitter();
|
|
|
12
12
|
* Set by the runner when it sees a subagent tool_call.
|
|
13
13
|
*/
|
|
14
14
|
export const queryToToolCallId = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Maps query hash to resolved toolCallId (preserved after connection event).
|
|
17
|
+
* Used to correlate messages when the tool completes.
|
|
18
|
+
*/
|
|
19
|
+
const queryToResolvedToolCallId = new Map();
|
|
15
20
|
/**
|
|
16
21
|
* Generate a hash from the query string for correlation.
|
|
17
22
|
*/
|
|
@@ -46,7 +51,8 @@ export function emitSubagentConnection(queryHash, connectionInfo) {
|
|
|
46
51
|
toolCallId,
|
|
47
52
|
...connectionInfo,
|
|
48
53
|
});
|
|
49
|
-
//
|
|
54
|
+
// Preserve the toolCallId for message emission, but remove from pending lookup
|
|
55
|
+
queryToResolvedToolCallId.set(queryHash, toolCallId);
|
|
50
56
|
queryToToolCallId.delete(queryHash);
|
|
51
57
|
}
|
|
52
58
|
else {
|
|
@@ -56,3 +62,28 @@ export function emitSubagentConnection(queryHash, connectionInfo) {
|
|
|
56
62
|
});
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Called by the subagent tool when it completes with accumulated messages.
|
|
67
|
+
* Emits an event with the messages for session storage.
|
|
68
|
+
*/
|
|
69
|
+
export function emitSubagentMessages(queryHash, messages) {
|
|
70
|
+
const toolCallId = queryToResolvedToolCallId.get(queryHash);
|
|
71
|
+
if (toolCallId) {
|
|
72
|
+
logger.info("Emitting subagent messages for storage", {
|
|
73
|
+
queryHash,
|
|
74
|
+
toolCallId,
|
|
75
|
+
messageCount: messages.length,
|
|
76
|
+
});
|
|
77
|
+
subagentEvents.emit("messages", {
|
|
78
|
+
toolCallId,
|
|
79
|
+
messages,
|
|
80
|
+
});
|
|
81
|
+
// Clean up the resolved mapping
|
|
82
|
+
queryToResolvedToolCallId.delete(queryHash);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
logger.warn("No resolved toolCallId for messages emission", {
|
|
86
|
+
queryHash,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|