@adminforth/agent 1.49.3 → 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 +1 -1
  37. package/surfaces/web-sse/createSseEventEmitter.ts +12 -0
package/custom/types.ts CHANGED
@@ -93,6 +93,12 @@ export type SpeechStreamEvent =
93
93
  type: 'data-tool-call';
94
94
  data: any;
95
95
  }
96
+ | {
97
+ type: 'open-page';
98
+ data: {
99
+ targetPath: string;
100
+ };
101
+ }
96
102
  | {
97
103
  type: 'finish';
98
104
  };
@@ -16,6 +16,9 @@ export type SequenceDebug = {
16
16
  reasoningTokens: number;
17
17
  text: string;
18
18
  textTokens: number;
19
+ uncachedInputTokens: number;
20
+ cachedInputTokens: number;
21
+ outputTokens: number;
19
22
  cachedTokens: number;
20
23
  responseId: string | null;
21
24
  toolCalls: SequenceDebugToolCall[];
@@ -28,6 +31,9 @@ type SequenceDebugModelCall = {
28
31
  reasoningTokens: number;
29
32
  text: string;
30
33
  textTokens: number;
34
+ uncachedInputTokens: number;
35
+ cachedInputTokens: number;
36
+ outputTokens: number;
31
37
  cachedTokens: number;
32
38
  responseId: string | null;
33
39
  resultType: SequenceDebugResultType;
@@ -30,6 +30,9 @@ function createPendingSequenceDebug(sequenceId) {
30
30
  reasoningTokens: 0,
31
31
  text: "",
32
32
  textTokens: 0,
33
+ uncachedInputTokens: 0,
34
+ cachedInputTokens: 0,
35
+ outputTokens: 0,
33
36
  cachedTokens: 0,
34
37
  responseId: null,
35
38
  toolCalls: [],
@@ -55,6 +58,9 @@ function finalizeSequenceDebug(sequence) {
55
58
  reasoningTokens: sequence.reasoningTokens,
56
59
  text: sequence.text,
57
60
  textTokens: sequence.textTokens,
61
+ uncachedInputTokens: sequence.uncachedInputTokens,
62
+ cachedInputTokens: sequence.cachedInputTokens,
63
+ outputTokens: sequence.outputTokens,
58
64
  cachedTokens: sequence.cachedTokens,
59
65
  responseId: sequence.responseId,
60
66
  toolCalls: sequence.toolCalls.map((_a) => {
@@ -68,6 +74,18 @@ function finalizeSequenceDebug(sequence) {
68
74
  function getDebugModelName(model) {
69
75
  return typeof model.getName === "function" ? model.getName() : undefined;
70
76
  }
77
+ function getDebugToolName(tool) {
78
+ if (!tool || typeof tool !== "object") {
79
+ return null;
80
+ }
81
+ const name = tool.name;
82
+ return typeof name === "string" ? name : null;
83
+ }
84
+ function formatToolsForDebug(tools) {
85
+ return tools.map((tool) => ({
86
+ name: getDebugToolName(tool),
87
+ }));
88
+ }
71
89
  function stringifyPromptForDebug(params) {
72
90
  var _a, _b, _c;
73
91
  const { model, systemMessage, messages, tools, toolChoice, modelSettings } = params;
@@ -79,7 +97,7 @@ function stringifyPromptForDebug(params) {
79
97
  provider: (_c = (_b = model._defaultConfig) === null || _b === void 0 ? void 0 : _b.modelProvider) !== null && _c !== void 0 ? _c : null,
80
98
  configuredModel: typeof model.model === "string" ? model.model : null,
81
99
  }, systemMessage,
82
- messages }, (tools.length > 0 ? { tools } : {})), (toolChoice !== undefined ? { toolChoice } : {})), (modelSettings ? { modelSettings } : {})), (invocationParams ? { invocationParams } : {})));
100
+ messages }, (tools.length > 0 ? { tools: formatToolsForDebug(tools) } : {})), (toolChoice !== undefined ? { toolChoice } : {})), (modelSettings ? { modelSettings } : {})), (invocationParams ? { invocationParams } : {})));
83
101
  }
84
102
  function getMessageBlocks(message) {
85
103
  if (Array.isArray(message.contentBlocks)) {
@@ -115,7 +133,10 @@ function countTokens(model, content) {
115
133
  });
116
134
  }
