@adminforth/agent 1.50.0 → 1.51.0

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.
Files changed (58) hide show
  1. package/agent/middleware/apiBasedTools.ts +13 -3
  2. package/agent/models/AgentModeResolver.ts +9 -0
  3. package/agent/models/AgentModelFactory.ts +28 -0
  4. package/agent/runtime/AgentContext.ts +30 -0
  5. package/agent/runtime/AgentRuntime.ts +68 -0
  6. package/agent/simpleAgent.ts +2 -129
  7. package/agent/speech/SpeechTurnService.ts +179 -0
  8. package/agent/tools/AgentToolProvider.ts +28 -0
  9. package/agent/tools/navigateUser.ts +2 -2
  10. package/agent/turn/TurnContextBuilder.ts +36 -0
  11. package/agent/turn/TurnLifecycleService.ts +29 -0
  12. package/agent/turn/TurnPersistenceService.ts +33 -0
  13. package/agent/turn/TurnPromptBuilder.ts +51 -0
  14. package/agent/turn/TurnStreamConsumer.ts +61 -0
  15. package/agent/turn/VegaLiteStreamBuffer.ts +90 -0
  16. package/agent/turn/turnTypes.ts +92 -0
  17. package/agentTurnService.ts +88 -461
  18. package/build.log +2 -2
  19. package/custom/skills/fetch_data/SKILL.md +2 -0
  20. package/custom/skills/mutate_data/SKILL.md +4 -0
  21. package/dist/agent/middleware/apiBasedTools.js +9 -2
  22. package/dist/agent/models/AgentModeResolver.d.ts +9 -0
  23. package/dist/agent/models/AgentModeResolver.js +9 -0
  24. package/dist/agent/models/AgentModelFactory.d.ts +7 -0
  25. package/dist/agent/models/AgentModelFactory.js +36 -0
  26. package/dist/agent/runtime/AgentContext.d.ts +28 -0
  27. package/dist/agent/runtime/AgentContext.js +17 -0
  28. package/dist/agent/runtime/AgentRuntime.d.ts +15 -0
  29. package/dist/agent/runtime/AgentRuntime.js +57 -0
  30. package/dist/agent/simpleAgent.d.ts +15 -45
  31. package/dist/agent/simpleAgent.js +1 -67
  32. package/dist/agent/speech/SpeechTurnService.d.ts +6 -0
  33. package/dist/agent/speech/SpeechTurnService.js +168 -0
  34. package/dist/agent/tools/AgentToolProvider.d.ts +9 -0
  35. package/dist/agent/tools/AgentToolProvider.js +27 -0
  36. package/dist/agent/tools/navigateUser.js +1 -1
  37. package/dist/agent/turn/TurnContextBuilder.d.ts +14 -0
  38. package/dist/agent/turn/TurnContextBuilder.js +31 -0
  39. package/dist/agent/turn/TurnLifecycleService.d.ts +17 -0
  40. package/dist/agent/turn/TurnLifecycleService.js +31 -0
  41. package/dist/agent/turn/TurnPersistenceService.d.ts +13 -0
  42. package/dist/agent/turn/TurnPersistenceService.js +35 -0
  43. package/dist/agent/turn/TurnPromptBuilder.d.ts +19 -0
  44. package/dist/agent/turn/TurnPromptBuilder.js +43 -0
  45. package/dist/agent/turn/TurnStreamConsumer.d.ts +10 -0
  46. package/dist/agent/turn/TurnStreamConsumer.js +78 -0
  47. package/dist/agent/turn/VegaLiteStreamBuffer.d.ts +7 -0
  48. package/dist/agent/turn/VegaLiteStreamBuffer.js +87 -0
  49. package/dist/agent/turn/turnTypes.d.ts +81 -0
  50. package/dist/agent/turn/turnTypes.js +1 -0
  51. package/dist/agentTurnService.d.ts +20 -69
  52. package/dist/agentTurnService.js +60 -373
  53. package/dist/custom/skills/fetch_data/SKILL.md +2 -0
  54. package/dist/custom/skills/mutate_data/SKILL.md +4 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +22 -7
  57. package/index.ts +35 -7
  58. package/package.json +1 -1
