@dudousxd/nestjs-agent 1.17.1 → 1.18.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/dist/index.js CHANGED
@@ -950,6 +950,9 @@ var AgentService = class {
950
950
  actor: params.actor,
951
951
  ...params.transient === true ? {
952
952
  transient: true
953
+ } : {},
954
+ ...params.newThreadId !== void 0 ? {
955
+ id: params.newThreadId
953
956
  } : {}
954
957
  });
955
958
  threadId = created.id;
@@ -1374,6 +1377,38 @@ var AgentService = class {
1374
1377
  });
1375
1378
  throw new RunNotActiveException(runId);
1376
1379
  }
1380
+ /** Who owns the thread, or `null` when there is no such (live) thread. */
1381
+ async threadOwner(threadId) {
1382
+ return this.store.ownerOfThread(threadId);
1383
+ }
1384
+ /**
1385
+ * May `actor` continue the run whose stream is `runId` — the AG-UI resume? Only the actor whose
1386
+ * thread is streaming it (`403` otherwise), and only while it still is: a run that is over is
1387
+ * waiting on no one (`409 run_not_active`), whatever an old interrupt id says.
1388
+ */
1389
+ async assertResumable(actor, runId) {
1390
+ const owner = await this.store.ownerOfActiveStream(runId);
1391
+ if (owner === null) {
1392
+ throw new RunNotActiveException(runId);
1393
+ }
1394
+ if (owner !== actor.id) {
1395
+ throw new ForbiddenException("run belongs to another actor");
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Everything {@link approve} / {@link reject} check before they signal — who may decide, and
1400
+ * that the request has not lapsed — without signalling. For a caller that settles several
1401
+ * decisions at once and must refuse all of them if one is refused (an AG-UI resume).
1402
+ */
1403
+ async checkDecision(actor, toolCallId2) {
1404
+ await this.assertMayDecide(actor, toolCallId2);
1405
+ await this.assertNotExpired(toolCallId2);
1406
+ }
1407
+ /** Everything {@link answer} checks before it signals, without signalling — see {@link checkDecision}. */
1408
+ async checkAnswer(actor, toolCallId2, answers2) {
1409
+ await this.assertOwnsToolCall(actor, toolCallId2);
1410
+ await this.assertAnswersFit(toolCallId2, answers2);
1411
+ }
1377
1412
  async cancel(actor, runId) {
1378
1413
  await this.assertOwnsActiveStream(actor, runId);
1379
1414
  return this.runner.cancel(runId);
@@ -5062,15 +5097,28 @@ var BASE_CONTROLLERS = [
5062
5097
  ConfigController,
5063
5098
  AttachmentsController
5064
5099
  ];
5065
- var GUARDABLE_CONTROLLERS = BASE_CONTROLLERS;
5066
- function controllersForSurface(surface) {
5067
- return surface === "engine" ? [] : BASE_CONTROLLERS;
5100
+ function adapterControllers(adapters) {
5101
+ return (adapters ?? []).flatMap((adapter) => adapter.controllers);
5102
+ }
5103
+ __name(adapterControllers, "adapterControllers");
5104
+ function adapterProviders(adapters) {
5105
+ return (adapters ?? []).flatMap((adapter) => adapter.providers ?? []);
5106
+ }
5107
+ __name(adapterProviders, "adapterProviders");
5108
+ function controllersForSurface(surface, adapters) {
5109
+ return surface === "engine" ? [] : [
5110
+ ...BASE_CONTROLLERS,
5111
+ ...adapterControllers(adapters)
5112
+ ];
5068
5113
  }
5069
5114
  __name(controllersForSurface, "controllersForSurface");
5070
5115
  var GUARDS_METADATA = "__guards__";
5071
- function applyGuards(guards) {
5116
+ function applyGuards(guards, adapters) {
5072
5117
  const resolved = guards ?? [];
5073
- for (const controller of GUARDABLE_CONTROLLERS) {
5118
+ for (const controller of [
5119
+ ...BASE_CONTROLLERS,
5120
+ ...adapterControllers(adapters)
5121
+ ]) {
5074
5122
  Reflect.defineMetadata(GUARDS_METADATA, resolved, controller);
5075
5123
  }
5076
5124
  }
@@ -5096,7 +5144,7 @@ var AgentModule = class _AgentModule {
5096
5144
  }
5097
5145
  static forRoot(options) {
5098
5146
  const path = options.path ?? DEFAULT_PATH;
5099
- applyGuards(options.guards);
5147
+ applyGuards(options.guards, options.adapters);
5100
5148
  return {
5101
5149
  module: _AgentModule,
5102
5150
  global: true,
@@ -5104,21 +5152,22 @@ var AgentModule = class _AgentModule {
5104
5152
  DiscoveryModule,
5105
5153
  routerFor(path)
5106
5154
  ],
5107
- controllers: controllersForSurface(options.surface),
5155
+ controllers: controllersForSurface(options.surface, options.adapters),
5108
5156
  providers: [
5109
5157
  {
5110
5158
  provide: AGENT_OPTIONS7,
5111
5159
  useValue: options
5112
5160
  },
5113
5161
  ...sharedProviders(options.durable ?? false),
5114
- ...guardProviders(options.guards)
5162
+ ...guardProviders(options.guards),
5163
+ ...adapterProviders(options.adapters)
5115
5164
  ],
5116
5165
  exports: exportsFor()
5117
5166
  };
5118
5167
  }
5119
5168
  static forRootAsync(options) {
5120
5169
  const path = options.path ?? DEFAULT_PATH;
5121
- applyGuards(options.guards);
5170
+ applyGuards(options.guards, options.adapters);
5122
5171
  return {
5123
5172
  module: _AgentModule,
5124
5173
  global: true,
@@ -5127,7 +5176,7 @@ var AgentModule = class _AgentModule {
5127
5176
  routerFor(path),
5128
5177
  ...options.imports ?? []
5129
5178
  ],
5130
- controllers: controllersForSurface(options.surface),
5179
+ controllers: controllersForSurface(options.surface, options.adapters),
5131
5180
  providers: [
5132
5181
  {
5133
5182
  provide: AGENT_OPTIONS7,
@@ -5145,7 +5194,8 @@ var AgentModule = class _AgentModule {
5145
5194
  inject: options.inject ?? []
5146
5195
  },
5147
5196
  ...sharedProviders(options.durable ?? false),
5148
- ...guardProviders(options.guards)
5197
+ ...guardProviders(options.guards),
5198
+ ...adapterProviders(options.adapters)
5149
5199
  ],
5150
5200
  exports: exportsFor()
5151
5201
  };
@@ -5156,6 +5206,326 @@ AgentModule = _ts_decorate22([
5156
5206
  Module({})
5157
5207
  ], AgentModule);
5158
5208
 
5209
+ // src/ag-ui/ag-ui.adapter.ts
5210
+ import { AGENT_ACTOR_RESOLVER as AGENT_ACTOR_RESOLVER14, AGENT_ATTACHMENT_STAGING as AGENT_ATTACHMENT_STAGING5, AGENT_OPTIONS as AGENT_OPTIONS8 } from "@dudousxd/nestjs-agent-core";
5211
+ import { AG_UI_CUSTOM, agUiEvents, agUiFramesFromNdjson, agUiSse, parseRunInput, planResume, readAnswersPayload, readApprovalPayload, readContext, readForwardedProps, readUserTurn } from "@dudousxd/nestjs-agent-core/ag-ui";
5212
+ import { BadRequestException as BadRequestException6, Body as Body6, Controller as Controller14, ForbiddenException as ForbiddenException3, HttpCode as HttpCode2, Inject as Inject21, Optional as Optional9, Post as Post7, Req as Req13, Res as Res2 } from "@nestjs/common";
5213
+ function _ts_decorate23(decorators, target, key, desc) {
5214
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5215
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
5216
+ r = Reflect.decorate(decorators, target, key, desc);
5217
+ } else {
5218
+ for (var i = decorators.length - 1; i >= 0; i--) {
5219
+ if (d = decorators[i]) {
5220
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5221
+ }
5222
+ }
5223
+ }
5224
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
5225
+ }
5226
+ __name(_ts_decorate23, "_ts_decorate");
5227
+ function _ts_metadata22(metadataKey, metadataValue) {
5228
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
5229
+ return Reflect.metadata(metadataKey, metadataValue);
5230
+ }
5231
+ }
5232
+ __name(_ts_metadata22, "_ts_metadata");
5233
+ function _ts_param21(paramIndex, decorator) {
5234
+ return function(target, key) {
5235
+ decorator(target, key, paramIndex);
5236
+ };
5237
+ }
5238
+ __name(_ts_param21, "_ts_param");
5239
+ var AG_UI_VIA = "ag-ui";
5240
+ function invalid(code, message) {
5241
+ return new BadRequestException6({
5242
+ statusCode: 400,
5243
+ code,
5244
+ message
5245
+ });
5246
+ }
5247
+ __name(invalid, "invalid");
5248
+ function warningEvents(warnings) {
5249
+ return warnings.map((message) => ({
5250
+ type: "CUSTOM",
5251
+ name: AG_UI_CUSTOM.warning,
5252
+ value: {
5253
+ message
5254
+ }
5255
+ }));
5256
+ }
5257
+ __name(warningEvents, "warningEvents");
5258
+ var AgUiRunHandler = class {
5259
+ static {
5260
+ __name(this, "AgUiRunHandler");
5261
+ }
5262
+ agent;
5263
+ actorResolver;
5264
+ options;
5265
+ staging;
5266
+ quietMs;
5267
+ constructor(agent, actorResolver, options, staging, quietMs) {
5268
+ this.agent = agent;
5269
+ this.actorResolver = actorResolver;
5270
+ this.options = options;
5271
+ this.staging = staging;
5272
+ this.quietMs = quietMs;
5273
+ }
5274
+ async handle(req, res, body) {
5275
+ const input = parseRunInput(body);
5276
+ if (typeof input === "string") {
5277
+ throw invalid("invalid_input", input);
5278
+ }
5279
+ const forwarded = readForwardedProps(input.forwardedProps);
5280
+ const actor = await this.actorResolver.resolve(req);
5281
+ const warnings = [];
5282
+ if (Array.isArray(input.tools) && input.tools.length > 0) {
5283
+ warnings.push("Frontend tools are not supported by this agent: the tools list was ignored.");
5284
+ }
5285
+ const quiet = this.quietMs !== void 0 ? {
5286
+ quietMs: this.quietMs
5287
+ } : {};
5288
+ if (input.resume !== void 0 && input.resume.length > 0) {
5289
+ const plan = planResume(input.resume);
5290
+ if (typeof plan === "string") {
5291
+ throw invalid("invalid_resume", plan);
5292
+ }
5293
+ for (const id of plan.unrecognised) {
5294
+ warnings.push(`The resume entry ${id} answers an interrupt this agent did not raise.`);
5295
+ }
5296
+ const first = plan.decisions[0];
5297
+ if (first !== void 0) {
5298
+ const streamRunId = first.address.stream;
5299
+ await this.agent.assertResumable(actor, streamRunId);
5300
+ await this.settle(actor, plan.decisions);
5301
+ await this.pipe(res, streamRunId, {
5302
+ threadId: input.threadId,
5303
+ runId: input.runId,
5304
+ streamRunId,
5305
+ skip: first.address.position,
5306
+ answered: plan.decisions.map((decision) => decision.address.toolCallId),
5307
+ ...quiet,
5308
+ preamble: warningEvents(warnings)
5309
+ });
5310
+ return;
5311
+ }
5312
+ }
5313
+ const turn = readUserTurn(input.messages);
5314
+ if (turn === null) {
5315
+ throw invalid("no_user_message", "messages carries no user message to answer");
5316
+ }
5317
+ warnings.push(...turn.dropped);
5318
+ const owner = await this.agent.threadOwner(input.threadId);
5319
+ if (owner !== null && owner !== actor.id) {
5320
+ throw new ForbiddenException3("thread belongs to another actor");
5321
+ }
5322
+ const refs = await this.stage(actor, turn.media, warnings);
5323
+ if (turn.text.trim().length === 0 && refs.length === 0) {
5324
+ throw invalid("no_user_message", "the user message to answer is empty");
5325
+ }
5326
+ const context = readContext(input.context);
5327
+ const pageContext = forwarded.pageContext !== void 0 || context !== void 0 ? {
5328
+ ...forwarded.pageContext,
5329
+ ...context !== void 0 ? {
5330
+ agUiContext: context
5331
+ } : {}
5332
+ } : void 0;
5333
+ const started = await this.agent.chat({
5334
+ actor,
5335
+ message: turn.text,
5336
+ ...owner !== null ? {
5337
+ threadId: input.threadId
5338
+ } : {
5339
+ newThreadId: input.threadId
5340
+ },
5341
+ ...forwarded.agent !== void 0 ? {
5342
+ agentName: forwarded.agent
5343
+ } : {},
5344
+ ...forwarded.model !== void 0 ? {
5345
+ model: forwarded.model
5346
+ } : {},
5347
+ ...pageContext !== void 0 ? {
5348
+ pageContext
5349
+ } : {},
5350
+ ...refs.length > 0 ? {
5351
+ attachments: refs
5352
+ } : {}
5353
+ });
5354
+ await this.pipe(res, started.runId, {
5355
+ threadId: input.threadId,
5356
+ runId: input.runId,
5357
+ streamRunId: started.runId,
5358
+ streamThreadId: started.threadId,
5359
+ ...quiet,
5360
+ preamble: warningEvents(warnings)
5361
+ });
5362
+ }
5363
+ /** Stage the turn's inline media through the bound store, within its limits; say what was not used. */
5364
+ async stage(actor, media, warnings) {
5365
+ const refs = [];
5366
+ const limits = attachmentLimits(this.options, this.staging);
5367
+ for (const item of media) {
5368
+ const label = `The ${item.kind} "${item.filename}" was not used`;
5369
+ if (this.staging === void 0) {
5370
+ warnings.push(`${label}: attachments are not enabled on this agent.`);
5371
+ } else if (refs.length >= limits.maxPerMessage) {
5372
+ warnings.push(`${label}: a message carries at most ${limits.maxPerMessage} attachments.`);
5373
+ } else if (!limits.allowedContentTypes.includes(item.contentType)) {
5374
+ warnings.push(`${label}: ${item.contentType} is not an accepted type.`);
5375
+ } else if (item.data.length > limits.maxBytes) {
5376
+ warnings.push(`${label}: it is larger than ${limits.maxBytes} bytes.`);
5377
+ } else {
5378
+ try {
5379
+ const staged = await this.staging.stage({
5380
+ data: item.data,
5381
+ filename: item.filename,
5382
+ contentType: item.contentType,
5383
+ sizeBytes: item.data.length,
5384
+ actor
5385
+ });
5386
+ refs.push({
5387
+ mediaId: staged.mediaId
5388
+ });
5389
+ } catch {
5390
+ warnings.push(`${label}: it could not be stored.`);
5391
+ }
5392
+ }
5393
+ }
5394
+ return refs;
5395
+ }
5396
+ /**
5397
+ * Deliver what a resume list decided, through the same calls the native decision routes make.
5398
+ * Everything that can refuse — who may decide, an answer a question's rules reject, a payload that
5399
+ * says nothing — is checked for EVERY entry before the first one is delivered, so a list with one
5400
+ * bad entry settles nothing.
5401
+ */
5402
+ async settle(actor, decisions) {
5403
+ const deliveries = [];
5404
+ for (const { address, entry } of decisions) {
5405
+ const { toolCallId: toolCallId2 } = address;
5406
+ const abandoned = entry.status === "cancelled";
5407
+ if (address.kind === "approval") {
5408
+ await this.agent.checkDecision(actor, toolCallId2);
5409
+ const decision = abandoned ? {
5410
+ approved: false
5411
+ } : readApprovalPayload(entry.payload);
5412
+ if (decision === null) {
5413
+ throw invalid("invalid_resume", "an approval is answered with { approved: boolean, reason?, remember? }");
5414
+ }
5415
+ const reason = rejectReason(decision.reason ?? (abandoned ? "abandoned" : void 0));
5416
+ deliveries.push(() => decision.approved ? this.agent.approve(actor, toolCallId2, {
5417
+ via: AG_UI_VIA,
5418
+ ...decision.remember === true ? {
5419
+ remember: true
5420
+ } : {}
5421
+ }) : this.agent.reject(actor, toolCallId2, reason, {
5422
+ via: AG_UI_VIA
5423
+ }));
5424
+ continue;
5425
+ }
5426
+ if (abandoned) {
5427
+ await this.agent.checkAnswer(actor, toolCallId2, {});
5428
+ deliveries.push(() => this.agent.skip(actor, toolCallId2, {
5429
+ via: AG_UI_VIA
5430
+ }));
5431
+ continue;
5432
+ }
5433
+ const read = readAnswersPayload(entry.payload);
5434
+ if (read === null) {
5435
+ throw invalid("invalid_resume", "a question set is answered with { answers: { <questionId>: string[] } }");
5436
+ }
5437
+ const answers2 = answers(read) ?? {};
5438
+ await this.agent.checkAnswer(actor, toolCallId2, answers2);
5439
+ deliveries.push(() => this.agent.answer(actor, toolCallId2, answers2, {
5440
+ via: AG_UI_VIA
5441
+ }));
5442
+ }
5443
+ for (const deliver of deliveries) await deliver();
5444
+ }
5445
+ /**
5446
+ * Pipe a run to the client as AG-UI 1.0 over SSE: one event per `data:` line, from `RUN_STARTED`
5447
+ * to the run's terminal event. It ends when the run stops to ask — the library run stays parked,
5448
+ * and the request that answers re-attaches to it.
5449
+ */
5450
+ async pipe(res, runId, options) {
5451
+ res.status(200);
5452
+ res.setHeader("Content-Type", "text/event-stream");
5453
+ res.setHeader("Cache-Control", "no-cache, no-transform");
5454
+ res.setHeader("Connection", "keep-alive");
5455
+ res.setHeader("X-Accel-Buffering", "no");
5456
+ res.setHeader("X-Agent-Run-Id", runId);
5457
+ res.flushHeaders();
5458
+ let terminal = false;
5459
+ try {
5460
+ const frames = agUiFramesFromNdjson(this.agent.subscribe(runId));
5461
+ for await (const event of agUiEvents(frames, options)) {
5462
+ if (event.type === "RUN_FINISHED" || event.type === "RUN_ERROR") terminal = true;
5463
+ res.write(agUiSse(event));
5464
+ }
5465
+ } catch {
5466
+ if (!terminal) {
5467
+ res.write(agUiSse({
5468
+ type: "RUN_ERROR",
5469
+ message: "The run could not be followed.",
5470
+ code: "run_failed"
5471
+ }));
5472
+ }
5473
+ }
5474
+ res.end();
5475
+ }
5476
+ };
5477
+ function agUiAdapter(options = {}) {
5478
+ const path = (options.path ?? "ag-ui").replace(/^\/+|\/+$/g, "");
5479
+ const quietMs = options.quietMs;
5480
+ let AgUiController = class AgUiController extends AgUiRunHandler {
5481
+ static {
5482
+ __name(this, "AgUiController");
5483
+ }
5484
+ constructor(agent, actorResolver, moduleOptions, staging) {
5485
+ super(agent, actorResolver, moduleOptions, staging, quietMs);
5486
+ }
5487
+ run(req, res, body) {
5488
+ return this.handle(req, res, body);
5489
+ }
5490
+ };
5491
+ _ts_decorate23([
5492
+ Post7(path),
5493
+ HttpCode2(200),
5494
+ _ts_param21(0, Req13()),
5495
+ _ts_param21(1, Res2()),
5496
+ _ts_param21(2, Body6()),
5497
+ _ts_metadata22("design:type", Function),
5498
+ _ts_metadata22("design:paramtypes", [
5499
+ typeof Request === "undefined" ? Object : Request,
5500
+ typeof Response === "undefined" ? Object : Response,
5501
+ Object
5502
+ ]),
5503
+ _ts_metadata22("design:returntype", typeof Promise === "undefined" ? Object : Promise)
5504
+ ], AgUiController.prototype, "run", null);
5505
+ AgUiController = _ts_decorate23([
5506
+ Controller14(),
5507
+ _ts_param21(0, Inject21(AgentService)),
5508
+ _ts_param21(1, Inject21(AGENT_ACTOR_RESOLVER14)),
5509
+ _ts_param21(2, Inject21(AGENT_OPTIONS8)),
5510
+ _ts_param21(3, Optional9()),
5511
+ _ts_param21(3, Inject21(AGENT_ATTACHMENT_STAGING5)),
5512
+ _ts_metadata22("design:type", Function),
5513
+ _ts_metadata22("design:paramtypes", [
5514
+ typeof AgentService === "undefined" ? Object : AgentService,
5515
+ typeof ActorResolver === "undefined" ? Object : ActorResolver,
5516
+ typeof AgentModuleOptions === "undefined" ? Object : AgentModuleOptions,
5517
+ typeof AttachmentStagingStore === "undefined" ? Object : AttachmentStagingStore
5518
+ ])
5519
+ ], AgUiController);
5520
+ return {
5521
+ name: "ag-ui",
5522
+ controllers: [
5523
+ AgUiController
5524
+ ]
5525
+ };
5526
+ }
5527
+ __name(agUiAdapter, "agUiAdapter");
5528
+
5159
5529
  // src/resolver/header-actor-resolver.ts
5160
5530
  import { UnauthorizedException } from "@nestjs/common";
5161
5531
  function readHeader(req, name) {
@@ -5238,6 +5608,7 @@ export {
5238
5608
  AGENT_TOOL_BRAND,
5239
5609
  AI_TOOL_METADATA,
5240
5610
  ATTACHMENT_PAGE_SIZE,
5611
+ AgUiRunHandler,
5241
5612
  Agent,
5242
5613
  AgentApprovalPortAdapter,
5243
5614
  AgentDepsFactory2 as AgentDepsFactory,
@@ -5271,6 +5642,7 @@ export {
5271
5642
  SystemPrompt,
5272
5643
  SystemPromptContributor,
5273
5644
  ToolsController,
5645
+ agUiAdapter,
5274
5646
  attachmentLimits,
5275
5647
  declaredSkillProvider,
5276
5648
  defaultRequestUserMapper,