117
135
  function extractSequenceResponseDebug(message) {
118
- var _a, _b, _c, _d, _e, _f, _g;
136
+ var _a, _b, _c, _d, _e, _f;
137
+ const usageMetadata = message.usage_metadata;
138
+ const promptTokens = (_a = usageMetadata === null || usageMetadata === void 0 ? void 0 : usageMetadata.input_tokens) !== null && _a !== void 0 ? _a : 0;
139
+ const cachedInputTokens = (_c = (_b = usageMetadata === null || usageMetadata === void 0 ? void 0 : usageMetadata.input_token_details) === null || _b === void 0 ? void 0 : _b.cache_read) !== null && _c !== void 0 ? _c : 0;
119
140
  const blocks = getMessageBlocks(message);
120
141
  const reasoning = blocks
121
142
  .filter((block) => (block === null || block === void 0 ? void 0 : block.type) === "reasoning")
@@ -126,13 +147,16 @@ function extractSequenceResponseDebug(message) {
126
147
  .map((block) => { var _a; return String((_a = block.text) !== null && _a !== void 0 ? _a : ""); })
127
148
  .join("");
128
149
  return {
129
- promptTokens: (_b = (_a = message.usage_metadata) === null || _a === void 0 ? void 0 : _a.input_tokens) !== null && _b !== void 0 ? _b : 0,
150
+ promptTokens,
130
151
  reasoning,
131
152
  reasoningTokens: 0,
132
153
  text: textFromBlocks || (typeof message.content === "string" ? message.content : ""),
133
154
  textTokens: 0,
134
- cachedTokens: (_e = (_d = (_c = message.usage_metadata) === null || _c === void 0 ? void 0 : _c.input_token_details) === null || _d === void 0 ? void 0 : _d.cache_read) !== null && _e !== void 0 ? _e : 0,
135
- responseId: (_g = (_f = message.response_metadata) === null || _f === void 0 ? void 0 : _f.id) !== null && _g !== void 0 ? _g : null,
155
+ uncachedInputTokens: Math.max(promptTokens - cachedInputTokens, 0),
156
+ cachedInputTokens,
157
+ outputTokens: (_d = usageMetadata === null || usageMetadata === void 0 ? void 0 : usageMetadata.output_tokens) !== null && _d !== void 0 ? _d : 0,
158
+ cachedTokens: cachedInputTokens,
159
+ responseId: (_f = (_e = message.response_metadata) === null || _e === void 0 ? void 0 : _e.id) !== null && _f !== void 0 ? _f : null,
136
160
  resultType: hasToolCallSignal(message) ? "tool_calls" : "final_text",
137
161
  };
138
162
  }
@@ -171,6 +195,9 @@ export function createSequenceDebugCollector() {
171
195
  sequenceDebug.reasoningTokens = params.reasoningTokens;
172
196
  sequenceDebug.text = params.text;
173
197
  sequenceDebug.textTokens = params.textTokens;
198
+ sequenceDebug.uncachedInputTokens = params.uncachedInputTokens;
199
+ sequenceDebug.cachedInputTokens = params.cachedInputTokens;
200
+ sequenceDebug.outputTokens = params.outputTokens;
174
201
  sequenceDebug.cachedTokens = params.cachedTokens;
175
202
  sequenceDebug.responseId = params.responseId;
176
203
  sequenceDebug.resultType = params.resultType;
@@ -244,7 +271,7 @@ export function createSequenceDebugMiddleware(sink) {
244
271
  ]);
245
272
  sink.handleModelCallComplete(Object.assign(Object.assign({}, debug), { promptTokens,
246
273
  reasoningTokens,
247
- textTokens }));
274
+ textTokens, uncachedInputTokens: debug.promptTokens ? debug.uncachedInputTokens : promptTokens, outputTokens: debug.outputTokens || reasoningTokens + textTokens }));
248
275
  return response;
249
276
  });
250
277
  },
@@ -6,6 +6,7 @@ import { createSequenceDebugMiddleware, type SequenceDebugModelCallSink } from "
6
6
  import type { ApiBasedTool } from "../apiBasedTools.js";
7
7
  import type { ToolCallEventSink } from "./toolCallEvents.js";
8
8
  import type { CurrentPageContext } from "./tools/getUserLocation.js";
