@adminforth/agent 1.43.29 → 1.44.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.
package/index.ts CHANGED
@@ -1,4 +1,12 @@
1
- import type { AdminUser, AdminForthResource, IAdminForth, IHttpServer } from "adminforth";
1
+ import type {
2
+ AdminUser,
3
+ AdminForthResource,
4
+ ChatSurfaceAdapter,
5
+ ChatSurfaceEventSink,
6
+ ChatSurfaceIncomingMessage,
7
+ IAdminForth,
8
+ IHttpServer,
9
+ } from "adminforth";
2
10
 
3
11
  import { AdminForthPlugin, logger, Filters, Sorts } from "adminforth";
4
12
 
@@ -12,12 +20,14 @@ import { AdminForthCheckpointSaver } from "./agent/checkpointer.js";
12
20
  import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
13
21
  import { detectUserLanguage, type PreviousUserMessage } from "./agent/languageDetect.js";
14
22
  import { prepareApiBasedTools as buildApiBasedTools } from './apiBasedTools.js';
15
- import { createAgentEventStream } from "./agentResponseEvents.js";
23
+ import type { AgentEventEmitter } from "./agentEvents.js";
24
+ import { createSseEventEmitter } from "./surfaces/web-sse/createSseEventEmitter.js";
16
25
  import { appendCustomSystemPrompt, buildAgentSystemPrompt, buildAgentTurnSystemPrompt, DEFAULT_AGENT_SYSTEM_PROMPT} from "./agent/systemPrompt.js";
17
- import type { ToolCallEvent } from "./agent/toolCallEvents.js";
18
26
  import type { CurrentPageContext } from "./agent/tools/getUserLocation.js";
19
27
  import { sanitizeSpeechText } from "./sanitizeSpeechText.js";
20
28
 
29
+ export type { AgentEvent, AgentEventEmitter } from "./agentEvents.js";
30
+
21
31
  type MulterFile = {
22
32
  buffer: Buffer;
23
33
  originalname: string;
@@ -37,18 +47,21 @@ type AgentTurnRunInput = {
37
47
  abortSignal?: AbortSignal;
38
48
  adminUser: AdminUser;
39
49
  sequenceDebugCollector: ReturnType<typeof createSequenceDebugCollector>;
40
- emitReasoningDelta?: (delta: string) => void;
41
- emitTextDelta?: (delta: string) => void;
42
- emitToolCallEvent?: (event: ToolCallEvent) => void;
50
+ emit?: AgentEventEmitter;
43
51
  };
44
52
 
45
53
  type RunAndPersistAgentResponseInput =
46
54
  Omit<AgentTurnRunInput, "turnId" | "sequenceDebugCollector" | "previousUserMessages"> & {
47
- emitErrorResponse?: (response: string) => void;
48
55
  failureLogMessage: string;
49
56
  abortLogMessage: string;
50
57
  };
51
58
 
59
+ type HandleTurnInput = Omit<RunAndPersistAgentResponseInput, "failureLogMessage" | "abortLogMessage"> & {
60
+ emit: AgentEventEmitter;
61
+ failureLogMessage?: string;
62
+ abortLogMessage?: string;
63
+ };
64
+
52
65
  const agentResponseBodySchema = z.object({
53
66
  message: z.string(),
54
67
  sessionId: z.string(),
@@ -154,6 +167,33 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
154
167
  }));
155
168
  }
156
169
 
170
+ private getChatSurfaceSessionId(incoming: ChatSurfaceIncomingMessage) {
171
+ return `${incoming.surface}:${incoming.externalConversationId}`;
172
+ }
173
+
174
+ private async getOrCreateChatSurfaceSession(
175
+ incoming: ChatSurfaceIncomingMessage,
176
+ adminUser: AdminUser,
177
+ ) {
178
+ const sessionId = this.getChatSurfaceSessionId(incoming);
179
+ const sessionResource = this.adminforth.resource(this.options.sessionResource.resourceId);
180
+ const session = await sessionResource.get(
181
+ [Filters.EQ(this.options.sessionResource.idField, sessionId)]
182
+ );
183
+
184
+ if (session) {
185
+ return sessionId;
186
+ }
187
+
188
+ await sessionResource.create({
189
+ [this.options.sessionResource.idField]: sessionId,
190
+ [this.options.sessionResource.titleField]: incoming.prompt.slice(0, 40) || "New Session",
191
+ [this.options.sessionResource.askerIdField]: adminUser.pk,
192
+ });
193
+
194
+ return sessionId;
195
+ }
196
+
157
197
  private getCheckpointer() {
158
198
  if (this.checkpointer) return this.checkpointer;
159
199
 
@@ -223,6 +263,9 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
223
263
 
224
264
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
225
265
  this.options.audioAdapter?.validate();
266
+ for (const chatSurfaceAdapter of this.options.chatSurfaceAdapters ?? []) {
267
+ chatSurfaceAdapter.validate();
268
+ }
226
269
  this.agentSystemPromptPromise = buildAgentSystemPrompt(
227
270
  adminforth,
228
271
  this.getInternalAgentResourceIds(),
@@ -295,7 +338,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
295
338
  abortSignal: input.abortSignal,
296
339
  emitToolCallEvent: (event) => {
297
340
  input.sequenceDebugCollector.handleToolCallEvent(event);
298
- input.emitToolCallEvent?.(event);
341
+ void input.emit?.({
342
+ type: "tool-call",
343
+ data: event,
344
+ });
299
345
  },
300
346
  sequenceDebugSink: input.sequenceDebugCollector,
301
347
  });
