@adminforth/agent 1.49.2 → 1.50.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 (37) hide show
  1. package/agent/middleware/sequenceDebug.ts +42 -7
  2. package/agent/simpleAgent.ts +15 -0
  3. package/agent/systemPrompt.ts +6 -1
  4. package/agent/tools/index.ts +2 -0
  5. package/agent/tools/navigateUser.ts +210 -0
  6. package/agentEvents.ts +4 -0
  7. package/agentTurnService.ts +14 -0
  8. package/build.log +2 -2
  9. package/chatSurfaceService.ts +7 -0
  10. package/custom/composables/agentStore/useAgentChat.ts +8 -1
  11. package/custom/composables/useAgentAudio.ts +5 -0
  12. package/custom/composables/useAgentStore.ts +43 -0
  13. package/custom/tsconfig.json +0 -1
  14. package/custom/types.ts +6 -0
  15. package/dist/agent/middleware/sequenceDebug.d.ts +6 -0
  16. package/dist/agent/middleware/sequenceDebug.js +33 -6
  17. package/dist/agent/simpleAgent.d.ts +8 -0
  18. package/dist/agent/simpleAgent.js +9 -1
  19. package/dist/agent/systemPrompt.d.ts +1 -0
  20. package/dist/agent/systemPrompt.js +5 -1
  21. package/dist/agent/tools/index.js +2 -0
  22. package/dist/agent/tools/navigateUser.d.ts +55 -0
  23. package/dist/agent/tools/navigateUser.js +163 -0
  24. package/dist/agentEvents.d.ts +3 -0
  25. package/dist/agentTurnService.d.ts +2 -0
  26. package/dist/agentTurnService.js +10 -0
  27. package/dist/chatSurfaceService.js +10 -3
  28. package/dist/custom/composables/agentStore/useAgentChat.ts +8 -1
  29. package/dist/custom/composables/useAgentAudio.ts +5 -0
  30. package/dist/custom/composables/useAgentStore.ts +43 -0
  31. package/dist/custom/tsconfig.json +0 -1
  32. package/dist/custom/types.ts +6 -0
  33. package/dist/endpoints/chatSurfaces.js +20 -0
  34. package/dist/surfaces/web-sse/createSseEventEmitter.js +11 -0
  35. package/endpoints/chatSurfaces.ts +29 -0
  36. package/package.json +2 -2
  37. package/surfaces/web-sse/createSseEventEmitter.ts +12 -0