9
+ import type { AgentEventEmitter } from "../agentEvents.js";
9
10
  export declare const contextSchema: z.ZodObject<{
10
11
  adminUser: z.ZodCustom<AdminUser, AdminUser>;
11
12
  userTimeZone: z.ZodString;
@@ -13,7 +14,11 @@ export declare const contextSchema: z.ZodObject<{
13
14
  turnId: z.ZodString;
14
15
  abortSignal: z.ZodOptional<z.ZodCustom<AbortSignal, AbortSignal>>;
15
16
  currentPage: z.ZodOptional<z.ZodCustom<CurrentPageContext, CurrentPageContext>>;
17
+ chatSurface: z.ZodOptional<z.ZodString>;
18
+ adminBaseUrl: z.ZodOptional<z.ZodString>;
19
+ adminPublicOrigin: z.ZodOptional<z.ZodString>;
16
20
  emitToolCallEvent: z.ZodCustom<ToolCallEventSink, ToolCallEventSink>;
21
+ emitAgentEvent: z.ZodOptional<z.ZodCustom<AgentEventEmitter, AgentEventEmitter>>;
17
22
  }, z.core.$strip>;
18
23
  export type AgentChatModel = BaseChatModel<any, any>;
19
24
  export type AgentModelPurpose = "primary" | "summary";
@@ -54,9 +59,12 @@ export declare function callAgent(params: {
54
59
  sessionId: string;
55
60
  turnId: string;
56
61
  currentPage?: CurrentPageContext;
62
+ chatSurface?: string;
63
+ adminPublicOrigin?: string;
57
64
  userTimeZone: string;
58
65
  abortSignal?: AbortSignal;
59
66
  emitToolCallEvent: ToolCallEventSink;
67
+ emitAgentEvent?: AgentEventEmitter;
60
68
  sequenceDebugSink: SequenceDebugModelCallSink;
61
69
  }): Promise<import("@langchain/core/utils/stream").IterableReadableStream<[import("langchain").BaseMessage<import("@langchain/core/messages").MessageStructure<import("@langchain/core/messages").MessageToolSet>, import("@langchain/core/messages").MessageType>, Record<string, any>]>>;
62
70
  export {};
@@ -21,7 +21,11 @@ export const contextSchema = z.object({
21
21
  turnId: z.string(),
22
22
  abortSignal: z.custom().optional(),
23
23
  currentPage: z.custom().optional(),
24
+ chatSurface: z.string().optional(),
25
+ adminBaseUrl: z.string().optional(),
26
+ adminPublicOrigin: z.string().optional(),
24
27
  emitToolCallEvent: z.custom(),
28
+ emitAgentEvent: z.custom().optional(),
25
29
  });
26
30
  function isLangChainAgentCompletionAdapter(adapter) {
27
31
  return typeof adapter
@@ -132,7 +136,7 @@ export function createAgentChatModel(params) {
132
136
  }
133
137
  export function callAgent(params) {
134
138
  return __awaiter(this, void 0, void 0, function* () {
135
- const { name, model, summaryModel, modelMiddleware = [], checkpointer, messages, adminUser, adminforth, apiBasedTools, customComponentsDir, pluginCustomFolderPaths, sessionId, turnId, currentPage, userTimeZone, abortSignal, emitToolCallEvent, sequenceDebugSink, } = params;
139
+ const { name, model, summaryModel, modelMiddleware = [], checkpointer, messages, adminUser, adminforth, apiBasedTools, customComponentsDir, pluginCustomFolderPaths, sessionId, turnId, currentPage, chatSurface, adminPublicOrigin, userTimeZone, abortSignal, emitToolCallEvent, emitAgentEvent, sequenceDebugSink, } = params;
136
140
  const tools = yield createAgentTools(customComponentsDir, apiBasedTools, pluginCustomFolderPaths);
137
141
  const apiBasedToolsMiddleware = createApiBasedToolsMiddleware(apiBasedTools, adminforth);
138
142
  const sequenceDebugMiddleware = createSequenceDebugMiddleware(sequenceDebugSink);
@@ -169,7 +173,11 @@ export function callAgent(params) {
169
173
  turnId,
170
174
  abortSignal,
171
175
  currentPage,
176
+ chatSurface,
177
+ adminBaseUrl: adminforth.config.baseUrlSlashed,
178
+ adminPublicOrigin,
172
179
  emitToolCallEvent,
180
+ emitAgentEvent,
173
181
  },
174
182
  });
175
183
  });
@@ -7,5 +7,6 @@ export declare function buildAgentTurnSystemPrompt(input: {
7
7
  adminUser: AdminUser;
8
8
  usernameField: string;
9
9
  userLanguage: DetectedLanguage | null;
10
+ chatSurface?: string;
10
11
  }): string;
11
12
  export declare function buildAgentSystemPrompt(adminforth: IAdminForth, hiddenResourceIds?: Iterable<string>): Promise<string>;
@@ -58,8 +58,11 @@ export function buildAgentTurnSystemPrompt(input) {
58
58
  return [
59
59
  input.agentSystemPrompt,
60
60
  formatAdminUserPrompt(input.adminUser, input.usernameField),
61
+ input.chatSurface
62
+ ? `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.`
63
+ : "",
61
64
  formatLanguagePrompt(input.userLanguage),
62
- ].join("\n\n");
65
+ ].filter(Boolean).join("\n\n");
63
66
  }