@@ -332,12 +378,18 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
332
378
  .join("");
333
379
 
334
380
  if (reasoningDelta) {
335
- input.emitReasoningDelta?.(reasoningDelta);
381
+ await input.emit?.({
382
+ type: "reasoning-delta",
383
+ delta: reasoningDelta,
384
+ });
336
385
  }
337
386
 
338
387
  if (textDelta) {
339
388
  fullResponse += textDelta;
340
- input.emitTextDelta?.(textDelta);
389
+ await input.emit?.({
390
+ type: "text-delta",
391
+ delta: textDelta,
392
+ });
341
393
  }
342
394
  }
343
395
 
@@ -369,9 +421,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
369
421
  abortSignal: input.abortSignal,
370
422
  adminUser: input.adminUser,
371
423
  sequenceDebugCollector,
372
- emitToolCallEvent: input.emitToolCallEvent,
373
- emitReasoningDelta: input.emitReasoningDelta,
374
- emitTextDelta: input.emitTextDelta,
424
+ emit: input.emit,
375
425
  });
376
426
  fullResponse = agentResponse.text;
377
427
  } catch (error) {
@@ -382,7 +432,6 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
382
432
  failed = true;
383
433
  fullResponse = getErrorMessage(error);
384
434
  logger.error(`${input.failureLogMessage}:\n${fullResponse}`);
385
- input.emitErrorResponse?.(fullResponse);
386
435
  }
387
436
  }
388
437
 
@@ -405,7 +454,134 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
405
454
  };
406
455
  }
407
456
 