@@ -11,6 +11,8 @@ import {
11
11
  } from "../toolCallEvents.js";
12
12
  import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "../tools/index.js";
13
13
  import { createApiTool } from "../tools/apiTool.js";
14
+ import type { AgentEventEmitter } from "../../agentEvents.js";
15
+ import type { SequenceDebugCollector } from "./sequenceDebug.js";
14
16
 
15
17
  function getEnabledApiToolNames(messages: unknown[]) {
16
18
  const enabledToolNames = new Set<string>();
@@ -80,11 +82,19 @@ export function createApiBasedToolsMiddleware(
80
82
  async wrapToolCall(request, handler) {
81
83
  const startedAt = Date.now();
82
84
  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 {
84
86
  adminUser: AdminUser;
85
- emitToolCallEvent: ToolCallEventSink;
87
+ emit?: AgentEventEmitter;
88
+ sequenceDebugSink: SequenceDebugCollector;
86
89
  userTimeZone: string;
87
90
  };
91
+ const emitToolCall: ToolCallEventSink = (event) => {
92
+ sequenceDebugSink.handleToolCallEvent(event);
93
+ void emit?.({
94
+ type: "tool-call",
95
+ data: event,
96
+ });
97
+ };
88
98
  const toolArgs = (request.toolCall.args ?? {}) as Record<string, unknown>;
89
99
  let toolInfo: string | undefined;
90
100
 
@@ -102,7 +112,7 @@ export function createApiBasedToolsMiddleware(
102
112
  });
103
113
  }
104
114
  const toolCallTracker = createToolCallTracker({
105
- emit: emitToolCallEvent,
115
+ emit: emitToolCall,
106
116
  toolCallId: request.toolCall.id,
107
117
  toolName: request.toolCall.name,
108
118
  toolInfo,
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -1,39 +1,13 @@
1
1
  import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
2
- import { createAgent, summarizationMiddleware } from "langchain";
3
2
  import {
4
3
  logger,
5
- type AdminUser,
6
4
  type CompletionAdapter,
7
- type IAdminForth,
8
5
  } from "adminforth";
9
6
  import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
10
- import {type BaseCheckpointSaver, type Messages } from "@langchain/langgraph";
11
7
  import type { LLMResult } from "@langchain/core/outputs";
12
- import { z } from "zod";
13
- import { createAgentTools } from "./tools/index.js";
14
- import { createApiBasedToolsMiddleware } from "./middleware/apiBasedTools.js";
15
8
  import {
16
9
  createSequenceDebugMiddleware,
17
- type SequenceDebugModelCallSink,
18
10
  } from "./middleware/sequenceDebug.js";
19
- import type { ApiBasedTool } from "../apiBasedTools.js";
20
- import type { ToolCallEventSink } from "./toolCallEvents.js";
21
- import type { CurrentPageContext } from "./tools/getUserLocation.js";
22
- import type { AgentEventEmitter } from "../agentEvents.js";
23
-
24
- export const contextSchema = z.object({
25
- adminUser: z.custom<AdminUser>(),
26
- userTimeZone: z.string(),
27
- sessionId: z.string(),
28
- turnId: z.string(),
29
- abortSignal: z.custom<AbortSignal>().optional(),
30
- currentPage: z.custom<CurrentPageContext>().optional(),
31
- chatSurface: z.string().optional(),
32
- adminBaseUrl: z.string().optional(),
33
- adminPublicOrigin: z.string().optional(),
34
- emitToolCallEvent: z.custom<ToolCallEventSink>(),
35
- emitAgentEvent: z.custom<AgentEventEmitter>().optional(),
36
- });
37
11
 
38
12
  export type AgentChatModel = BaseChatModel<any, any>;
39
13
  export type AgentModelPurpose = "primary" | "summary";
@@ -50,7 +24,7 @@ export type AgentModeCompletionAdapter = CompletionAdapter & {
50
24
  };
51
25
  };
52
26
 
53
- type AgentMiddleware = ReturnType<typeof createSequenceDebugMiddleware>;
27
+ export type AgentMiddleware = ReturnType<typeof createSequenceDebugMiddleware>;
54
28
 
55
29
  type AgentChatModelSpec = {
56
30
  model: AgentChatModel;
@@ -202,7 +176,7 @@ class AgentLlmMetricsLogger extends BaseCallbackHandler {
202
176
  }
203
177
  }
204
178
 
205
- function createAgentLlmMetricsLogger() {
179
+ export function createAgentLlmMetricsLogger() {
206
180
  return new AgentLlmMetricsLogger();
207
181
  }
208
182
 
@@ -223,104 +197,3 @@ export async function createAgentChatModel(params: {
223
197
  purpose: params.purpose,
224
198
  });
225
199
  }
226
-
227
- export async function callAgent(params: {
228
- name: string;
229
- model: AgentChatModel;
230
- summaryModel: AgentChatModel;
231
- modelMiddleware?: AgentMiddleware[];
232
- checkpointer?: BaseCheckpointSaver;
233
- messages: Messages;
234
- adminUser: AdminUser;
235
- adminforth: IAdminForth;
236
- apiBasedTools: Record<string, ApiBasedTool>;
237
- customComponentsDir: string;
238
- pluginCustomFolderPaths: string[];
239
- sessionId: string;
240
- turnId: string;
241
- currentPage?: CurrentPageContext;
242
- chatSurface?: string;
243
- adminPublicOrigin?: string;
244
- userTimeZone: string;
245
- abortSignal?: AbortSignal;
246
- emitToolCallEvent: ToolCallEventSink;
247
- emitAgentEvent?: AgentEventEmitter;
248
- sequenceDebugSink: SequenceDebugModelCallSink;
249
- }) {
250
- const {
251
- name,
252
- model,
253
- summaryModel,
254
- modelMiddleware = [],
255
- checkpointer,
256
- messages,
257
- adminUser,
258
- adminforth,
259
- apiBasedTools,
260
- customComponentsDir,
261
- pluginCustomFolderPaths,
262
- sessionId,
263
- turnId,
264
- currentPage,
265
- chatSurface,
266
- adminPublicOrigin,
267
- userTimeZone,
268
- abortSignal,
269
- emitToolCallEvent,
270
- emitAgentEvent,
271
- sequenceDebugSink,
272
- } = params;
273
-
274
- const tools = await createAgentTools(
275
- customComponentsDir,
276
- apiBasedTools,
277
- pluginCustomFolderPaths,
278
- );
279
- const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools, adminforth);
280
- const sequenceDebugMiddleware = createSequenceDebugMiddleware(
281
- sequenceDebugSink,
282
- );
283
-
284
- const middleware = [
285
- apiBasedToolsMiddleware,
286
- ...modelMiddleware,
287
- sequenceDebugMiddleware,
288
- summarizationMiddleware({
289
- model: summaryModel,
290
- trigger: { tokens: 1024 * 64 },
291
- keep: { messages: 10 },
292
- }),
293
- ] as const;
294
-
295
- const agent = createAgent<undefined, typeof contextSchema, typeof middleware>({
296
- name,
297
- model,
298
- checkpointer,
299
- tools,
300
- contextSchema,
301
- middleware,
302
- });
303
-
304
- return await agent.stream({ messages } as any, {
305
- streamMode: "messages",
306
- recursionLimit: 100,
307
- callbacks: [createAgentLlmMetricsLogger()],
308
- signal: abortSignal,
309
- configurable: {
310
- thread_id: sessionId,
311
- },
312
- context: {
313
- adminUser,
314
- userTimeZone,
315
- sessionId,
316
- turnId,
317
- abortSignal,
318
- currentPage,
319
- chatSurface,
320
- adminBaseUrl: adminforth.config.baseUrlSlashed,
321
- adminPublicOrigin,
322
- emitToolCallEvent,
323
- emitAgentEvent,
324
- },
325
- });
326
- }
@@ -0,0 +1,179 @@
1
+ import { logger } from "adminforth";
2
+ import { getErrorMessage, isAbortError } from "../../errors.js";
3
+ import { sanitizeSpeechText } from "../../sanitizeSpeechText.js";
4
+ import type {
5
+ RunAndPersistAgentResponseInput,
6
+ RunAndPersistAgentResponseResult,
7
+ SpeechAgentTurnInput,
8
+ } from "../turn/turnTypes.js";
9
+
10
+ export class SpeechTurnService {
11
+ constructor(
12
+ private readonly runAndPersistAgentResponse: (
13
+ input: RunAndPersistAgentResponseInput,
14
+ ) => Promise<RunAndPersistAgentResponseResult>,
15
+ ) {}
16
+
17
+ async handle(input: SpeechAgentTurnInput) {
18
+ let transcription;
19
+
20
+ try {
21
+ transcription = await input.audioAdapter.transcribe({
22
+ buffer: input.audio.buffer,
23
+ filename: input.audio.filename,
24
+ mimeType: input.audio.mimeType,
25
+ language: "auto",
26
+ abortSignal: input.abortSignal,
27
+ });
28
+ } catch (error) {
29
+ if (input.abortSignal?.aborted || isAbortError(error)) {
30
+ logger.info("Agent speech transcription aborted by the client");
31
+ await input.emit({ type: "finish" });
32
+ return null;
33
+ }
34
+
35
+ logger.error(`Agent speech transcription failed:\n${getErrorMessage(error)}`);
36
+ await input.emit({
37
+ type: "error",
38
+ error: "Speech transcription failed. Check server logs for details.",
39
+ });
40
+ await input.emit({ type: "finish" });
41
+ return null;
42
+ }
43
+
44
+ if (input.abortSignal?.aborted) {
45
+ await input.emit({ type: "finish" });
46
+ return null;
47
+ }
48
+
49
+ const prompt = transcription.text;
50
+ if (!prompt) {
51
+ await input.emit({
52
+ type: "error",
53
+ error: "Speech transcription is empty",
54
+ });
55
+ await input.emit({ type: "finish" });
56
+ return null;
57
+ }
58
+
59
+ await input.emit({
60
+ type: "transcript",
61
+ text: transcription.text,
62
+ language: transcription.language,
63
+ });
64
+
65
+ const agentResponse = await this.runAndPersistAgentResponse({
66
+ prompt,
67
+ sessionId: input.sessionId,
68
+ modeName: input.modeName,
69
+ userTimeZone: input.userTimeZone,
70
+ currentPage: input.currentPage,
71
+ chatSurface: input.chatSurface,
72
+ adminPublicOrigin: input.adminPublicOrigin,
73
+ abortSignal: input.abortSignal,
74
+ adminUser: input.adminUser,
75
+ emit: async (event) => {
76
+ if (event.type === "tool-call") {
77
+ await input.emit(event);
78
+ }
79
+ },
80
+ failureLogMessage: input.failureLogMessage ?? "Agent speech response failed",
81
+ abortLogMessage: input.abortLogMessage ?? "Agent speech response aborted by the client",
82
+ });
83
+
84
+ if (agentResponse.aborted) {
85
+ await input.emit({ type: "finish" });
86
+ return agentResponse;
87
+ }
88
+
89
+ if (agentResponse.failed) {
90
+ await input.emit({
91
+ type: "error",
92
+ error: agentResponse.text,
93
+ });
94
+ await input.emit({ type: "finish" });
95
+ return agentResponse;
96
+ }
97
+
98
+ try {
99
+ await input.emit({
100
+ type: "speech-response",
101
+ transcript: {
102
+ text: transcription.text,
103
+ language: transcription.language,
104
+ },
105
+ response: {
106
+ text: agentResponse.text,
107
+ },
108
+ sessionId: input.sessionId,
109
+ turnId: agentResponse.turnId,
110
+ });
111
+ const speech = await input.audioAdapter.synthesize({
112
+ text: sanitizeSpeechText(agentResponse.text),
113
+ stream: true,
114
+ streamFormat: "audio",
115
+ format: "pcm",
116
+ abortSignal: input.abortSignal,
117
+ });
118
+
119
+ await input.emit({
120
+ type: "audio-start",
121
+ mimeType: speech.mimeType,
122
+ format: speech.format,
123
+ sampleRate: 24000,
124
+ channelCount: 1,
125
+ bitsPerSample: 16,
126
+ });
127
+
128
+ const reader = speech.audioStream.getReader();
129
+ const cancelAudioStream = () => {
130
+ void reader.cancel().catch(() => undefined);
131
+ };
132
+
133
+ try {
134
+ input.abortSignal?.addEventListener("abort", cancelAudioStream, { once: true });
135
+
136
+ while (true) {
137
+ if (input.abortSignal?.aborted) {
138
+ await reader.cancel().catch(() => undefined);
139
+ break;
140
+ }
141
+
142
+ const { value, done } = await reader.read();
143
+
144
+ if (done) {
145
+ break;
146
+ }
147
+
148
+ if (input.abortSignal?.aborted) {
149
+ break;
150
+ }
151
+
152
+ await input.emit({
153
+ type: "audio-delta",
154
+ value,
155
+ });
156
+ }
157
+ } finally {
158
+ input.abortSignal?.removeEventListener("abort", cancelAudioStream);
159
+ reader.releaseLock();
160
+ }
161
+
162
+ await input.emit({ type: "audio-done" });
163
+ await input.emit({ type: "finish" });
164
+ return agentResponse;
165
+ } catch (error) {
166
+ if (input.abortSignal?.aborted || isAbortError(error)) {
167
+ logger.info("Agent speech audio streaming aborted by the client");
168
+ } else {
169
+ logger.error(`Agent speech audio streaming failed:\n${getErrorMessage(error)}`);
170
+ await input.emit({
171
+ type: "error",
172
+ error: getErrorMessage(error),
173
+ });
174
+ }
175
+ await input.emit({ type: "finish" });
176
+ return agentResponse;
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,28 @@
1
+ import type { IAdminForth } from "adminforth";
2
+ import { prepareApiBasedTools } from "../../apiBasedTools.js";
3
+ import type { ApiBasedTool } from "../../apiBasedTools.js";
4
+ import { createAgentTools } from "./index.js";
5
+
6
+ export class AgentToolProvider {
7
+ constructor(
8
+ private readonly getAdminforth: () => IAdminForth,
9
+ private readonly getInternalAgentResourceIds: () => string[],
10
+ ) {}
11
+
12
+ getApiBasedTools(): Record<string, ApiBasedTool> {
13
+ return prepareApiBasedTools(
14
+ this.getAdminforth(),
15
+ this.getInternalAgentResourceIds(),
16
+ );
17
+ }
18
+
19
+ async getTools(apiBasedTools: Record<string, ApiBasedTool>) {
20
+ const adminforth = this.getAdminforth();
21
+
22
+ return createAgentTools(
23
+ adminforth.config.customization.customComponentsDir ?? "custom",
24
+ apiBasedTools,
25
+ adminforth.activatedPlugins.map((plugin) => plugin.customFolderPath),
26
+ );
27
+ }
28
+ }
@@ -159,7 +159,7 @@ export function createNavigateUserTool() {
159
159
  chatSurface?: string;
160
160
  adminBaseUrl?: string;
161
161
  adminPublicOrigin?: string;
162
- emitAgentEvent?: AgentEventEmitter;
162
+ emit?: AgentEventEmitter;
163
163
  };
164
164
  const currentPage = context.currentPage;
165
165
  const basePath = input.targetPath
@@ -184,7 +184,7 @@ export function createNavigateUserTool() {
184
184
  );
185
185
  }
186
186
 
187
- await context.emitAgentEvent?.({
187
+ await context.emit?.({
188
188
  type: "open-page",
189
189
  targetPath,
190
190
  });
@@ -0,0 +1,36 @@
1
+ import type { AdminUser, IAdminForth } from "adminforth";
2
+ import type { AgentTurnContext, BaseAgentTurnInput } from "./turnTypes.js";
3
+
4
+ export type UserContextProvider = {
5
+ getUserTimeZone(adminUser: AdminUser): Promise<string | null | undefined> | string | null | undefined;
6
+ };
7
+
8
+ export class TurnContextBuilder {
9
+ constructor(
10
+ private readonly getAdminforth: () => IAdminForth,
11
+ private readonly userContextProvider?: UserContextProvider,
12
+ ) {}
13
+
14
+ async build(input: {
15
+ base: BaseAgentTurnInput;
16
+ turnId: string;
17
+ }): Promise<AgentTurnContext> {
18
+ const adminforth = this.getAdminforth();
19
+
20
+ return {
21
+ adminUser: input.base.adminUser,
22
+ sessionId: input.base.sessionId,
23
+ turnId: input.turnId,
24
+ abortSignal: input.base.abortSignal,
25
+ currentPage: input.base.currentPage,
26
+ chatSurface: input.base.chatSurface,
27
+ userTimeZone:
28
+ input.base.userTimeZone
29
+ ?? await this.userContextProvider?.getUserTimeZone(input.base.adminUser)
30
+ ?? "UTC",
31
+ adminPublicOrigin:
32
+ input.base.adminPublicOrigin
33
+ ?? adminforth.config.baseUrlSlashed,
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,29 @@
1
+ import type { AgentSessionStore } from "../../sessionStore.js";
2
+ import type { BaseAgentTurnInput } from "./turnTypes.js";
3
+ import { TurnPersistenceService } from "./TurnPersistenceService.js";
4
+
5
+ export class TurnLifecycleService {
6
+ constructor(
7
+ private readonly sessionStore: AgentSessionStore,
8
+ private readonly persistence: TurnPersistenceService,
9
+ ) {}
10
+
11
+ async start(input: BaseAgentTurnInput) {
12
+ const previousUserMessages = await this.sessionStore.getPreviousUserMessages(input.sessionId);
13
+ const turnId = await this.sessionStore.createNewTurn(input.sessionId, input.prompt);
14
+ await this.persistence.touchSession(input.sessionId);
15
+
16
+ return {
17
+ turnId,
18
+ previousUserMessages,
19
+ };
20
+ }
21
+
22
+ async finish(input: {
23
+ turnId: string;
24
+ responseText: string;
25
+ debugHistory?: unknown;
26
+ }) {
27
+ await this.persistence.saveTurnResponse(input);
28
+ }
29
+ }
@@ -0,0 +1,33 @@
1
+ import type { IAdminForth } from "adminforth";
2
+ import type { PluginOptions } from "../../types.js";
3
+
4
+ export class TurnPersistenceService {
5
+ constructor(
6
+ private readonly getAdminforth: () => IAdminForth,
7
+ private readonly options: PluginOptions,
8
+ ) {}
9
+
10
+ async touchSession(sessionId: string) {
11
+ await this.getAdminforth().resource(this.options.sessionResource.resourceId).update(sessionId, {
12
+ [this.options.sessionResource.createdAtField]: new Date().toISOString(),
13
+ });
14
+ }
15
+
16
+ async saveTurnResponse(input: {
17
+ turnId: string;
18
+ responseText: string;
19
+ debugHistory?: unknown;
20
+ }) {
21
+ const turnUpdates: Record<string, unknown> = {
22
+ [this.options.turnResource.responseField]: input.responseText,
23
+ };
24
+
25
+ if (this.options.turnResource.debugField) {
26
+ turnUpdates[this.options.turnResource.debugField] = input.debugHistory;
27
+ }
28
+
29
+ await this.getAdminforth()
30
+ .resource(this.options.turnResource.resourceId)
31
+ .update(input.turnId, turnUpdates);
32
+ }
33
+ }