64
67
  function formatResources(resources) {
65
68
  return resources
@@ -99,6 +102,7 @@ export function buildAgentSystemPrompt(adminforth_1) {
99
102
  "When fetch_tool_schema succeeds, that tool becomes available on the next step.",
100
103
  "All admin links must be root-relative and start with '/'.",
101
104
  "Build record links as '/resource/{resourceId}/show/{primary key}'. Never use bare 'resource/{resourceId}/show/{primary key}' without the leading slash.",
105
+ "When the user asks to open or show a page in the AdminForth UI, call navigate_user instead of only sending a link.",
102
106
  "Try to call as many tools as possible in parallel in one step.",
103
107
  ];
104
108
  return sections.filter(Boolean).join("\n\n");
@@ -11,6 +11,7 @@ import { createFetchSkillTool } from "./fetchSkill.js";
11
11
  import { createFetchToolSchemaTool } from "./fetchToolSchema.js";
12
12
  import { createApiTool } from "./apiTool.js";
13
13
  import { createGetUserLocationTool } from "./getUserLocation.js";
14
+ import { createNavigateUserTool } from "./navigateUser.js";
14
15
  export const ALWAYS_AVAILABLE_API_TOOL_NAMES = ["get_resource"];
15
16
  export function createAgentTools(customComponentsDir_1, apiBasedTools_1) {
16
17
  return __awaiter(this, arguments, void 0, function* (customComponentsDir, apiBasedTools, pluginCustomFolderPaths = []) {
@@ -23,6 +24,7 @@ export function createAgentTools(customComponentsDir_1, apiBasedTools_1) {
23
24
  return createApiTool(toolName, apiBasedTool);
24
25
  }),
25
26
  createGetUserLocationTool(),
27
+ createNavigateUserTool(),
26
28
  yield createFetchSkillTool(customComponentsDir, pluginCustomFolderPaths),
27
29
  yield createFetchToolSchemaTool(apiBasedTools),
28
30
  ];
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ export declare function createNavigateUserTool(): import("langchain").DynamicStructuredTool<z.ZodObject<{
3
+ targetPath: z.ZodOptional<z.ZodString>;
4
+ resourceId: z.ZodOptional<z.ZodString>;
5
+ mode: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
6
+ list: "list";
7
+ show: "show";
8
+ edit: "edit";
9
+ create: "create";
10
+ }>>>;
11
+ recordId: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
12
+ filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
13
+ column: z.ZodString;
14
+ operator: z.ZodString;
15
+ value: z.ZodUnknown;
16
+ }, z.core.$strip>>>;
17
+ sort: z.ZodOptional<z.ZodObject<{
18
+ column: z.ZodString;
19
+ direction: z.ZodEnum<{
20
+ asc: "asc";
21
+ desc: "desc";
22
+ }>;
23
+ }, z.core.$strip>>;
24
+ query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
25
+ }, z.core.$strip>, {
26
+ mode: "list" | "show" | "edit" | "create";
27
+ targetPath?: string | undefined;
28
+ resourceId?: string | undefined;
29
+ recordId?: string | number | undefined;
30
+ filters?: {
31
+ column: string;
32
+ operator: string;
33
+ value: unknown;
34
+ }[] | undefined;
35
+ sort?: {
36
+ column: string;
37
+ direction: "asc" | "desc";
38
+ } | undefined;
39
+ query?: Record<string, string | number | boolean> | undefined;
40
+ }, {
41
+ targetPath?: string | undefined;
42
+ resourceId?: string | undefined;
43
+ mode?: "list" | "show" | "edit" | "create" | undefined;
44
+ recordId?: string | number | undefined;
45
+ filters?: {
46
+ column: string;
47
+ operator: string;
48
+ value: unknown;
49
+ }[] | undefined;
50
+ sort?: {
51
+ column: string;
52
+ direction: "asc" | "desc";
53
+ } | undefined;
54
+ query?: Record<string, string | number | boolean> | undefined;
55
+ }, string, unknown, "navigate_user">;
@@ -0,0 +1,163 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { tool } from "langchain";
11
+ import { z } from "zod";
12
+ const filterSchema = z.object({
13
+ column: z.string().min(1).describe("Resource column name."),
14
+ operator: z
15
+ .string()
16
+ .min(1)
17
+ .describe("Filter operator suffix, for example eq, gte, lte, like, in."),
18
+ value: z.unknown().describe("Filter value. Dates should be ISO strings."),
19
+ });
20
+ const navigateUserSchema = z
21
+ .object({
22
+ targetPath: z
23
+ .string()
24
+ .optional()
25
+ .describe("Root-relative AdminForth path to open, with optional query string and hash, for example /resource/adminuser?sort=created_at__desc."),
26
+ resourceId: z
27
+ .string()
28
+ .optional()
29
+ .describe("Resource id to build an AdminForth resource route for."),
30
+ mode: z
31
+ .enum(["list", "show", "edit", "create"])
32
+ .optional()
33
+ .default("list")
34
+ .describe("Resource page mode. Defaults to list."),
35
+ recordId: z
36
+ .union([z.string(), z.number()])
37
+ .optional()
38
+ .describe("Record primary key for show or edit resource pages."),
39
+ filters: z
40
+ .array(filterSchema)
41
+ .optional()
42
+ .describe("List page filters. Each item becomes filter__{column}__{operator}=JSON.stringify(value)."),
43
+ sort: z
44
+ .object({
45
+ column: z.string().min(1),
46
+ direction: z.enum(["asc", "desc"]),
47
+ })
48
+ .optional()
49
+ .describe("List page sort. Becomes sort={column}__{direction}."),
50
+ query: z
51
+ .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
52
+ .optional()
53
+ .describe("Additional query parameters to append to the target URL."),
54
+ })
55
+ .refine((input) => input.targetPath || input.resourceId, {
56
+ message: "Either targetPath or resourceId is required.",
57
+ });
58
+ function normalizeTargetPath(targetPath, currentPage) {
59
+ const trimmed = targetPath.trim();
60
+ if (!trimmed) {
61
+ throw new Error("targetPath cannot be empty.");
62
+ }
63
+ const currentOrigin = (currentPage === null || currentPage === void 0 ? void 0 : currentPage.url) ? new URL(currentPage.url).origin : undefined;
64
+ if (currentOrigin) {
65
+ const targetUrl = new URL(trimmed, currentOrigin);
66
+ if (targetUrl.origin !== currentOrigin) {
67
+ throw new Error("Only same-origin navigation targets are allowed.");
68
+ }
69
+ return `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
70
+ }
71
+ const fallbackOrigin = "http://adminforth.local";
72
+ const targetUrl = new URL(trimmed, fallbackOrigin);
73
+ if (targetUrl.origin !== fallbackOrigin) {
74
+ throw new Error("Only relative AdminForth paths are allowed when current origin is unavailable.");
75
+ }
76
+ return `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
77
+ }
78
+ function appendQueryParams(path, params) {
79
+ const queryString = params.toString();
80
+ if (!queryString) {
81
+ return path;
82
+ }
83
+ const hashIndex = path.indexOf("#");
84
+ const pathWithoutHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
85
+ const hash = hashIndex === -1 ? "" : path.slice(hashIndex);
86
+ const separator = pathWithoutHash.includes("?") ? "&" : "?";
87
+ return `${pathWithoutHash}${separator}${queryString}${hash}`;
88
+ }
89
+ function buildResourcePath(input) {
90
+ var _a;
91
+ if (!input.resourceId) {
92
+ throw new Error("resourceId is required to build a resource route.");
93
+ }
94
+ const resourceId = encodeURIComponent(input.resourceId);
95
+ const mode = (_a = input.mode) !== null && _a !== void 0 ? _a : "list";
96
+ if (mode === "show" || mode === "edit") {
97
+ if (input.recordId === undefined || input.recordId === null) {
98
+ throw new Error(`recordId is required for ${mode} resource pages.`);
99
+ }
100
+ return `/resource/${resourceId}/${mode}/${encodeURIComponent(String(input.recordId))}`;
101
+ }
102
+ if (mode === "create") {
103
+ return `/resource/${resourceId}/create`;
104
+ }
105
+ return `/resource/${resourceId}`;
106
+ }
107
+ function buildQueryParams(input) {
108
+ var _a, _b;
109
+ const params = new URLSearchParams();
110
+ for (const filter of (_a = input.filters) !== null && _a !== void 0 ? _a : []) {
111
+ params.set(`filter__${filter.column}__${filter.operator}`, JSON.stringify(filter.value));
112
+ }
113
+ if (input.sort) {
114
+ params.set("sort", `${input.sort.column}__${input.sort.direction}`);
115
+ }
116
+ for (const [key, value] of Object.entries((_b = input.query) !== null && _b !== void 0 ? _b : {})) {
117
+ params.set(key, String(value));
118
+ }
119
+ return params;
120
+ }
121
+ function buildSurfaceUrl(targetPath, adminBaseUrl, adminPublicOrigin) {
122
+ var _a;
123
+ const normalizedBasePath = (_a = adminBaseUrl === null || adminBaseUrl === void 0 ? void 0 : adminBaseUrl.replace(/\/+$/, "")) !== null && _a !== void 0 ? _a : "";
124
+ const normalizedTargetPath = targetPath.replace(/^\/+/, "");
125
+ const path = `${normalizedBasePath}/${normalizedTargetPath}`;
126
+ return adminPublicOrigin ? new URL(path, adminPublicOrigin).toString() : path;
127
+ }
128
+ export function createNavigateUserTool() {
129
+ return tool((input, runtime) => __awaiter(this, void 0, void 0, function* () {
130
+ var _a;
131
+ const context = runtime.context;
132
+ const currentPage = context.currentPage;
133
+ const basePath = input.targetPath
134
+ ? normalizeTargetPath(input.targetPath, currentPage)
135
+ : buildResourcePath(input);
136
+ const targetPath = appendQueryParams(basePath, buildQueryParams(input));
137
+ if (context.chatSurface) {
138
+ const url = buildSurfaceUrl(targetPath, context.adminBaseUrl, context.adminPublicOrigin);
139
+ return JSON.stringify({
140
+ status: 200,
141
+ action: "link",
142
+ surface: context.chatSurface,
143
+ targetPath,
144
+ url,
145
+ message: `Send this link to the user: ${url}`,
146
+ }, null, 2);
147
+ }
148
+ yield ((_a = context.emitAgentEvent) === null || _a === void 0 ? void 0 : _a.call(context, {
149
+ type: "open-page",
150
+ targetPath,
151
+ }));
152
+ return JSON.stringify({
153
+ status: 200,
154
+ action: "navigate",
155
+ targetPath,
156
+ message: `Navigation requested to ${targetPath}.`,
157
+ }, null, 2);
158
+ }), {
159
+ name: "navigate_user",
160
+ description: "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.",
161
+ schema: navigateUserSchema,
162
+ });
163
+ }
@@ -15,6 +15,9 @@ export type AgentEvent = {
15
15
  type: "rendering";
16
16
  phase: "start" | "end";
17
17
  label: string;
18
+ } | {
19
+ type: "open-page";
20
+ targetPath: string;
18
21
  } | {
19
22
  type: "transcript";
20
23
  text: string;
@@ -10,6 +10,8 @@ export type RunAndPersistAgentResponseInput = {
10
10
  modeName?: string | null;
11
11
  userTimeZone: string;
12
12
  currentPage?: CurrentPageContext;
13
+ chatSurface?: string;
14
+ adminPublicOrigin?: string;
13
15
  abortSignal?: AbortSignal;
14
16
  adminUser: AdminUser;
15
17
  emit?: AgentEventEmitter;
@@ -70,6 +70,7 @@ export class AgentTurnService {
70
70
  adminUser: input.adminUser,
71
71
  usernameField: adminforth.config.auth.usernameField,
72
72
  userLanguage,
73
+ chatSurface: input.chatSurface,
73
74
  });
74
75
  const apiBasedTools = buildApiBasedTools(adminforth, this.serviceOptions.getInternalAgentResourceIds());
75
76
  const stream = yield callAgent({
@@ -90,6 +91,8 @@ export class AgentTurnService {
90
91
  sessionId: input.sessionId,
91
92
  turnId: input.turnId,
92
93
  currentPage: input.currentPage,
94
+ chatSurface: input.chatSurface,
95
+ adminPublicOrigin: input.adminPublicOrigin,
93
96
  userTimeZone: input.userTimeZone,
94
97
  abortSignal: input.abortSignal,
95
98
  emitToolCallEvent: (event) => {
@@ -100,6 +103,7 @@ export class AgentTurnService {
100
103
  data: event,
101
104
  }));
102
105
  },
106
+ emitAgentEvent: input.emit,
103
107
  sequenceDebugSink: input.sequenceDebugCollector,
104
108
  });
105
109
  try {
@@ -221,6 +225,8 @@ export class AgentTurnService {
221
225
  modeName: input.modeName,
222
226
  userTimeZone: input.userTimeZone,
223
227
  currentPage: input.currentPage,
228
+ chatSurface: input.chatSurface,
229
+ adminPublicOrigin: input.adminPublicOrigin,
224
230
  abortSignal: input.abortSignal,
225
231
  adminUser: input.adminUser,
226
232
  sequenceDebugCollector,
@@ -268,6 +274,8 @@ export class AgentTurnService {
268
274
  modeName: input.modeName,
269
275
  userTimeZone: input.userTimeZone,
270
276
  currentPage: input.currentPage,
277
+ chatSurface: input.chatSurface,
278
+ adminPublicOrigin: input.adminPublicOrigin,
271
279
  abortSignal: input.abortSignal,
272
280
  adminUser: input.adminUser,
273
281
  emit: input.emit,
@@ -345,6 +353,8 @@ export class AgentTurnService {
345
353
  modeName: input.modeName,
346
354
  userTimeZone: input.userTimeZone,
347
355
  currentPage: input.currentPage,
356
+ chatSurface: input.chatSurface,
357
+ adminPublicOrigin: input.adminPublicOrigin,
348
358
  abortSignal: input.abortSignal,
349
359
  adminUser: input.adminUser,
350
360
  emit: (event) => __awaiter(this, void 0, void 0, function* () {
@@ -129,15 +129,20 @@ export class ChatSurfaceService {
129
129
  }
130
130
  handleAgentSurfaceResponse(incoming, sink, adminUser, prompt, options) {
131
131
  return __awaiter(this, void 0, void 0, function* () {
132
- var _a, _b, _c;
132
+ var _a, _b, _c, _d;
133
133
  const emitDone = (_a = options === null || options === void 0 ? void 0 : options.emitDone) !== null && _a !== void 0 ? _a : true;
134
+ const adminPublicOrigin = typeof ((_b = incoming.metadata) === null || _b === void 0 ? void 0 : _b.adminPublicOrigin) === "string"
135
+ ? incoming.metadata.adminPublicOrigin
136
+ : undefined;
134
137
  const sessionId = yield this.sessionStore.getOrCreateChatSurfaceSession(Object.assign(Object.assign({}, incoming), { prompt }), adminUser);
135
138
  if (emitDone) {
136
139
  yield this.handleTurn({
137
140
  prompt,
138
141
  sessionId,
139
142
  modeName: incoming.modeName,
140
- userTimeZone: (_b = incoming.userTimeZone) !== null && _b !== void 0 ? _b : "UTC",
143
+ userTimeZone: (_c = incoming.userTimeZone) !== null && _c !== void 0 ? _c : "UTC",
144
+ chatSurface: incoming.surface,
145
+ adminPublicOrigin,
141
146
  adminUser,
142
147
  emit: this.createEventEmitter(sink),
143
148
  failureLogMessage: `Agent ${incoming.surface} surface response failed`,
@@ -149,7 +154,9 @@ export class ChatSurfaceService {
149
154
  prompt,
150
155
  sessionId,
151
156
  modeName: incoming.modeName,
152
- userTimeZone: (_c = incoming.userTimeZone) !== null && _c !== void 0 ? _c : "UTC",
157
+ userTimeZone: (_d = incoming.userTimeZone) !== null && _d !== void 0 ? _d : "UTC",
158
+ chatSurface: incoming.surface,
159
+ adminPublicOrigin,
153
160
  adminUser,
154
161
  emit: this.createEventEmitter(sink),
155
162
  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