457
+ async handleTurn(input: HandleTurnInput) {
458
+ await input.emit({
459
+ type: "turn-started",
460
+ messageId: randomUUID(),
461
+ });
462
+
463
+ const agentResponse = await this.runAndPersistAgentResponse({
464
+ prompt: input.prompt,
465
+ sessionId: input.sessionId,
466
+ modeName: input.modeName,
467
+ userTimeZone: input.userTimeZone,
468
+ currentPage: input.currentPage,
469
+ abortSignal: input.abortSignal,
470
+ adminUser: input.adminUser,
471
+ emit: input.emit,
472
+ failureLogMessage: input.failureLogMessage ?? "Agent response failed",
473
+ abortLogMessage: input.abortLogMessage ?? "Agent response aborted",
474
+ });
475
+
476
+ if (agentResponse.failed) {
477
+ await input.emit({
478
+ type: "error",
479
+ error: agentResponse.text,
480
+ });
481
+ } else if (!agentResponse.aborted) {
482
+ await input.emit({
483
+ type: "response",
484
+ text: agentResponse.text,
485
+ sessionId: input.sessionId,
486
+ turnId: agentResponse.turnId,
487
+ });
488
+ }
489
+
490
+ await input.emit({
491
+ type: "finish",
492
+ });
493
+
494
+ return agentResponse;
495
+ }
496
+
497
+ private createChatSurfaceEventEmitter(sink: ChatSurfaceEventSink): AgentEventEmitter {
498
+ return async (event) => {
499
+ if (event.type === "text-delta") {
500
+ await sink.emit({
501
+ type: "text_delta",
502
+ delta: event.delta,
503
+ });
504
+ return;
505
+ }
506
+
507
+ if (event.type === "response") {
508
+ await sink.emit({
509
+ type: "done",
510
+ text: event.text,
511
+ });
512
+ return;
513
+ }
514
+
515
+ if (event.type === "error") {
516
+ await sink.emit({
517
+ type: "error",
518
+ message: event.error,
519
+ });
520
+ }
521
+ };
522
+ }
523
+
524
+ private async handleChatSurfaceMessage(
525
+ adapter: ChatSurfaceAdapter,
526
+ incoming: ChatSurfaceIncomingMessage,
527
+ sink: ChatSurfaceEventSink,
528
+ ) {
529
+ const adminUser = await adapter.resolveAdminUser({
530
+ adminforth: this.adminforth,
531
+ incoming,
532
+ });
533
+
534
+ if (!adminUser) {
535
+ await sink.emit({
536
+ type: "error",
537
+ message: "This chat account is not authorized to use AdminForth Agent.",
538
+ });
539
+ return;
540
+ }
541
+
542
+ await this.handleTurn({
543
+ prompt: incoming.prompt,
544
+ sessionId: await this.getOrCreateChatSurfaceSession(incoming, adminUser),
545
+ modeName: incoming.modeName,
546
+ userTimeZone: incoming.userTimeZone ?? "UTC",
547
+ adminUser,
548
+ emit: this.createChatSurfaceEventEmitter(sink),
549
+ failureLogMessage: `Agent ${incoming.surface} surface response failed`,
550
+ abortLogMessage: `Agent ${incoming.surface} surface response aborted`,
551
+ });
552
+ }
553
+
408
554
  setupEndpoints(server: IHttpServer) {
555
+ for (const adapter of this.options.chatSurfaceAdapters ?? []) {
556
+ server.endpoint({
557
+ method: "POST",
558
+ noAuth: true,
559
+ path: `/agent/surface/${adapter.name}/webhook`,
560
+ handler: async (ctx) => {
561
+ const surfaceContext = {
562
+ body: ctx.body,
563
+ headers: ctx.headers,
564
+ abortSignal: ctx.abortSignal,
565
+ rawRequest: ctx._raw_express_req,
566
+ rawResponse: ctx._raw_express_res,
567
+ };
568
+ const incoming = await adapter.parseIncomingMessage(surfaceContext);
569
+
570
+ if (!incoming) return { ok: true };
571
+
572
+ const sink = await adapter.createEventSink(surfaceContext, incoming);
573
+
574
+ try {
575
+ await this.handleChatSurfaceMessage(adapter, incoming, sink);
576
+ } finally {
577
+ await sink.close?.();
578
+ }
579
+
580
+ return { ok: true };
581
+ },
582
+ });
583
+ }
584
+
409
585
  server.endpoint({
410
586
  method: 'POST',
411
587
  path: `/agent/get-placeholder-messages`,
@@ -435,11 +611,12 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
435
611
  const currentAdminUser = requireAdminUser(adminUser);
436
612
  const data = this.parseBody(agentResponseBodySchema, body, response);
437
613
  if (!data) return;
438
- const stream = createAgentEventStream(_raw_express_res, {vercelAiUiMessageStream: true, closeActiveBlockOnToolStart: true});
439
- const messageId = randomUUID();
614
+ const emit = createSseEventEmitter(_raw_express_res, {
615
+ vercelAiUiMessageStream: true,
616
+ closeActiveBlockOnToolStart: true,
617
+ });
440
618
 
441
- stream.start(messageId);
442
- await this.runAndPersistAgentResponse({
619
+ await this.handleTurn({
443
620
  prompt: data.message,
444
621
  sessionId: data.sessionId,
445
622
  modeName: data.mode,
@@ -447,14 +624,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
447
624
  currentPage: data.currentPage,
448
625
  abortSignal,
449
626
  adminUser: currentAdminUser,
450
- emitToolCallEvent: stream.toolCall,
451
- emitReasoningDelta: stream.reasoningDelta,
452
- emitTextDelta: stream.textDelta,
453
- emitErrorResponse: stream.textDelta,
627
+ emit,
454
628
  failureLogMessage: "Agent response streaming failed",
455
629
  abortLogMessage: "Agent response streaming aborted by the client",
456
630
  });
457
- stream.end();
458
631
  return null;
459
632
  }
460
633
  });
@@ -476,7 +649,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
476
649
  response.setStatus(400, "Audio file is required");
477
650
  return { error: "Audio file is required" };
478
651
  }
479
- const stream = createAgentEventStream(_raw_express_res);
652
+ const emit = createSseEventEmitter(_raw_express_res);
480
653
 
481
654
  let transcription;
482
655
 
@@ -491,28 +664,38 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
491
664
  } catch (error) {
492
665
  if (abortSignal.aborted || isAbortError(error)) {
493
666
  logger.info("Agent speech transcription aborted by the client");
494
- stream.end();
667
+ await emit({ type: "finish" });
495
668
  return null;
496
669
  }
497
670
 
498
671
  logger.error(`Agent speech transcription failed:\n${getErrorMessage(error)}`);
499
- stream.error("Speech transcription failed. Check server logs for details.");
500
- stream.end();
672
+ await emit({
673
+ type: "error",
674
+ error: "Speech transcription failed. Check server logs for details.",
675
+ });
676
+ await emit({ type: "finish" });
501
677
  return null;
502
678
  }
