Skip to content

Commit 611a655

Browse files
committed
feat: implement tool call tracking for API-based tools
1 parent be944b5 commit 611a655

3 files changed

Lines changed: 87 additions & 55 deletions

File tree

agent/middleware/apiBasedTools.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { ToolMessage } from "@langchain/core/messages";
22
import { createMiddleware } from "langchain";
33
import { logger } from "adminforth";
4-
import type { ApiBasedTool } from "../../apiBasedTools.js";
4+
import { type ApiBasedTool } from "../../apiBasedTools.js";
5+
import {
6+
createToolCallTracker,
7+
type ToolCallEventSink,
8+
} from "../toolCallEvents.js";
59
import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
610
import { createApiTool } from "../tools/apiTool.js";
711

@@ -67,37 +71,54 @@ export function createApiBasedToolsMiddleware(
6771
async wrapToolCall(request, handler) {
6872
const startedAt = Date.now();
6973
const toolInput = JSON.stringify(request.toolCall.args ?? {});
74+
const { emitToolCallEvent } = request.runtime.context as {
75+
emitToolCallEvent: ToolCallEventSink;
76+
};
77+
const toolCallTracker = createToolCallTracker({
78+
emit: emitToolCallEvent,
79+
toolCallId: request.toolCall.id,
80+
toolName: request.toolCall.name,
81+
input: (request.toolCall.args ?? {}) as Record<string, unknown>,
82+
startedAt,
83+
});
84+
toolCallTracker.start();
7085
logger.info(
7186
`Invoking tool "${request.toolCall.name}" with input: ${toolInput}`,
7287
);
7388

7489
try {
75-
if (request.tool) {
76-
return await handler(request);
77-
}
90+
let result;
7891

79-
const enabledApiToolNames = getEnabledApiToolNames(request.state.messages);
92+
if (request.tool) {
93+
result = await handler(request);
94+
} else {
95+
const enabledApiToolNames = getEnabledApiToolNames(request.state.messages);
8096

81-
if (enabledApiToolNames.has(request.toolCall.name)) {
82-
return await handler({
83-
...request,
84-
tool: dynamicTools[request.toolCall.name],
85-
});
97+
if (enabledApiToolNames.has(request.toolCall.name)) {
98+
result = await handler({
99+
...request,
100+
tool: dynamicTools[request.toolCall.name],
101+
});
102+
} else {
103+
result = new ToolMessage({
104+
content: `Tool "${request.toolCall.name}" is not loaded. Call fetch_tool_schema first.`,
105+
tool_call_id: request.toolCall.id ?? "",
106+
name: request.toolCall.name,
107+
status: "error",
108+
});
109+
}
86110
}
87111

88-
return new ToolMessage({
89-
content: `Tool "${request.toolCall.name}" is not loaded. Call fetch_tool_schema first.`,
90-
tool_call_id: request.toolCall.id ?? "",
91-
name: request.toolCall.name,
92-
status: "error",
93-
});
112+
toolCallTracker.finishSuccess(result);
113+
return result;
94114
} catch (error) {
95115
const errorDetails =
96116
error instanceof Error ? error.stack ?? error.message : String(error);
97117

98118
logger.error(
99119
`Tool "${request.toolCall.name}" failed after ${Date.now() - startedAt}ms with input: ${toolInput}\n${errorDetails}`,
100120
);
121+
toolCallTracker.finishError(error);
101122
throw error;
102123
} finally {
103124
logger.info(

agent/toolCallEvents.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { randomUUID } from "crypto";
2+
import YAML from "yaml";
3+
import { serializeUnknownError } from "../apiBasedTools.js";
4+
15
export type ToolCallEvent =
26
| {
37
toolCallId: string;
@@ -15,3 +19,45 @@ export type ToolCallEvent =
1519
};
1620

1721
export type ToolCallEventSink = (event: ToolCallEvent) => void;
22+
23+
export function createToolCallTracker(params: {
24+
emit: ToolCallEventSink;
25+
toolCallId?: string;
26+
toolName: string;
27+
input?: Record<string, unknown>;
28+
startedAt?: number;
29+
}) {
30+
const toolCallId = params.toolCallId ?? randomUUID();
31+
const startedAt = params.startedAt ?? Date.now();
32+
33+
return {
34+
start() {
35+
params.emit({
36+
toolCallId,
37+
toolName: params.toolName,
38+
phase: "start",
39+
input: YAML.stringify(params.input ?? {}),
40+
});
41+
},
42+
finishSuccess(output: unknown) {
43+
params.emit({
44+
toolCallId,
45+
toolName: params.toolName,
46+
phase: "end",
47+
durationMs: Date.now() - startedAt,
48+
output: YAML.stringify(output).trimEnd(),
49+
error: null,
50+
});
51+
},
52+
finishError(error: unknown) {
53+
params.emit({
54+
toolCallId,
55+
toolName: params.toolName,
56+
phase: "end",
57+
durationMs: Date.now() - startedAt,
58+
output: null,
59+
error: YAML.stringify(serializeUnknownError(error)).trimEnd(),
60+
});
61+
},
62+
};
63+
}

agent/tools/apiTool.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { tool } from "langchain";
2-
import { randomUUID } from "crypto";
3-
import YAML from "yaml";
42
import type { ApiBasedTool } from "../../apiBasedTools.js";
5-
import { serializeUnknownError } from "../../apiBasedTools.js";
63

74
const emptyToolSchema = {
85
type: "object",
@@ -52,43 +49,11 @@ export function createApiTool(toolName: string, apiBasedTool: ApiBasedTool) {
5249
return tool(
5350
async (input, runtime) => {
5451
const normalizedInput = (input ?? {}) as Record<string, unknown>;
55-
const toolCallId = randomUUID();
56-
const startedAt = Date.now();
57-
runtime.context.emitToolCallEvent({
58-
toolCallId,
59-
toolName,
60-
phase: "start",
61-
input: YAML.stringify(normalizedInput),
52+
return apiBasedTool.call({
53+
adminUser: runtime.context.adminUser,
54+
inputs: normalizedInput,
55+
userTimeZone: runtime.context.userTimeZone,
6256
});
63-
64-
try {
65-
const output = await apiBasedTool.call({
66-
adminUser: runtime.context.adminUser,
67-
inputs: normalizedInput,
68-
userTimeZone: runtime.context.userTimeZone,
69-
});
70-
71-
runtime.context.emitToolCallEvent({
72-
toolCallId,
73-
toolName,
74-
phase: "end",
75-
durationMs: Date.now() - startedAt,
76-
output,
77-
error: null,
78-
});
79-
80-
return output;
81-
} catch (error) {
82-
runtime.context.emitToolCallEvent({
83-
toolCallId,
84-
toolName,
85-
phase: "end",
86-
durationMs: Date.now() - startedAt,
87-
output: null,
88-
error: YAML.stringify(serializeUnknownError(error)),
89-
});
90-
throw error;
91-
}
9257
},
9358
{
9459
name: toolName,

0 commit comments

Comments
 (0)