Skip to content

Commit 44dba2b

Browse files
committed
2 parents 6cd1503 + 67e14dc commit 44dba2b

32 files changed

Lines changed: 1255 additions & 584 deletions

agent/middleware/apiBasedTools.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
} from "../toolCallEvents.js";
1212
import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
1313
import { createApiTool } from "../tools/apiTool.js";
14+
import type { AgentEventEmitter } from "../../agentEvents.js";
15+
import type { SequenceDebugCollector } from "./sequenceDebug.js";
1416

1517
function getEnabledApiToolNames(messages: unknown[]) {
1618
const enabledToolNames = new Set<string>();
@@ -80,11 +82,19 @@ export function createApiBasedToolsMiddleware(
8082
async wrapToolCall(request, handler) {
8183
const startedAt = Date.now();
8284
const toolInput = JSON.stringify(request.toolCall.args ?? {});
83-
const { adminUser, emitToolCallEvent, userTimeZone } = request.runtime.context as {
85+
const { adminUser, emit, sequenceDebugSink, userTimeZone } = request.runtime.context as {
8486
adminUser: AdminUser;
85-
emitToolCallEvent: ToolCallEventSink;
87+
emit?: AgentEventEmitter;
88+
sequenceDebugSink: SequenceDebugCollector;
8689
userTimeZone: string;
8790
};
91+
const emitToolCall: ToolCallEventSink = (event) => {
92+
sequenceDebugSink.handleToolCallEvent(event);
93+
void emit?.({
94+
type: "tool-call",
95+
data: event,
96+
});
97+
};
8898
const toolArgs = (request.toolCall.args ?? {}) as Record<string, unknown>;
8999
let toolInfo: string | undefined;
90100

@@ -102,7 +112,7 @@ export function createApiBasedToolsMiddleware(
102112
});
103113
}
104114
const toolCallTracker = createToolCallTracker({
105-
emit: emitToolCallEvent,
115+
emit: emitToolCall,
106116
toolCallId: request.toolCall.id,
107117
toolName: request.toolCall.name,
108118
toolInfo,

agent/middleware/sequenceDebug.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export type SequenceDebug = {
2626
reasoningTokens: number;
2727
text: string;
2828
textTokens: number;
29+
uncachedInputTokens: number;
30+
cachedInputTokens: number;
31+
outputTokens: number;
2932
cachedTokens: number;
3033
responseId: string | null;
3134
toolCalls: SequenceDebugToolCall[];
@@ -45,13 +48,17 @@ type SequenceDebugModelCall = {
4548
reasoningTokens: number;
4649
text: string;
4750
textTokens: number;
51+
uncachedInputTokens: number;
52+
cachedInputTokens: number;
53+
outputTokens: number;
4854
cachedTokens: number;
4955
responseId: string | null;
5056
resultType: SequenceDebugResultType;
5157
};
5258

5359
type OpenAiUsageMetadata = {
5460
input_tokens?: number;
61+
output_tokens?: number;
5562
input_token_details?: {
5663
cache_read?: number;
5764
};
@@ -82,6 +89,9 @@ function createPendingSequenceDebug(sequenceId: number): PendingSequenceDebug {
8289
reasoningTokens: 0,
8390
text: "",
8491
textTokens: 0,
92+
uncachedInputTokens: 0,
93+
cachedInputTokens: 0,
94+
outputTokens: 0,
8595
cachedTokens: 0,
8696
responseId: null,
8797
toolCalls: [],
@@ -112,6 +122,9 @@ function finalizeSequenceDebug(sequence: PendingSequenceDebug): SequenceDebug {
112122
reasoningTokens: sequence.reasoningTokens,
113123
text: sequence.text,
114124
textTokens: sequence.textTokens,
125+
uncachedInputTokens: sequence.uncachedInputTokens,
126+
cachedInputTokens: sequence.cachedInputTokens,
127+
outputTokens: sequence.outputTokens,
115128
cachedTokens: sequence.cachedTokens,
116129
responseId: sequence.responseId,
117130
toolCalls: sequence.toolCalls.map(({ completed: _completed, ...toolCall }) => toolCall),
@@ -133,6 +146,21 @@ function getDebugModelName(model: SequenceDebugPromptModel) {
133146
return typeof model.getName === "function" ? model.getName() : undefined;
134147
}
135148

149+
function getDebugToolName(tool: unknown) {
150+
if (!tool || typeof tool !== "object") {
151+
return null;
152+
}
153+
154+
const name = (tool as { name?: unknown }).name;
155+
return typeof name === "string" ? name : null;
156+
}
157+
158+
function formatToolsForDebug(tools: unknown[]) {
159+
return tools.map((tool) => ({
160+
name: getDebugToolName(tool),
161+
}));
162+
}
163+
136164
function stringifyPromptForDebug(params: {
137165
model: SequenceDebugPromptModel;
138166
systemMessage: { text: string };
@@ -160,7 +188,7 @@ function stringifyPromptForDebug(params: {
160188
},
161189
systemMessage,
162190
messages,
163-
...(tools.length > 0 ? { tools } : {}),
191+
...(tools.length > 0 ? { tools: formatToolsForDebug(tools) } : {}),
164192
...(toolChoice !== undefined ? { toolChoice } : {}),
165193
...(modelSettings ? { modelSettings } : {}),
166194
...(invocationParams ? { invocationParams } : {}),
@@ -219,6 +247,9 @@ async function countTokens(model: unknown, content: string) {
219247
}
220248

221249
function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCall {
250+
const usageMetadata = message.usage_metadata as OpenAiUsageMetadata | undefined;
251+
const promptTokens = usageMetadata?.input_tokens ?? 0;
252+
const cachedInputTokens = usageMetadata?.input_token_details?.cache_read ?? 0;
222253
const blocks = getMessageBlocks(message);
223254
const reasoning = blocks
224255
.filter((block: any) => block?.type === "reasoning")
@@ -230,16 +261,15 @@ function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCal
230261
.join("");
231262

232263
return {
233-
promptTokens:
234-
(message.usage_metadata as OpenAiUsageMetadata | undefined)?.input_tokens ??
235-
0,
264+
promptTokens,
236265
reasoning,
237266
reasoningTokens: 0,
238267
text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
239268
textTokens: 0,
240-
cachedTokens:
241-
(message.usage_metadata as OpenAiUsageMetadata | undefined)
242-
?.input_token_details?.cache_read ?? 0,
269+
uncachedInputTokens: Math.max(promptTokens - cachedInputTokens, 0),
270+
cachedInputTokens,
271+
outputTokens: usageMetadata?.output_tokens ?? 0,
272+
cachedTokens: cachedInputTokens,
243273
responseId:
244274
(message.response_metadata as OpenAiResponseMetadata | undefined)?.id ??
245275
null,
@@ -290,6 +320,9 @@ export function createSequenceDebugCollector(): SequenceDebugCollector {
290320
sequenceDebug.reasoningTokens = params.reasoningTokens;
291321
sequenceDebug.text = params.text;
292322
sequenceDebug.textTokens = params.textTokens;
323+
sequenceDebug.uncachedInputTokens = params.uncachedInputTokens;
324+
sequenceDebug.cachedInputTokens = params.cachedInputTokens;
325+
sequenceDebug.outputTokens = params.outputTokens;
293326
sequenceDebug.cachedTokens = params.cachedTokens;
294327
sequenceDebug.responseId = params.responseId;
295328
sequenceDebug.resultType = params.resultType;
@@ -387,6 +420,8 @@ export function createSequenceDebugMiddleware(
387420
promptTokens,
388421
reasoningTokens,
389422
textTokens,
423+
uncachedInputTokens: debug.promptTokens ? debug.uncachedInputTokens : promptTokens,
424+
outputTokens: debug.outputTokens || reasoningTokens + textTokens,
390425
});
391426
return response;
392427
},

agent/models/AgentModeResolver.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { PluginOptions } from "../../types.js";
2+
3+
export class AgentModeResolver {
4+
constructor(private readonly options: PluginOptions) {}
5+
6+
resolve(modeName?: string | null) {
7+
return this.options.modes.find((mode) => mode.name === modeName) ?? this.options.modes[0];
8+
}
9+
}

agent/models/AgentModelFactory.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { CompletionAdapter } from "adminforth";
2+
import { createAgentChatModel } from "../simpleAgent.js";
3+
import type { AgentTurnModels } from "../turn/turnTypes.js";
4+
5+
export class AgentModelFactory {
6+
constructor(private readonly maxTokens: number) {}
7+
8+
async create(completionAdapter: CompletionAdapter): Promise<AgentTurnModels> {
9+
const [primaryModelSpec, summaryModelSpec] = await Promise.all([
10+
createAgentChatModel({
11+
adapter: completionAdapter,
12+
maxTokens: this.maxTokens,
13+
purpose: "primary",
14+
}),
15+
createAgentChatModel({
16+
adapter: completionAdapter,
17+
maxTokens: this.maxTokens,
18+
purpose: "summary",
19+
}),
20+
]);
21+
22+
return {
23+
model: primaryModelSpec.model,
24+
summaryModel: summaryModelSpec.model,
25+
modelMiddleware: primaryModelSpec.middleware,
26+
};
27+
}
28+
}

agent/runtime/AgentContext.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { AdminUser } from "adminforth";
2+
import { z } from "zod";
3+
import type { AgentEventEmitter } from "../../agentEvents.js";
4+
import type { SequenceDebugCollector } from "../middleware/sequenceDebug.js";
5+
import type { CurrentPageContext } from "../tools/getUserLocation.js";
6+
import type { AgentTurnContext } from "../turn/turnTypes.js";
7+
8+
export const contextSchema = z.object({
9+
adminUser: z.custom<AdminUser>(),
10+
userTimeZone: z.string(),
11+
sessionId: z.string(),
12+
turnId: z.string(),
13+
abortSignal: z.custom<AbortSignal>().optional(),
14+
currentPage: z.custom<CurrentPageContext>().optional(),
15+
chatSurface: z.string().optional(),
16+
adminBaseUrl: z.string().optional(),
17+
adminPublicOrigin: z.string().optional(),
18+
emit: z.custom<AgentEventEmitter>().optional(),
19+
sequenceDebugSink: z.custom<SequenceDebugCollector>(),
20+
});
21+
22+
export function toLangchainAgentContext(
23+
context: AgentTurnContext & {
24+
adminBaseUrl: string;
25+
emit?: AgentEventEmitter;
26+
sequenceDebugSink: SequenceDebugCollector;
27+
},
28+
) {
29+
return context;
30+
}

agent/runtime/AgentRuntime.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { IAdminForth } from "adminforth";
2+
import { createAgent, summarizationMiddleware } from "langchain";
3+
import type { BaseCheckpointSaver } from "@langchain/langgraph";
4+
import { createApiBasedToolsMiddleware } from "../middleware/apiBasedTools.js";
5+
import { createSequenceDebugMiddleware } from "../middleware/sequenceDebug.js";
6+
import { createAgentLlmMetricsLogger } from "../simpleAgent.js";
7+
import type { AgentToolProvider } from "../tools/AgentToolProvider.js";
8+
import type { AgentRuntimeRunInput } from "../turn/turnTypes.js";
9+
import { contextSchema, toLangchainAgentContext } from "./AgentContext.js";
10+
11+
export type AgentRuntimeOptions = {
12+
name: string;
13+
getAdminforth: () => IAdminForth;
14+
getCheckpointer: () => BaseCheckpointSaver;
15+
toolProvider: AgentToolProvider;
16+
};
17+
18+
export class AgentRuntime {
19+
constructor(private readonly options: AgentRuntimeOptions) {}
20+
21+
async stream(input: AgentRuntimeRunInput) {
22+
const apiBasedTools = this.options.toolProvider.getApiBasedTools();
23+
const tools = await this.options.toolProvider.getTools(apiBasedTools);
24+
const adminforth = this.options.getAdminforth();
25+
const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(
26+
apiBasedTools,
27+
adminforth,
28+
);
29+
const sequenceDebugMiddleware = createSequenceDebugMiddleware(
30+
input.observability.sequenceDebugSink,
31+
);
32+
const middleware = [
33+
apiBasedToolsMiddleware,
34+
...(input.models.modelMiddleware ?? []),
35+
sequenceDebugMiddleware,
36+
summarizationMiddleware({
37+
model: input.models.summaryModel,
38+
trigger: { tokens: 1024 * 64 },
39+
keep: { messages: 10 },
40+
}),
41+
] as const;
42+
43+
const agent = createAgent({
44+
name: this.options.name,
45+
model: input.models.model,
46+
checkpointer: this.options.getCheckpointer(),
47+
tools,
48+
contextSchema,
49+
middleware,
50+
});
51+
52+
return agent.stream({ messages: input.messages } as any, {
53+
streamMode: "messages",
54+
recursionLimit: 100,
55+
callbacks: [createAgentLlmMetricsLogger()],
56+
signal: input.context.abortSignal,
57+
configurable: {
58+
thread_id: input.context.sessionId,
59+
},
60+
context: toLangchainAgentContext({
61+
...input.context,
62+
adminBaseUrl: adminforth.config.baseUrlSlashed,
63+
emit: input.observability.emit,
64+
sequenceDebugSink: input.observability.sequenceDebugSink,
65+
}),
66+
});
67+
}
68+
}

0 commit comments

Comments
 (0)