@@ -26,6 +26,9 @@ export type SequenceDebug = {
26
26
  reasoningTokens: number;
27
27
  text: string;
28
28
  textTokens: number;
29
+ uncachedInputTokens: number;
30
+ cachedInputTokens: number;
31
+ outputTokens: number;
29
32
  cachedTokens: number;
30
33
  responseId: string | null;
31
34
  toolCalls: SequenceDebugToolCall[];
@@ -45,6 +48,9 @@ type SequenceDebugModelCall = {
45
48
  reasoningTokens: number;
46
49
  text: string;
47
50
  textTokens: number;
51
+ uncachedInputTokens: number;
52
+ cachedInputTokens: number;
53
+ outputTokens: number;
48
54
  cachedTokens: number;
49
55
  responseId: string | null;
50
56
  resultType: SequenceDebugResultType;
@@ -52,6 +58,7 @@ type SequenceDebugModelCall = {
52
58
 
53
59
  type OpenAiUsageMetadata = {
54
60
  input_tokens?: number;
61
+ output_tokens?: number;
55
62
  input_token_details?: {
56
63
  cache_read?: number;
57
64
  };
@@ -82,6 +89,9 @@ function createPendingSequenceDebug(sequenceId: number): PendingSequenceDebug {
82
89
  reasoningTokens: 0,
83
90
  text: "",
84
91
  textTokens: 0,
92
+ uncachedInputTokens: 0,
93
+ cachedInputTokens: 0,
94
+ outputTokens: 0,
85
95
  cachedTokens: 0,
86
96
  responseId: null,
87
97
  toolCalls: [],
@@ -112,6 +122,9 @@ function finalizeSequenceDebug(sequence: PendingSequenceDebug): SequenceDebug {
112
122
  reasoningTokens: sequence.reasoningTokens,
113
123
  text: sequence.text,
114
124
  textTokens: sequence.textTokens,
125
+ uncachedInputTokens: sequence.uncachedInputTokens,
126
+ cachedInputTokens: sequence.cachedInputTokens,
127
+ outputTokens: sequence.outputTokens,
115
128
  cachedTokens: sequence.cachedTokens,
116
129
  responseId: sequence.responseId,
117
130
  toolCalls: sequence.toolCalls.map(({ completed: _completed, ...toolCall }) => toolCall),
@@ -133,6 +146,21 @@ function getDebugModelName(model: SequenceDebugPromptModel) {
133
146
  return typeof model.getName === "function" ? model.getName() : undefined;
134
147
  }
135
148
 
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
+
136
164
  function stringifyPromptForDebug(params: {
137
165
  model: SequenceDebugPromptModel;
138
166
  systemMessage: { text: string };
@@ -160,7 +188,7 @@ function stringifyPromptForDebug(params: {
160
188
  },
161
189
  systemMessage,
162
190
  messages,
163
- ...(tools.length > 0 ? { tools } : {}),
191
+ ...(tools.length > 0 ? { tools: formatToolsForDebug(tools) } : {}),
164
192
  ...(toolChoice !== undefined ? { toolChoice } : {}),
165
193
  ...(modelSettings ? { modelSettings } : {}),
166
194
  ...(invocationParams ? { invocationParams } : {}),
@@ -219,6 +247,9 @@ async function countTokens(model: unknown, content: string) {
219
247
  }
220
248
 
221
249
  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;
222
253
  const blocks = getMessageBlocks(message);
223
254
  const reasoning = blocks
224
255
  .filter((block: any) => block?.type === "reasoning")
@@ -230,16 +261,15 @@ function extractSequenceResponseDebug(message: AIMessage): SequenceDebugModelCal
230
261
  .join("");
231
262
 
232
263
  return {
233
- promptTokens:
234
- (message.usage_metadata as OpenAiUsageMetadata | undefined)?.input_tokens ??
235
- 0,
264
+ promptTokens,
236
265
  reasoning,
237
266
  reasoningTokens: 0,
238
267
  text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
239
268
  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,
243
273
  responseId:
244
274
  (message.response_metadata as OpenAiResponseMetadata | undefined)?.id ??
245
275
  null,
@@ -290,6 +320,9 @@ export function createSequenceDebugCollector(): SequenceDebugCollector {
290
320
  sequenceDebug.reasoningTokens = params.reasoningTokens;
291
321
  sequenceDebug.text = params.text;
292
322
  sequenceDebug.textTokens = params.textTokens;
323
+ sequenceDebug.uncachedInputTokens = params.uncachedInputTokens;
324
+ sequenceDebug.cachedInputTokens = params.cachedInputTokens;
325
+ sequenceDebug.outputTokens = params.outputTokens;
293
326
  sequenceDebug.cachedTokens = params.cachedTokens;
294
327
  sequenceDebug.responseId = params.responseId;
295
328
  sequenceDebug.resultType = params.resultType;
@@ -387,6 +420,8 @@ export function createSequenceDebugMiddleware(
387
420
  promptTokens,
388
421
  reasoningTokens,
389
422
  textTokens,
423
+ uncachedInputTokens: debug.promptTokens ? debug.uncachedInputTokens : promptTokens,
424
+ outputTokens: debug.outputTokens || reasoningTokens + textTokens,
390
425
  });
391
426
  return response;
392
427
  },
@@ -19,6 +19,7 @@ import {
19
19
  import type { ApiBasedTool } from "../apiBasedTools.js";
20
20
  import type { ToolCallEventSink } from "./toolCallEvents.js";
21
21
  import type { CurrentPageContext } from "./tools/getUserLocation.js";
22
+ import type { AgentEventEmitter } from "../agentEvents.js";
22
23
 
23
24
  export const contextSchema = z.object({
24
25
  adminUser: z.custom<AdminUser>(),
@@ -27,7 +28,11 @@ export const contextSchema = z.object({
27
28
  turnId: z.string(),
28
29
  abortSignal: z.custom<AbortSignal>().optional(),
29
30
  currentPage: z.custom<CurrentPageContext>().optional(),
31
+ chatSurface: z.string().optional(),
32
+ adminBaseUrl: z.string().optional(),
33
+ adminPublicOrigin: z.string().optional(),
30
34
  emitToolCallEvent: z.custom<ToolCallEventSink>(),
35
+ emitAgentEvent: z.custom<AgentEventEmitter>().optional(),
31
36
  });
32
37
 
33
38
  export type AgentChatModel = BaseChatModel<any, any>;
@@ -234,9 +239,12 @@ export async function callAgent(params: {
234
239
  sessionId: string;
235
240
  turnId: string;
236
241
  currentPage?: CurrentPageContext;
242
+ chatSurface?: string;
243
+ adminPublicOrigin?: string;
237
244
  userTimeZone: string;
238
245
  abortSignal?: AbortSignal;
239
246
  emitToolCallEvent: ToolCallEventSink;
247
+ emitAgentEvent?: AgentEventEmitter;
240
248
  sequenceDebugSink: SequenceDebugModelCallSink;
241
249
  }) {
242
250
  const {
@@ -254,9 +262,12 @@ export async function callAgent(params: {
254
262
  sessionId,
255
263
  turnId,
256
264
  currentPage,
265
+ chatSurface,
266
+ adminPublicOrigin,
257
267
  userTimeZone,
258
268
  abortSignal,
259
269
  emitToolCallEvent,
270
+ emitAgentEvent,
260
271
  sequenceDebugSink,
261
272
  } = params;
262
273
 
@@ -305,7 +316,11 @@ export async function callAgent(params: {
305
316
  turnId,
306
317
  abortSignal,
307
318
  currentPage,
319
+ chatSurface,
320
+ adminBaseUrl: adminforth.config.baseUrlSlashed,
321
+ adminPublicOrigin,
308
322
  emitToolCallEvent,
323
+ emitAgentEvent,
309
324
  },
310
325
  });
311
326
  }
@@ -70,12 +70,16 @@ export function buildAgentTurnSystemPrompt(input: {
70
70
  adminUser: AdminUser;
71
71
  usernameField: string;
72
72
  userLanguage: DetectedLanguage | null;
73
+ chatSurface?: string;
73
74
  }) {
74
75
  return [
75
76
  input.agentSystemPrompt,
76
77
  formatAdminUserPrompt(input.adminUser, input.usernameField),
78
+ input.chatSurface
79
+ ? `Current chat surface: ${input.chatSurface}. The user is not in the AdminForth web UI, so tools cannot move their browser. When navigate_user returns a link, send that link to the user.`
80
+ : "",
77
81
  formatLanguagePrompt(input.userLanguage),
78
- ].join("\n\n");
82
+ ].filter(Boolean).join("\n\n");
79
83
  }
80
84
 
81
85
  function formatResources(resources: AdminForthResource[]) {
@@ -124,6 +128,7 @@ export async function buildAgentSystemPrompt(
124
128
  "When fetch_tool_schema succeeds, that tool becomes available on the next step.",
125
129
  "All admin links must be root-relative and start with '/'.",
126
130
  "Build record links as '/resource/{resourceId}/show/{primary key}'. Never use bare 'resource/{resourceId}/show/{primary key}' without the leading slash.",
131
+ "When the user asks to open or show a page in the AdminForth UI, call navigate_user instead of only sending a link.",
127
132
  "Try to call as many tools as possible in parallel in one step.",
128
133
  ];
129
134
 
@@ -4,6 +4,7 @@ import { createFetchToolSchemaTool } from "./fetchToolSchema.js";
4
4
  import type { ApiBasedTool } from "../../apiBasedTools.js";
5
5
  import { createApiTool } from "./apiTool.js";
6
6
  import { createGetUserLocationTool } from "./getUserLocation.js";
7
+ import { createNavigateUserTool } from "./navigateUser.js";
7
8
 
8
9
  export const ALWAYS_AVAILABLE_API_TOOL_NAMES = ["get_resource"] as const;
9
10
 
@@ -23,6 +24,7 @@ export async function createAgentTools(
23
24
  return createApiTool(toolName, apiBasedTool);
24
25
  }),
25
26
  createGetUserLocationTool(),
27
+ createNavigateUserTool(),
26
28
  await createFetchSkillTool(customComponentsDir, pluginCustomFolderPaths),
27
29
  await createFetchToolSchemaTool(apiBasedTools),
28
30
  ];
@@ -0,0 +1,210 @@
1
+ import { tool } from "langchain";
2
+ import { z } from "zod";
3
+ import type { CurrentPageContext } from "./getUserLocation.js";
4
+ import type { AgentEventEmitter } from "../../agentEvents.js";
5
+
6
+ const filterSchema = z.object({
7
+ column: z.string().min(1).describe("Resource column name."),
8
+ operator: z
9
+ .string()
10
+ .min(1)
11
+ .describe("Filter operator suffix, for example eq, gte, lte, like, in."),
12
+ value: z.unknown().describe("Filter value. Dates should be ISO strings."),
13
+ });
14
+
15
+ const navigateUserSchema = z
16
+ .object({
17
+ targetPath: z
18
+ .string()
19
+ .optional()
20
+ .describe(
21
+ "Root-relative AdminForth path to open, with optional query string and hash, for example /resource/adminuser?sort=created_at__desc.",
22
+ ),
23
+ resourceId: z
24
+ .string()
25
+ .optional()
26
+ .describe("Resource id to build an AdminForth resource route for."),
27
+ mode: z
28
+ .enum(["list", "show", "edit", "create"])
29
+ .optional()
30
+ .default("list")
31
+ .describe("Resource page mode. Defaults to list."),
32
+ recordId: z
33
+ .union([z.string(), z.number()])
34
+ .optional()
35
+ .describe("Record primary key for show or edit resource pages."),
36
+ filters: z
37
+ .array(filterSchema)
38
+ .optional()
39
+ .describe(
40
+ "List page filters. Each item becomes filter__{column}__{operator}=JSON.stringify(value).",
41
+ ),
42
+ sort: z
43
+ .object({
44
+ column: z.string().min(1),
45
+ direction: z.enum(["asc", "desc"]),
46
+ })
47
+ .optional()
48
+ .describe("List page sort. Becomes sort={column}__{direction}."),
49
+ query: z
50
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
51
+ .optional()
52
+ .describe("Additional query parameters to append to the target URL."),
53
+ })
54
+ .refine((input) => input.targetPath || input.resourceId, {
55
+ message: "Either targetPath or resourceId is required.",
56
+ });
57
+
58
+ function normalizeTargetPath(targetPath: string, currentPage?: CurrentPageContext) {
59
+ const trimmed = targetPath.trim();
60
+
61
+ if (!trimmed) {
62
+ throw new Error("targetPath cannot be empty.");
63
+ }
64
+
65
+ const currentOrigin = currentPage?.url ? new URL(currentPage.url).origin : undefined;
66
+
67
+ if (currentOrigin) {
68
+ const targetUrl = new URL(trimmed, currentOrigin);
69
+
70
+ if (targetUrl.origin !== currentOrigin) {
71
+ throw new Error("Only same-origin navigation targets are allowed.");
72
+ }
73
+
74
+ return `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
75
+ }
76
+
77
+ const fallbackOrigin = "http://adminforth.local";
78
+ const targetUrl = new URL(trimmed, fallbackOrigin);
79
+
80
+ if (targetUrl.origin !== fallbackOrigin) {
81
+ throw new Error("Only relative AdminForth paths are allowed when current origin is unavailable.");
82
+ }
83
+
84
+ return `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
85
+ }
86
+
87
+ function appendQueryParams(path: string, params: URLSearchParams) {
88
+ const queryString = params.toString();
89
+
90
+ if (!queryString) {
91
+ return path;
92
+ }
93
+
94
+ const hashIndex = path.indexOf("#");
95
+ const pathWithoutHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
96
+ const hash = hashIndex === -1 ? "" : path.slice(hashIndex);
97
+ const separator = pathWithoutHash.includes("?") ? "&" : "?";
98
+
99
+ return `${pathWithoutHash}${separator}${queryString}${hash}`;
100
+ }
101
+
102
+ function buildResourcePath(input: z.infer<typeof navigateUserSchema>) {
103
+ if (!input.resourceId) {
104
+ throw new Error("resourceId is required to build a resource route.");
105
+ }
106
+
107
+ const resourceId = encodeURIComponent(input.resourceId);
108
+ const mode = input.mode ?? "list";
109
+
110
+ if (mode === "show" || mode === "edit") {
111
+ if (input.recordId === undefined || input.recordId === null) {
112
+ throw new Error(`recordId is required for ${mode} resource pages.`);
113
+ }
114
+
115
+ return `/resource/${resourceId}/${mode}/${encodeURIComponent(String(input.recordId))}`;
116
+ }
117
+
118
+ if (mode === "create") {
119
+ return `/resource/${resourceId}/create`;
120
+ }
121
+
122
+ return `/resource/${resourceId}`;
123
+ }
124
+
125
+ function buildQueryParams(input: z.infer<typeof navigateUserSchema>) {
126
+ const params = new URLSearchParams();
127
+
128
+ for (const filter of input.filters ?? []) {
129
+ params.set(
130
+ `filter__${filter.column}__${filter.operator}`,
131
+ JSON.stringify(filter.value),
132
+ );
133
+ }
134
+
135
+ if (input.sort) {
136
+ params.set("sort", `${input.sort.column}__${input.sort.direction}`);
137
+ }
138
+
139
+ for (const [key, value] of Object.entries(input.query ?? {})) {
140
+ params.set(key, String(value));
141
+ }
142
+
143
+ return params;
144
+ }
145
+
146
+ function buildSurfaceUrl(targetPath: string, adminBaseUrl?: string, adminPublicOrigin?: string) {
147
+ const normalizedBasePath = adminBaseUrl?.replace(/\/+$/, "") ?? "";
148
+ const normalizedTargetPath = targetPath.replace(/^\/+/, "");
149
+ const path = `${normalizedBasePath}/${normalizedTargetPath}`;
150
+
151
+ return adminPublicOrigin ? new URL(path, adminPublicOrigin).toString() : path;
152
+ }
153
+
154
+ export function createNavigateUserTool() {
155
+ return tool(
156
+ async (input, runtime) => {
157
+ const context = runtime.context as {
158
+ currentPage?: CurrentPageContext;
159
+ chatSurface?: string;
160
+ adminBaseUrl?: string;
161
+ adminPublicOrigin?: string;
162
+ emitAgentEvent?: AgentEventEmitter;
163
+ };
164
+ const currentPage = context.currentPage;
165
+ const basePath = input.targetPath
166
+ ? normalizeTargetPath(input.targetPath, currentPage)
167
+ : buildResourcePath(input);
168
+ const targetPath = appendQueryParams(basePath, buildQueryParams(input));
169
+
170
+ if (context.chatSurface) {
171
+ const url = buildSurfaceUrl(targetPath, context.adminBaseUrl, context.adminPublicOrigin);
172
+
173
+ return JSON.stringify(
174
+ {
175
+ status: 200,
176
+ action: "link",
177
+ surface: context.chatSurface,
178
+ targetPath,
179
+ url,
180
+ message: `Send this link to the user: ${url}`,
181
+ },
182
+ null,
183
+ 2,
184
+ );
185
+ }
186
+
187
+ await context.emitAgentEvent?.({
188
+ type: "open-page",
189
+ targetPath,
190
+ });
191
+
192
+ return JSON.stringify(
193
+ {
194
+ status: 200,
195
+ action: "navigate",
196
+ targetPath,
197
+ message: `Navigation requested to ${targetPath}.`,
198
+ },
199
+ null,
200
+ 2,
201
+ );
202
+ },
203
+ {
204
+ name: "navigate_user",
205
+ description:
206
+ "Navigate the user to another AdminForth page. Use this only when the user asks to open, show, go to, or switch to a resource list/detail page, including filtered or sorted resource lists. Or if the user is asked to open something on the left. Do not use this tool in any other case.",
207
+ schema: navigateUserSchema,
208
+ },
209
+ );
210
+ }
package/agentEvents.ts CHANGED
@@ -22,6 +22,10 @@ export type AgentEvent =
22
22
  phase: "start" | "end";
23
23
  label: string;
24
24
  }
25
+ | {
26
+ type: "open-page";
27
+ targetPath: string;
28
+ }
25
29
  | {
26
30
  type: "transcript";
27
31
  text: string;
@@ -23,6 +23,8 @@ type AgentTurnRunInput = {
23
23
  modeName?: string | null;
24
24
  userTimeZone: string;
25
25
  currentPage?: CurrentPageContext;
26
+ chatSurface?: string;
27
+ adminPublicOrigin?: string;
26
28
  abortSignal?: AbortSignal;
27
29
  adminUser: AdminUser;
28
30
  sequenceDebugCollector: ReturnType<typeof createSequenceDebugCollector>;
@@ -35,6 +37,8 @@ export type RunAndPersistAgentResponseInput = {
35
37
  modeName?: string | null;
36
38
  userTimeZone: string;
37
39
  currentPage?: CurrentPageContext;
40
+ chatSurface?: string;
41
+ adminPublicOrigin?: string;
38
42
  abortSignal?: AbortSignal;
39
43
  adminUser: AdminUser;
40
44
  emit?: AgentEventEmitter;
@@ -118,6 +122,7 @@ export class AgentTurnService {
118
122
  adminUser: input.adminUser,
119
123
  usernameField: adminforth.config.auth!.usernameField,
120
124
  userLanguage,
125
+ chatSurface: input.chatSurface,
121
126
  });
122
127
  const apiBasedTools = buildApiBasedTools(
123
128
  adminforth,
@@ -142,6 +147,8 @@ export class AgentTurnService {
142
147
  sessionId: input.sessionId,
143
148
  turnId: input.turnId,
144
149
  currentPage: input.currentPage,
150
+ chatSurface: input.chatSurface,
151
+ adminPublicOrigin: input.adminPublicOrigin,
145
152
  userTimeZone: input.userTimeZone,
146
153
  abortSignal: input.abortSignal,
147
154
  emitToolCallEvent: (event) => {
@@ -151,6 +158,7 @@ export class AgentTurnService {
151
158
  data: event,
152
159
  });
153
160
  },
161
+ emitAgentEvent: input.emit,
154
162
  sequenceDebugSink: input.sequenceDebugCollector,
155
163
  });
156
164
 
@@ -278,6 +286,8 @@ export class AgentTurnService {
278
286
  modeName: input.modeName,
279
287
  userTimeZone: input.userTimeZone,
280
288
  currentPage: input.currentPage,
289
+ chatSurface: input.chatSurface,
290
+ adminPublicOrigin: input.adminPublicOrigin,
281
291
  abortSignal: input.abortSignal,
282
292
  adminUser: input.adminUser,
283
293
  sequenceDebugCollector,
@@ -326,6 +336,8 @@ export class AgentTurnService {
326
336
  modeName: input.modeName,
327
337
  userTimeZone: input.userTimeZone,
328
338
  currentPage: input.currentPage,
339
+ chatSurface: input.chatSurface,
340
+ adminPublicOrigin: input.adminPublicOrigin,
329
341
  abortSignal: input.abortSignal,
330
342
  adminUser: input.adminUser,
331
343
  emit: input.emit,
@@ -408,6 +420,8 @@ export class AgentTurnService {
408
420
  modeName: input.modeName,
409
421
  userTimeZone: input.userTimeZone,
410
422
  currentPage: input.currentPage,
423
+ chatSurface: input.chatSurface,
424
+ adminPublicOrigin: input.adminPublicOrigin,
411
425
  abortSignal: input.abortSignal,
412
426
  adminUser: input.adminUser,
413
427
  emit: async (event) => {
package/build.log CHANGED
@@ -62,5 +62,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
62
62
  custom/speech_recognition_frontend/types/
63
63
  custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
64
64
 
65
- sent 1,668,700 bytes received 921 bytes 3,339,242.00 bytes/sec
66
- total size is 1,664,547 speedup is 1.00
65
+ sent 1,670,250 bytes received 921 bytes 3,342,342.00 bytes/sec
66
+ total size is 1,666,155 speedup is 1.00
@@ -184,6 +184,9 @@ export class ChatSurfaceService {
184
184
  options?: { emitDone?: boolean },
185
185
  ) {
186
186
  const emitDone = options?.emitDone ?? true;
187
+ const adminPublicOrigin = typeof incoming.metadata?.adminPublicOrigin === "string"
188
+ ? incoming.metadata.adminPublicOrigin
189
+ : undefined;
187
190
  const sessionId = await this.sessionStore.getOrCreateChatSurfaceSession(
188
191
  { ...incoming, prompt },
189
192
  adminUser,
@@ -195,6 +198,8 @@ export class ChatSurfaceService {
195
198
  sessionId,
196
199
  modeName: incoming.modeName,
197
200
  userTimeZone: incoming.userTimeZone ?? "UTC",
201
+ chatSurface: incoming.surface,
202
+ adminPublicOrigin,
198
203
  adminUser,
199
204
  emit: this.createEventEmitter(sink),
200
205
  failureLogMessage: `Agent ${incoming.surface} surface response failed`,
@@ -208,6 +213,8 @@ export class ChatSurfaceService {
208
213
  sessionId,
209
214
  modeName: incoming.modeName,
210
215
  userTimeZone: incoming.userTimeZone ?? "UTC",
216
+ chatSurface: incoming.surface,
217
+ adminPublicOrigin,
211
218
  adminUser,
212
219
  emit: this.createEventEmitter(sink),
213
220
  failureLogMessage: `Agent ${incoming.surface} surface response failed`,
@@ -13,11 +13,13 @@ type AgentImportMeta = ImportMeta & {
13
13
  type CreateAgentChatManagerOptions = {
14
14
  lastMessage: Ref<string>;
15
15
  activeModeName: Ref<string | null>;
16
+ onOpenPage: (targetPath: string) => void;
16
17
  };
17
18
 
18
19
  export function createAgentChatManager({
19
20
  lastMessage,
20
21
  activeModeName,
22
+ onOpenPage,
21
23
  }: CreateAgentChatManagerOptions) {
22
24
  const chats = new Map<string, Chat<any>>();
23
25
  const currentChat = shallowRef<Chat<any> | null>();
@@ -52,6 +54,11 @@ export function createAgentChatManager({
52
54
  onError(error: unknown) {
53
55
  console.error('Chat error:', error);
54
56
  },
57
+ onData(dataPart: any) {
58
+ if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
59
+ onOpenPage(dataPart.data.targetPath);
60
+ }
61
+ },
55
62
  });
56
63
  chats.set(sessionId, newChat);
57
64
  currentChat.value = newChat;
@@ -67,4 +74,4 @@ export function createAgentChatManager({
67
74
  setCurrentChat,
68
75
  abortCurrentChatRequest,
69
76
  };
70
- }
77
+ }
@@ -187,6 +187,11 @@ export const useAgentAudio = defineStore('agentAudio', () => {
187
187
  playStandByAudio();
188
188
  }
189
189
  agentStore.addDataToolCallMessage(event.data);
190
+ return;
191
+ }
192
+
193
+ if (event.type === 'open-page') {
194
+ agentStore.openAgentPage(event.data.targetPath);
190
195
  }
191
196
  }
192
197
 
@@ -1,6 +1,7 @@
1
1
  import { defineStore } from 'pinia';
2
2
  import { IAgentSession, ISessionsListItem } from '../types';
3
3
  import { ref, nextTick, computed, watch, onMounted } from 'vue';
4
+ import { useRouter } from 'vue-router';
4
5
  import { useAdminforth } from '@/adminforth';
5
6
  import { useCoreStore } from '@/stores/core';
6
7
  import { useAgentTransitions } from './useAgentTransitions';
@@ -28,6 +29,7 @@ export const useAgentStore = defineStore('agent', () => {
28
29
  const adminforth = useAdminforth();
29
30
  const isChatOpen = ref(false);
30
31
  const isSessionHistoryOpen = ref(false);
32
+ const router = useRouter();
31
33
  const textInput = ref<HTMLTextAreaElement | null>(null);
32
34
  const userMessageInput = ref();
33
35
  const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : '');
@@ -54,6 +56,7 @@ export const useAgentStore = defineStore('agent', () => {
54
56
  } = createAgentChatManager({
55
57
  lastMessage,
56
58
  activeModeName,
59
+ onOpenPage: openAgentPage,
57
60
  });
58
61
  const {
59
62
  userMessagePlaceholder,
@@ -324,6 +327,45 @@ export const useAgentStore = defineStore('agent', () => {
324
327
  addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.AGENT_RESPONSE_ABORTED);
325
328
  }
326
329
 
330
+ function resolveInternalRoute(href: string): string | null {
331
+ if (href.startsWith('#')) {
332
+ return `${window.location.pathname}${window.location.search}${href}`;
333
+ }
334
+
335
+ if (href.startsWith('//')) {
336
+ return null;
337
+ }
338
+
339
+ const isAbsoluteWithScheme = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(href);
340
+ const baseUrl = isAbsoluteWithScheme
341
+ ? undefined
342
+ : `${window.location.origin}/`;
343
+ const resolvedUrl = new URL(href, baseUrl ?? window.location.href);
344
+
345
+ if (resolvedUrl.origin !== window.location.origin) {
346
+ return null;
347
+ }
348
+
349
+ return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
350
+ }
351
+
352
+ function openAgentPage(targetPath: string) {
353
+ const internalRoute = resolveInternalRoute(targetPath);
354
+
355
+ if (internalRoute === null) {
356
+ console.warn('Ignoring external agent navigation target:', targetPath);
357
+ return;
358
+ }
359
+
360
+ if (isFullScreen.value && !coreStore.isMobile) {
361
+ setFullScreen(false);
362
+ } else if (coreStore.isMobile) {
363
+ setIsChatOpen(false);
364
+ }
365
+
366
+ void router.push(internalRoute);
367
+ }
368
+
327
369
  return {
328
370
  //_________-Sessions management-_____________
329
371
  activeSessionId,
@@ -374,6 +416,7 @@ export const useAgentStore = defineStore('agent', () => {
374
416
  addAgentMessage,
375
417
  addUserMessage,
376
418
  addDataToolCallMessage,
419
+ openAgentPage,
377
420
  setCurrentChatStatus,
378
421
  updateLastAgentMessage
379
422
  }
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "baseUrl": ".", // This should point to your project root
4
3
  "paths": {
5
4
  "@/*": [
6
5
  "../node_modules/adminforth/dist/spa/src/*"