503
679
 
504
680
  if (abortSignal.aborted) {
505
- stream.end();
681
+ await emit({ type: "finish" });
506
682
  return null;
507
683
  }
508
684
 
509
685
  const prompt = transcription.text;
510
686
  if (!prompt) {
511
- stream.error("Speech transcription is empty");
512
- stream.end();
687
+ await emit({
688
+ type: "error",
689
+ error: "Speech transcription is empty",
690
+ });
691
+ await emit({ type: "finish" });
513
692
  return null;
514
693
  }
515
- stream.transcript(transcription.text, transcription.language);
694
+ await emit({
695
+ type: "transcript",
696
+ text: transcription.text,
697
+ language: transcription.language,
698
+ });
516
699
 
517
700
  const sessionId = data.sessionId as string;
518
701
  const currentPage = data.currentPage;
@@ -524,34 +707,42 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
524
707
  currentPage,
525
708
  abortSignal,
526
709
  adminUser: currentAdminUser,
527
- emitToolCallEvent: stream.toolCall,
710
+ emit: async (event) => {
711
+ if (event.type === "tool-call") {
712
+ await emit(event);
713
+ }
714
+ },
528
715
  failureLogMessage: "Agent speech response failed",
529
716
  abortLogMessage: "Agent speech response aborted by the client",
530
717
  });
531
718
 
532
719
  if (agentResponse.aborted) {
533
- stream.end();
720
+ await emit({ type: "finish" });
534
721
  return null;
535
722
  }
536
723
 
537
724
  if (agentResponse.failed) {
538
- stream.error(agentResponse.text);
539
- stream.end();
725
+ await emit({
726
+ type: "error",
727
+ error: agentResponse.text,
728
+ });
729
+ await emit({ type: "finish" });
540
730
  return null;
541
731
  }
542
732
 
543
733
  try {
544
- stream.speechResponse(
545
- {
734
+ await emit({
735
+ type: "speech-response",
736
+ transcript: {
546
737
  text: transcription.text,
547
738
  language: transcription.language,
548
739
  },
549
- {
740
+ response: {
550
741
  text: agentResponse.text,
551
742
  },
552
743
  sessionId,
553
- agentResponse.turnId,
554
- );
744
+ turnId: agentResponse.turnId,
745
+ });
555
746
  const speech = await audioAdapter.synthesize({
556
747
  text: sanitizeSpeechText(agentResponse.text),
557
748
  stream: true,
@@ -560,7 +751,14 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
560
751
  abortSignal,
561
752
  });
562
753
 
563
- stream.audioStart(speech.mimeType, speech.format, 24000, 1, 16);
754
+ await emit({
755
+ type: "audio-start",
756
+ mimeType: speech.mimeType,
757
+ format: speech.format,
758
+ sampleRate: 24000,
759
+ channelCount: 1,
760
+ bitsPerSample: 16,
761
+ });
564
762
 
565
763
  const reader = speech.audioStream.getReader();
566
764
  const cancelAudioStream = () => {
@@ -586,24 +784,30 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
586
784
  break;
587
785
  }
588
786
 
589
- stream.audioDelta(value);
787
+ await emit({
788
+ type: "audio-delta",
789
+ value,
790
+ });
590
791
  }
591
792
  } finally {
592
793
  abortSignal.removeEventListener("abort", cancelAudioStream);
593
794
  reader.releaseLock();
594
795
  }
595
796
 
596
- stream.audioDone();
597
- stream.end();
797
+ await emit({ type: "audio-done" });
798
+ await emit({ type: "finish" });
598
799
  return null;
599
800
  } catch (error) {
600
801
  if (abortSignal.aborted || isAbortError(error)) {
601
802
  logger.info("Agent speech audio streaming aborted by the client");
602
803
  } else {
603
804
  logger.error(`Agent speech audio streaming failed:\n${error}`);
604
- stream.error(getErrorMessage(error));
805
+ await emit({
806
+ type: "error",
807
+ error: getErrorMessage(error),
808
+ });
605
809
  }
606
- stream.end();
810
+ await emit({ type: "finish" });
607
811
  return null;
608
812
  }
609
813
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "1.43.29",
3
+ "version": "1.44.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "@langchain/core": "^1.1.40",
34
34
  "@langchain/langgraph": "^1.2.8",
35
35
  "@langchain/langgraph-checkpoint": "^1.0.1",
36
- "adminforth": "^2.53.5",
36
+ "adminforth": ">=2.54.0",
37
37
  "dayjs": "^1.11.20",
38
38
  "langchain": "^1.3.3",
39
39
  "multer": "^2.1.1",