@adminforth/agent 2.6.0 → 2.7.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.
@@ -25,6 +25,7 @@ export type AgentModeCompletionAdapter = CompletionAdapter & {
25
25
  */
26
26
  export type LlmStreamInput = {
27
27
  completionAdapter: AgentModeCompletionAdapter;
28
+ systemPrompt: string;
28
29
  input: {
29
30
  messages: AgentMessage[];
30
31
  } | {
@@ -60,6 +60,7 @@ export type RunTurnUseCaseDeps = {
60
60
  export declare class RunTurnUseCase {
61
61
  private readonly deps;
62
62
  private readonly pendingInterrupts;
63
+ private readonly lastDetectedLanguage;
63
64
  constructor(deps: RunTurnUseCaseDeps);
64
65
  private resolveMode;
65
66
  private buildContext;
@@ -78,6 +79,7 @@ export declare class RunTurnUseCase {
78
79
  */
79
80
  private prepareEditTurn;
80
81
  private prepareTurn;
82
+ private resolveUserLanguage;
81
83
  private buildMessages;
82
84
  private handleInterrupt;
83
85
  private runAgentTurn;
@@ -17,7 +17,6 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
17
17
  import { logger, Filters } from "adminforth";
18
18
  import { randomUUID } from "crypto";
19
19
  import { VegaLiteStreamBuffer } from "../domain/vegaLiteStreamBuffer.js";
20
- import { buildAgentTurnSystemPrompt } from "../domain/systemPrompt.js";
21
20
  import { getErrorMessage, isAbortError } from "../shared/errors.js";
22
21
  function getApprovalDecision(input) {
23
22
  return "approvalDecision" in input
@@ -77,6 +76,7 @@ export class RunTurnUseCase {
77
76
  constructor(deps) {
78
77
  this.deps = deps;
79
78
  this.pendingInterrupts = new Map();
79
+ this.lastDetectedLanguage = new Map();
80
80
  }
81
81
  resolveMode(modeName) {
82
82
  var _a;
@@ -175,6 +175,7 @@ export class RunTurnUseCase {
175
175
  // Editing supersedes any in-flight turn state for this session.
176
176
  this.deps.steerBuffer.clear(input.sessionId);
177
177
  this.pendingInterrupts.delete(input.sessionId);
178
+ this.lastDetectedLanguage.delete(input.sessionId);
178
179
  // Apply the edit + truncate (deletes later turns first, then rewrites the edited turn).
179
180
  yield this.deps.sessions.editTurnAndTruncateAfter({
180
181
  sessionId: input.sessionId,
@@ -239,9 +240,13 @@ export class RunTurnUseCase {
239
240
  };
240
241
  });
241
242
  }
242
- buildMessages(prepared) {
243
+ resolveUserLanguage(prepared) {
243
244
  return __awaiter(this, void 0, void 0, function* () {
244
- const userLanguage = yield this.deps.llm
245
+ var _a;
246
+ if (prepared.resume) {
247
+ return (_a = this.lastDetectedLanguage.get(prepared.sessionId)) !== null && _a !== void 0 ? _a : null;
248
+ }
249
+ const detected = yield this.deps.llm
245
250
  .detectLanguage({
246
251
  completionAdapter: prepared.mode.completionAdapter,
247
252
  prompt: prepared.prompt,
@@ -255,20 +260,13 @@ export class RunTurnUseCase {
255
260
  logger.warn(`Failed to detect user language: ${getErrorMessage(error)}`);
256
261
  return null;
257
262
  });
258
- const adminforth = this.deps.getAdminforth();
259
- const systemPrompt = buildAgentTurnSystemPrompt({
260
- agentSystemPrompt: yield this.deps.getAgentSystemPrompt(),
261
- adminUser: prepared.context.adminUser,
262
- usernameField: adminforth.config.auth.usernameField,
263
- userLanguage,
264
- chatSurface: prepared.context.chatSurface,
265
- });
266
- return [
267
- { role: "system", content: systemPrompt },
268
- { role: "user", content: prepared.prompt },
269
- ];
263
+ this.lastDetectedLanguage.set(prepared.sessionId, detected);
264
+ return detected;
270
265
  });
271
266
  }
267
+ buildMessages(prepared) {
268
+ return [{ role: "user", content: prepared.prompt }];
269
+ }
272
270
  handleInterrupt(prepared, interrupt, descriptors) {
273
271
  return __awaiter(this, void 0, void 0, function* () {
274
272
  var _a, _b, _c;
@@ -290,6 +288,7 @@ export class RunTurnUseCase {
290
288
  runAgentTurn(prepared) {
291
289
  return __awaiter(this, void 0, void 0, function* () {
292
290
  var _a, e_1, _b, _c;
291
+ const userLanguage = yield this.resolveUserLanguage(prepared);
293
292
  const streamInput = prepared.resume
294
293
  ? {
295
294
  resume: buildResumeValue({
@@ -298,11 +297,12 @@ export class RunTurnUseCase {
298
297
  prompt: prepared.prompt,
299
298
  }),
300
299
  }
301
- : { messages: yield this.buildMessages(prepared) };
300
+ : { messages: this.buildMessages(prepared) };
302
301
  const stream = yield this.deps.llm.streamTurn({
303
302
  completionAdapter: prepared.mode.completionAdapter,
303
+ systemPrompt: yield this.deps.getAgentSystemPrompt(),
304
304
  input: streamInput,
305
- context: prepared.context,
305
+ context: Object.assign(Object.assign({}, prepared.context), { userLanguage }),
306
306
  observability: prepared.observability,
307
307
  branchFromCheckpointId: prepared.branchFromCheckpointId,
308
308
  });
@@ -353,6 +353,9 @@ export class RunTurnUseCase {
353
353
  // done and leftover (unconsumed) steers must not leak into the next turn.
354
354
  if (!interrupted) {
355
355
  this.deps.steerBuffer.clear(prepared.sessionId);
356
+ // The detected language is only needed to bridge an interrupt -> resume; once
357
+ // the turn is done the next one re-detects.
358
+ this.lastDetectedLanguage.delete(prepared.sessionId);
356
359
  }
357
360
  if (!this.deps.hasPersistentCheckpointer && !interrupted) {
358
361
  this.pendingInterrupts.delete(prepared.sessionId);
@@ -2,11 +2,10 @@ import type { AdminUser, IAdminForth } from "adminforth";
2
2
  import type { DetectedLanguage } from "./languageDetect.js";
3
3
  export declare const DEFAULT_AGENT_SYSTEM_PROMPT: string;
4
4
  export declare function appendCustomSystemPrompt(systemPrompt: string, customSystemPrompt?: string): string;
5
- export declare function buildAgentTurnSystemPrompt(input: {
6
- agentSystemPrompt: string;
5
+ export declare function buildDynamicSystemPrompt(input: {
7
6
  adminUser: AdminUser;
8
7
  usernameField: string;
9
- userLanguage: DetectedLanguage | null;
8
+ userLanguage?: DetectedLanguage | null;
10
9
  chatSurface?: string;
11
10
  }): string;
12
11
  export declare function buildAgentSystemPrompt(adminforth: IAdminForth, hiddenResourceIds?: Iterable<string>): Promise<string>;
@@ -51,14 +51,14 @@ function formatAdminUserPrompt(adminUser, usernameField) {
51
51
  "Use this admin user email when the user asks to send information to themselves, the current admin, or the logged-in user.",
52
52
  ].join("\n");
53
53
  }
54
- export function buildAgentTurnSystemPrompt(input) {
54
+ export function buildDynamicSystemPrompt(input) {
55
+ var _a;
55
56
  return [
56
- input.agentSystemPrompt,
57
57
  formatAdminUserPrompt(input.adminUser, input.usernameField),
58
58
  input.chatSurface
59
59
  ? `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.`
60
60
  : "",
61
- formatLanguagePrompt(input.userLanguage),
61
+ formatLanguagePrompt((_a = input.userLanguage) !== null && _a !== void 0 ? _a : null),
62
62
  ].filter(Boolean).join("\n\n");
63
63
  }
64
64
  function formatResources(resources) {
@@ -1,4 +1,5 @@
1
1
  import type { AdminUser, AudioAdapter } from "adminforth";
2
+ import type { DetectedLanguage } from "./languageDetect.js";
2
3
  import type { CurrentPageContext } from "../tools/getUserLocation.js";
3
4
  import type { AgentEventEmitter } from "./agentEvents.js";
4
5
  /**
@@ -53,6 +54,7 @@ export type AgentTurnContext = {
53
54
  currentPage?: CurrentPageContext;
54
55
  chatSurface?: string;
55
56
  adminPublicOrigin?: string;
57
+ userLanguage?: DetectedLanguage | null;
56
58
  };
57
59
  export type AgentTurnObservability = {
58
60
  emit?: AgentEventEmitter;
@@ -76,12 +78,8 @@ export type HandleSpeechTurnInput = SpeechAgentTurnInput;
76
78
  export type HandleEditTurnInput = TextAgentTurnInput & {
77
79
  turnId: string;
78
80
  };
79
- /**
80
- * Provider-agnostic message passed from the application layer to the LLM port.
81
- * The infrastructure adapter maps it onto concrete LangChain messages.
82
- */
83
81
  export type AgentMessage = {
84
- role: "system" | "user";
82
+ role: "user";
85
83
  content: string;
86
84
  };
87
85
  /**
@@ -4,6 +4,7 @@ import type { AgentEventEmitter } from "../domain/agentEvents.js";
4
4
  import type { SequenceDebugCollector } from "./middleware/sequenceDebug.js";
5
5
  import type { CurrentPageContext } from "../tools/getUserLocation.js";
6
6
  import type { AgentTurnContext } from "../domain/turnTypes.js";
7
+ import type { DetectedLanguage } from "../domain/languageDetect.js";
7
8
  export declare const contextSchema: z.ZodObject<{
8
9
  adminUser: z.ZodCustom<AdminUser, AdminUser>;
9
10
  userTimeZone: z.ZodString;
@@ -14,9 +15,11 @@ export declare const contextSchema: z.ZodObject<{
14
15
  chatSurface: z.ZodOptional<z.ZodString>;
15
16
  adminBaseUrl: z.ZodOptional<z.ZodString>;
16
17
  adminPublicOrigin: z.ZodOptional<z.ZodString>;
18
+ userLanguage: z.ZodOptional<z.ZodCustom<DetectedLanguage | null, DetectedLanguage | null>>;
17
19
  emit: z.ZodOptional<z.ZodCustom<AgentEventEmitter, AgentEventEmitter>>;
18
20
  sequenceDebugSink: z.ZodCustom<SequenceDebugCollector, SequenceDebugCollector>;
19
21
  }, z.core.$strip>;
22
+ export type AgentContext = z.infer<typeof contextSchema>;
20
23
  export declare function toLangchainAgentContext(context: AgentTurnContext & {
21
24
  adminBaseUrl: string;
22
25
  emit?: AgentEventEmitter;
@@ -9,6 +9,7 @@ export const contextSchema = z.object({
9
9
  chatSurface: z.string().optional(),
10
10
  adminBaseUrl: z.string().optional(),
11
11
  adminPublicOrigin: z.string().optional(),
12
+ userLanguage: z.custom().optional(),
12
13
  emit: z.custom().optional(),
13
14
  sequenceDebugSink: z.custom(),
14
15
  });
@@ -16,6 +16,7 @@ export type AgentTurnModels = {
16
16
  };
17
17
  export type AgentRuntimeRunInput = {
18
18
  models: AgentTurnModels;
19
+ systemPrompt: string;
19
20
  input: {
20
21
  messages: Messages;
21
22
  } | Command;
@@ -7,13 +7,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { createAgent, summarizationMiddleware, humanInTheLoopMiddleware } from "langchain";
10
+ import { createAgent, summarizationMiddleware, humanInTheLoopMiddleware, dynamicSystemPromptMiddleware, } from "langchain";
11
11
  import { createApiBasedToolsMiddleware } from "./middleware/apiToolsMiddleware.js";
12
12
  import { createSequenceDebugMiddleware } from "./middleware/sequenceDebug.js";
13
13
  import { createSteerMiddleware } from "./middleware/steerMiddleware.js";
14
14
  import { createAgentLlmMetricsLogger } from "./agentModels.js";
15
15
  import { contextSchema, toLangchainAgentContext } from "./agentContext.js";
16
- import { createSystemMessagesMiddleware } from "./middleware/systemMessages.js";
16
+ import { createLegacySystemMessagesMiddleware } from "./middleware/legacySystemMessages.js";
17
+ import { buildDynamicSystemPrompt } from "../domain/systemPrompt.js";
17
18
  function createHumanInTheLoopInterrupts(apiBasedTools) {
18
19
  return Object.fromEntries(Object.entries(apiBasedTools)
19
20
  .filter(([, apiBasedTool]) => { var _a; return ((_a = apiBasedTool.agent) === null || _a === void 0 ? void 0 : _a.isDangerous) === true; })
@@ -44,14 +45,22 @@ export class AgentRuntime {
44
45
  descriptionPrefix: "Tool execution pending approval",
45
46
  });
46
47
  const steerMiddleware = createSteerMiddleware(this.options.steerBuffer);
47
- const systemMessagesMiddleware = createSystemMessagesMiddleware();
48
+ const legacySystemMessagesMiddleware = createLegacySystemMessagesMiddleware();
49
+ const usernameField = adminforth.config.auth.usernameField;
50
+ const dynamicPromptMiddleware = dynamicSystemPromptMiddleware((_state, runtime) => buildDynamicSystemPrompt({
51
+ adminUser: runtime.context.adminUser,
52
+ usernameField,
53
+ userLanguage: runtime.context.userLanguage,
54
+ chatSurface: runtime.context.chatSurface,
55
+ }));
48
56
  const middleware = [
49
57
  steerMiddleware,
50
58
  apiBasedToolsMiddleware,
51
59
  hitlMiddleware,
60
+ legacySystemMessagesMiddleware,
61
+ dynamicPromptMiddleware,
52
62
  ...((_a = input.models.modelMiddleware) !== null && _a !== void 0 ? _a : []),
53
63
  sequenceDebugMiddleware,
54
- systemMessagesMiddleware,
55
64
  summarizationMiddleware({
56
65
  model: input.models.summaryModel,
57
66
  trigger: { tokens: 1024 * 64 },
@@ -61,6 +70,11 @@ export class AgentRuntime {
61
70
  const agent = createAgent({
62
71
  name: this.options.name,
63
72
  model: input.models.model,
73
+ // Passed as a plain string on purpose: LangChain normalizes it to a single text
74
+ // block, and DynamicSystemPromptMiddleware appends the per-turn half as a second
75
+ // one. That block split is all the agent owes the provider — whether the boundary
76
+ // becomes a cache breakpoint is the adapter's call.
77
+ systemPrompt: input.systemPrompt,
64
78
  checkpointer: this.options.getCheckpointer(),
65
79
  tools,
66
80
  contextSchema,
@@ -8,13 +8,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { Command } from "@langchain/langgraph";
11
- import { HumanMessage, SystemMessage } from "langchain";
11
+ import { HumanMessage } from "langchain";
12
12
  import { detectUserLanguage } from "../domain/languageDetect.js";
13
13
  import { adaptRawStream, normalizeInterrupts } from "./streamAdapter.js";
14
14
  function toLangchainMessages(messages) {
15
- return messages.map((message) => message.role === "system"
16
- ? new SystemMessage(message.content)
17
- : new HumanMessage(message.content));
15
+ return messages.map((message) => new HumanMessage(message.content));
18
16
  }
19
17
  /**
20
18
  * LangChain/LangGraph implementation of {@link LlmPort}. This is the single
@@ -34,6 +32,7 @@ export class LangGraphLlm {
34
32
  : { messages: toLangchainMessages(input.input.messages) };
35
33
  const rawStream = yield this.runtime.stream({
36
34
  models,
35
+ systemPrompt: input.systemPrompt,
37
36
  input: runtimeInput,
38
37
  context: input.context,
39
38
  observability: input.observability,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Migration shield for threads checkpointed before the system prompt moved out of the
3
+ * message history.
4
+ *
5
+ * Turns used to be started with `[SystemMessage, HumanMessage]`, so every such turn left
6
+ * a system message inside the thread's persisted state. Those messages are still in old
7
+ * checkpoints and would now arrive mid-history — both an invalid shape for providers that
8
+ * take the system prompt out-of-band, and a stale duplicate of a prompt the agent already
9
+ * supplies. So they are dropped, not re-hoisted: the authoritative prompt is
10
+ * `createAgent({ systemPrompt })` plus DynamicSystemPromptMiddleware.
11
+ *
12
+ * Nothing writes system messages into state any more, so for new threads this is a no-op.
13
+ * It stays as a cheap guard (and can be deleted once no pre-refactor checkpoints remain).
14
+ */
15
+ export declare function createLegacySystemMessagesMiddleware(): import("langchain").AgentMiddleware<undefined, undefined, unknown, readonly (import("@langchain/core/tools").ClientTool | import("@langchain/core/tools").ServerTool)[]>;
@@ -0,0 +1,45 @@
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 { createMiddleware } from "langchain";
11
+ function isSystemMessage(message) {
12
+ var _a, _b;
13
+ return (((_a = message._getType) === null || _a === void 0 ? void 0 : _a.call(message)) === "system" ||
14
+ ((_b = message.getType) === null || _b === void 0 ? void 0 : _b.call(message)) === "system" ||
15
+ message.type === "system");
16
+ }
17
+ /**
18
+ * Migration shield for threads checkpointed before the system prompt moved out of the
19
+ * message history.
20
+ *
21
+ * Turns used to be started with `[SystemMessage, HumanMessage]`, so every such turn left
22
+ * a system message inside the thread's persisted state. Those messages are still in old
23
+ * checkpoints and would now arrive mid-history — both an invalid shape for providers that
24
+ * take the system prompt out-of-band, and a stale duplicate of a prompt the agent already
25
+ * supplies. So they are dropped, not re-hoisted: the authoritative prompt is
26
+ * `createAgent({ systemPrompt })` plus DynamicSystemPromptMiddleware.
27
+ *
28
+ * Nothing writes system messages into state any more, so for new threads this is a no-op.
29
+ * It stays as a cheap guard (and can be deleted once no pre-refactor checkpoints remain).
30
+ */
31
+ export function createLegacySystemMessagesMiddleware() {
32
+ return createMiddleware({
33
+ name: "LegacySystemMessagesMiddleware",
34
+ wrapModelCall(request, handler) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ const typed = request;
37
+ const messages = typed.messages.filter((message) => !isSystemMessage(message));
38
+ if (messages.length === typed.messages.length) {
39
+ return handler(request);
40
+ }
41
+ return handler(Object.assign(Object.assign({}, request), { messages }));
42
+ });
43
+ },
44
+ });
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -44,11 +44,11 @@
44
44
  "typescript": "^5.7.3"
45
45
  },
46
46
  "dependencies": {
47
- "@langchain/core": "^1.1.40",
48
- "@langchain/langgraph": "^1.2.8",
49
- "@langchain/langgraph-checkpoint": "^1.0.1",
47
+ "@langchain/core": "1.2.5",
48
+ "@langchain/langgraph": "1.2.8",
49
+ "@langchain/langgraph-checkpoint": "1.0.1",
50
50
  "dayjs": "^1.11.20",
51
- "langchain": "^1.3.3",
51
+ "langchain": "1.3.3",
52
52
  "multer": "^2.1.1",
53
53
  "vue-i18n": "^11.4.0",
54
54
  "yaml": "^2.8.3",
@@ -1 +0,0 @@
1
- export declare function createSystemMessagesMiddleware(): import("langchain").AgentMiddleware<undefined, undefined, unknown, readonly (import("@langchain/core/tools").ClientTool | import("@langchain/core/tools").ServerTool)[]>;
@@ -1,55 +0,0 @@
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 { createMiddleware } from "langchain";
11
- function isSystemMessage(message) {
12
- var _a, _b;
13
- return (((_a = message._getType) === null || _a === void 0 ? void 0 : _a.call(message)) === "system" ||
14
- ((_b = message.getType) === null || _b === void 0 ? void 0 : _b.call(message)) === "system" ||
15
- message.type === "system");
16
- }
17
- function contentToText(content, text) {
18
- if (text)
19
- return text;
20
- if (typeof content === "string")
21
- return content;
22
- if (!Array.isArray(content))
23
- return "";
24
- return content
25
- .map((block) => {
26
- var _a;
27
- if (typeof block === "string")
28
- return block;
29
- if (block && typeof block === "object" && "text" in block) {
30
- return String((_a = block.text) !== null && _a !== void 0 ? _a : "");
31
- }
32
- return "";
33
- })
34
- .join("");
35
- }
36
- function normalizeSystemMessages(request) {
37
- const extraSystemText = request.messages
38
- .filter(isSystemMessage)
39
- .map((message) => contentToText(message.content, message.text))
40
- .filter(Boolean)
41
- .join("\n\n");
42
- return Object.assign(Object.assign({}, request), { systemMessage: extraSystemText
43
- ? request.systemMessage.concat(extraSystemText)
44
- : request.systemMessage, messages: request.messages.filter((message) => !isSystemMessage(message)) });
45
- }
46
- export function createSystemMessagesMiddleware() {
47
- return createMiddleware({
48
- name: "SystemMessagesMiddleware",
49
- wrapModelCall(request, handler) {
50
- return __awaiter(this, void 0, void 0, function* () {
51
- return handler(normalizeSystemMessages(request));
52
- });
53
- },
54
- });
55
- }