@adonis-agora/agent 0.47.0 → 0.49.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 (71) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/providers/agent_provider.d.ts.map +1 -1
  3. package/dist/providers/agent_provider.js +84 -60
  4. package/dist/providers/agent_provider.js.map +1 -1
  5. package/dist/providers/dashboard_provider.js +1 -1
  6. package/dist/providers/dashboard_provider.js.map +1 -1
  7. package/dist/src/agent-loop.d.ts.map +1 -1
  8. package/dist/src/agent-loop.js +30 -8
  9. package/dist/src/agent-loop.js.map +1 -1
  10. package/dist/src/agent-service.d.ts +33 -9
  11. package/dist/src/agent-service.d.ts.map +1 -1
  12. package/dist/src/agent-service.js +58 -4
  13. package/dist/src/agent-service.js.map +1 -1
  14. package/dist/src/client/chat-client.d.ts +20 -0
  15. package/dist/src/client/chat-client.d.ts.map +1 -1
  16. package/dist/src/client/chat-client.js +38 -2
  17. package/dist/src/client/chat-client.js.map +1 -1
  18. package/dist/src/client/index.d.ts +1 -1
  19. package/dist/src/client/index.d.ts.map +1 -1
  20. package/dist/src/client/index.js +1 -1
  21. package/dist/src/client/index.js.map +1 -1
  22. package/dist/src/client/sse.d.ts +14 -10
  23. package/dist/src/client/sse.d.ts.map +1 -1
  24. package/dist/src/client/sse.js +20 -73
  25. package/dist/src/client/sse.js.map +1 -1
  26. package/dist/src/define_config.d.ts +3 -14
  27. package/dist/src/define_config.d.ts.map +1 -1
  28. package/dist/src/define_config.js.map +1 -1
  29. package/dist/src/elicitation.d.ts +12 -0
  30. package/dist/src/elicitation.d.ts.map +1 -1
  31. package/dist/src/elicitation.js +9 -1
  32. package/dist/src/elicitation.js.map +1 -1
  33. package/dist/src/genui/index.d.ts +1 -1
  34. package/dist/src/genui/index.js +1 -1
  35. package/dist/src/index.d.ts +6 -5
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -4
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/redis-token-stream-sink.d.ts +2 -3
  40. package/dist/src/redis-token-stream-sink.d.ts.map +1 -1
  41. package/dist/src/redis-token-stream-sink.js +2 -3
  42. package/dist/src/redis-token-stream-sink.js.map +1 -1
  43. package/dist/src/spi/model-catalog.d.ts +13 -0
  44. package/dist/src/spi/model-catalog.d.ts.map +1 -1
  45. package/dist/src/spi/model-catalog.js.map +1 -1
  46. package/dist/src/spi/quota-provider.d.ts +30 -1
  47. package/dist/src/spi/quota-provider.d.ts.map +1 -1
  48. package/dist/src/spi/quota-provider.js +34 -1
  49. package/dist/src/spi/quota-provider.js.map +1 -1
  50. package/dist/src/spi/token-stream-sink.d.ts +2 -3
  51. package/dist/src/spi/token-stream-sink.d.ts.map +1 -1
  52. package/dist/src/spi/token-stream-sink.js.map +1 -1
  53. package/dist/src/spi/tool.d.ts +1 -2
  54. package/dist/src/spi/tool.d.ts.map +1 -1
  55. package/dist/src/sse.d.ts +0 -20
  56. package/dist/src/sse.d.ts.map +1 -1
  57. package/dist/src/sse.js +0 -27
  58. package/dist/src/sse.js.map +1 -1
  59. package/dist/src/stores/ledger-quota-provider.d.ts +10 -1
  60. package/dist/src/stores/ledger-quota-provider.d.ts.map +1 -1
  61. package/dist/src/stores/ledger-quota-provider.js +14 -3
  62. package/dist/src/stores/ledger-quota-provider.js.map +1 -1
  63. package/dist/src/stream-events.d.ts +2 -2
  64. package/dist/src/tool-presentation.d.ts +5 -0
  65. package/dist/src/tool-presentation.d.ts.map +1 -1
  66. package/dist/src/tool-presentation.js +5 -1
  67. package/dist/src/tool-presentation.js.map +1 -1
  68. package/dist/src/types.d.ts +19 -1
  69. package/dist/src/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/skills/agent-testing/SKILL.md +5 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @adonis-agora/agent
2
2
 
3
+ ## 0.49.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#222](https://github.com/DavideCarvalho/adonis-agora-agent/pull/222) [`241bfdc`](https://github.com/DavideCarvalho/adonis-agora-agent/commit/241bfdc298373052435890daa4513597e046377d) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - The chat contract `@dudousxd/nestjs-agent` settled (its `docs/stream-protocol.md`), ported:
8
+
9
+ - **Refusals answer `{ message, code? }` (BREAKING for readers of `error`).** Every refused request on the agent routes (and the dashboard's asset route) answers its status with `{ message, code? }` instead of `{ error }` — `code` where there is a machine-readable reason (`unauthorized`, `model_not_allowed`, `thread_required`, `quota_exceeded`). `@adonis-agora/agent/client` now throws a refused send as `AgentChatHttpError` (`status`, `code`, `body`, and the server's `message` as its own message) instead of a bare `Error('Failed to start agent chat (HTTP n).')`.
10
+ - **A send's `model` is that turn's only** — never stored on the thread; `PATCH threads/:id { model }` is the only pin (documented and tested; the service already behaved this way).
11
+ - **Regenerate.** `POST chat { threadId, regenerate: true }` answers the thread's last user message again: `400 thread_required` without a thread, `message` ignored, no user message stored, the answer(s) after it dropped (a new `regenerate:truncate` durable step — normal turns journal exactly what they did), a new run. `AgentRunInput.regenerate` replaces the unused `isRegenerate`; `RegenerateNeedsThreadError` is exported.
12
+ - **Model lock.** `ModelCatalogView.locked: { model, reason? }` runs every turn of that agent on `locked.model` and refuses a send naming another (`400 model_not_allowed`); `GET agents` entries carry `lockedModel` (`AgentCatalogEntry`). `listAgents(actor)` is now async.
13
+ - **Quota soft limit and USD-only windows.** `QuotaWindow.usedTokens` is optional; `QuotaWindow.warnAt`, `QuotaReport.warning: { period, ratio, reason? }` (never while blocked), `quotaWarning(windows)`, `quotaUsedRatio(window)`; `quota: { limits, warnAt }` stamps it on the ledger windows (`LedgerQuotaProvider`'s new `options.warnAt`). `exhaustedWindow` reads a missing `usedTokens` as `0`.
14
+ - **Who answered a question.** `POST tool-call/answer|skip` take `via?` (1–64 characters, `'web'` by default; `400` otherwise); the settled outcome carries `answeredBy` / `answeredVia` (`ElicitationReply.answeredVia`, `ElicitationOutcome.answeredBy|answeredVia`), streamed as the call's `tool-output` and persisted with its result.
15
+ - **`GET tools?agent=*`** (`ALL_AGENTS`) answers the union across every agent, each tool once, under the same gates; an unknown name is still `404`.
16
+ - **Frame ids.** `SseEvent.id` (the client parser now keeps the `id:` line). Ids stay strictly increasing within a run; readers must tolerate gaps.
17
+
18
+ ## 0.48.0
19
+
20
+ ### Minor Changes
21
+
22
+ - [#220](https://github.com/DavideCarvalho/adonis-agora-agent/pull/220) [`17ea478`](https://github.com/DavideCarvalho/adonis-agora-agent/commit/17ea478a7531d15bf42b87335036ed04e319ccab) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - **BREAKING — the agent stream protocol is the only wire.** `POST /agent/chat` and `GET /agent/chat/:runId/stream` always write the chat stream protocol shared with `@dudousxd/nestjs-agent`: `event: meta`, then one numbered `AgentStreamEvent` per `data:` frame (`{"kind":"text","text":…}`, `ui`, `elicitation`, `approval-requested`, reasoning, tool calls, steps, title, …), then `event: done` — or `event: error` `{ code, message }` for a failed run. `@dudousxd/nestjs-agent-react` and `@adonis-agora/agent/client` / `./react` read it unchanged.
23
+
24
+ - **The `'legacy'` envelope is removed** — `data: {"delta":…}` text, the named `event: component` / `event: elicitation` / `event: approval` frames and the `[error]` text delta are no longer written, and `@adonis-agora/agent/client`'s `decodeFrame` no longer reads them (a `{"delta"}` payload decodes to `null`).
25
+ - **The `streamProtocol` option is removed** from `defineConfig`, with no shim: delete `streamProtocol: 'agent'` from `config/agent.ts`; if you relied on the old `'legacy'` default, move the browser to `@adonis-agora/agent/client` (or `./react`, or `@dudousxd/nestjs-agent-react`) or read `{"kind":"text","text":…}` where you read `{"delta":…}`.
26
+ - **`frameToSse` and the `StreamProtocol` type are no longer exported.** Use `AgentSseEncoder` (or the pure `frameToEvents`).
27
+
28
+ `ChatFrame` / `ChatPart` / `foldPart` are unchanged, so code built on the package client needs no change beyond the server config.
29
+
3
30
  ## 0.47.0
4
31
 
5
32
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"agent_provider.d.ts","sourceRoot":"","sources":["../../providers/agent_provider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAoJ/D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;;IAMpB,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAA7C,YAAsB,GAAG,EAAE,kBAAkB,EAAI;IAEjD,QAAQ,SAcP;IAEK,IAAI,kBAiJT;IAEK,QAAQ,kBASb;CAynCF"}
1
+ {"version":3,"file":"agent_provider.d.ts","sourceRoot":"","sources":["../../providers/agent_provider.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA4J/D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,OAAO,OAAO,aAAa;;IAMpB,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAA7C,YAAsB,GAAG,EAAE,kBAAkB,EAAI;IAEjD,QAAQ,SAcP;IAEK,IAAI,kBAsJT;IAEK,QAAQ,kBASb;CAqpCF"}
@@ -1,5 +1,5 @@
1
1
  import { pathToFileURL } from 'node:url';
2
- import { AgentDepsFactory, AgentGenui, AgentRegistry, AgentService, AgentSseEncoder, AnonymousActorResolver, AttachmentRefusedError, DefaultToolAuthorizer, discoverTools, evaluateGovernanceGate, evaluateOwnership, frameToSse, governanceQueries as governanceQueriesFactories, HumanReplyMismatchError, InlineAgentRunner, InProcessTokenStreamSink, isQuotaProvider, LedgerQuotaProvider, lucidStoreConnection, ModelNotAllowedError, memoryForgetVerdict, offerMemories, parseStreamCursor, pricingStores, QuotaBlockedError, REQUESTER_APPROVER, registerDelegateTools, registerFunctionalTool, registerToolsFromBarrel, resolveActorResolver, ToolRegistry, toApprovalPolicy, toModelCatalog, withActorLabel, withActorLabels, } from '../src/index.js';
2
+ import { AgentDepsFactory, AgentGenui, AgentRegistry, AgentService, AgentSseEncoder, ALL_AGENTS, AnonymousActorResolver, AttachmentRefusedError, DefaultToolAuthorizer, discoverTools, evaluateGovernanceGate, evaluateOwnership, governanceQueries as governanceQueriesFactories, HumanReplyMismatchError, InlineAgentRunner, InProcessTokenStreamSink, isQuotaProvider, LedgerQuotaProvider, lucidStoreConnection, ModelNotAllowedError, memoryForgetVerdict, offerMemories, parseStreamCursor, pricingStores, QuotaBlockedError, REQUESTER_APPROVER, RegenerateNeedsThreadError, registerDelegateTools, registerFunctionalTool, registerToolsFromBarrel, resolveActorResolver, ToolRegistry, toApprovalPolicy, toModelCatalog, withActorLabel, withActorLabels, } from '../src/index.js';
3
3
  import { McpToolImporter } from '../src/mcp-client/index.js';
4
4
  import { setTelescopeGovernanceQueries } from '../src/telescope/governance-registry.js';
5
5
  /** The catalog a model provider carries (`aiSdkModels(…).catalog`), if it carries one. */
@@ -174,7 +174,7 @@ export default class AgentProvider {
174
174
  ? undefined
175
175
  : isQuotaProvider(config.quota)
176
176
  ? config.quota
177
- : new LedgerQuotaProvider(store, undefined, config.quota.limits);
177
+ : new LedgerQuotaProvider(store, undefined, config.quota.limits, config.quota.warnAt !== undefined ? { warnAt: config.quota.warnAt } : {});
178
178
  const pricingStore = await this.#resolvePricing(config);
179
179
  // Governance read-model is resolved after pricing so the Lucid read-model prices its rollups
180
180
  // against the same live prices the loop's cost fold uses.
@@ -422,7 +422,6 @@ export default class AgentProvider {
422
422
  allowedContentTypes: declared.allowedContentTypes ?? DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES,
423
423
  };
424
424
  const defaultAgentName = config.defaultAgent?.name ?? 'default';
425
- const protocol = config.streamProtocol ?? 'legacy';
426
425
  // 1. POST /agent/chat — resolve actor, start the run, SSE-pipe the token stream. When the caller
427
426
  // continues an EXISTING thread (`body.threadId`), it must own it: otherwise an authenticated caller
428
427
  // could pass another actor's threadId to load that thread's full history into the model (read it
@@ -442,15 +441,24 @@ export default class AgentProvider {
442
441
  if (!(await this.#assertOwner(ctx, actor, owner, 'thread', governanceAuthorize)))
443
442
  return;
444
443
  }
444
+ const regenerate = body.regenerate === true;
445
+ if (regenerate && body.threadId === undefined) {
446
+ return ctx.response.badRequest({
447
+ message: 'regenerate requires an existing threadId',
448
+ code: 'thread_required',
449
+ });
450
+ }
445
451
  const refs = attachmentRefs(body.attachments);
446
452
  if (typeof refs === 'string') {
447
- return ctx.response.badRequest({ error: refs });
453
+ return ctx.response.badRequest({ message: refs });
448
454
  }
449
455
  let started;
450
456
  try {
451
457
  started = await service.chat({
452
458
  actor,
453
- message: body.message,
459
+ // A regenerate answers the stored user message; whatever `message` says is ignored.
460
+ message: regenerate ? '' : body.message,
461
+ ...(regenerate ? { regenerate: true } : {}),
454
462
  ...(typeof body.model === 'string' && body.model.length > 0 ? { model: body.model } : {}),
455
463
  ...(body.threadId !== undefined ? { threadId: body.threadId } : {}),
456
464
  ...(body.agent !== undefined ? { agentName: body.agent } : {}),
@@ -461,10 +469,13 @@ export default class AgentProvider {
461
469
  }
462
470
  catch (error) {
463
471
  if (error instanceof AttachmentRefusedError) {
464
- return ctx.response.status(error.status).json({ error: error.message });
472
+ return ctx.response.status(error.status).json({ message: error.message });
465
473
  }
466
474
  if (error instanceof ModelNotAllowedError) {
467
- return ctx.response.badRequest({ error: error.message });
475
+ return ctx.response.badRequest({ message: error.message, code: 'model_not_allowed' });
476
+ }
477
+ if (error instanceof RegenerateNeedsThreadError) {
478
+ return ctx.response.badRequest({ message: error.message, code: 'thread_required' });
468
479
  }
469
480
  if (error instanceof QuotaBlockedError) {
470
481
  return ctx.response.status(429).json({
@@ -475,7 +486,7 @@ export default class AgentProvider {
475
486
  }
476
487
  throw error;
477
488
  }
478
- await this.#pipe(ctx, service, protocol, started.runId, started.threadId);
489
+ await this.#pipe(ctx, service, started.runId, started.threadId);
479
490
  });
480
491
  // 2. GET /agent/chat/:runId/stream — re-attach SSE. Authenticated: the actor resolver reads the
481
492
  // request's session/cookies (an EventSource can't set headers), 401 on failure — so an anonymous
@@ -491,14 +502,14 @@ export default class AgentProvider {
491
502
  // Nothing buffered under the run — it ended while the client was away, or its buffer went with
492
503
  // a restarted process. A 404 reads as "nothing to resume"; subscribing would wait forever.
493
504
  if (!(await service.hasStream(runId))) {
494
- return ctx.response.notFound({ error: 'Nothing is streaming under that run.' });
505
+ return ctx.response.notFound({ message: 'Nothing is streaming under that run.' });
495
506
  }
496
507
  // `?after=<seq>` (or the `Last-Event-ID` a browser EventSource sends on its own) skips the
497
508
  // events a reconnecting client already has; `after` wins when both are present.
498
509
  const after = parseStreamCursor(ctx.request.qs().after) ??
499
510
  parseStreamCursor(ctx.request.header('last-event-id')) ??
500
511
  0;
501
- await this.#pipe(ctx, service, protocol, runId, undefined, after);
512
+ await this.#pipe(ctx, service, runId, undefined, after);
502
513
  });
503
514
  // 3. POST /agent/chat/:runId/cancel — authenticated + owner-scoped (a caller cancels only its own
504
515
  // runs, unless governance-privileged).
@@ -525,7 +536,7 @@ export default class AgentProvider {
525
536
  const via = decisionVia(body.via);
526
537
  if (remember === null || via === null) {
527
538
  return ctx.response.badRequest({
528
- error: `remember must be a boolean and via a string of 1-${MAX_VIA_LENGTH} characters`,
539
+ message: `remember must be a boolean and via a string of 1-${MAX_VIA_LENGTH} characters`,
529
540
  });
530
541
  }
531
542
  const runId = await this.#runOfToolCall(ctx, service, body);
@@ -545,7 +556,7 @@ export default class AgentProvider {
545
556
  const via = decisionVia(body.via);
546
557
  if (via === null) {
547
558
  return ctx.response.badRequest({
548
- error: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
559
+ message: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
549
560
  });
550
561
  }
551
562
  const runId = await this.#runOfToolCall(ctx, service, body);
@@ -564,6 +575,12 @@ export default class AgentProvider {
564
575
  if (actor === null)
565
576
  return;
566
577
  const body = (ctx.request.body() ?? {});
578
+ const via = decisionVia(body.via);
579
+ if (via === null) {
580
+ return ctx.response.badRequest({
581
+ message: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
582
+ });
583
+ }
567
584
  const runId = await this.#runOfToolCall(ctx, service, body);
568
585
  if (runId === null)
569
586
  return;
@@ -574,7 +591,7 @@ export default class AgentProvider {
574
591
  // the person who typed it hears why — rather than signalled and quietly dropped by the loop.
575
592
  const problem = await service.answerProblem(body.toolCallId, body.answers ?? {});
576
593
  if (problem !== null) {
577
- return ctx.response.badRequest({ error: problem });
594
+ return ctx.response.badRequest({ message: problem });
578
595
  }
579
596
  try {
580
597
  await service.answer({
@@ -582,6 +599,7 @@ export default class AgentProvider {
582
599
  toolCallId: body.toolCallId,
583
600
  answers: body.answers ?? {},
584
601
  answeredByRef: actor.id,
602
+ answeredVia: via,
585
603
  });
586
604
  }
587
605
  catch (error) {
@@ -596,6 +614,12 @@ export default class AgentProvider {
596
614
  if (actor === null)
597
615
  return;
598
616
  const body = (ctx.request.body() ?? {});
617
+ const via = decisionVia(body.via);
618
+ if (via === null) {
619
+ return ctx.response.badRequest({
620
+ message: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
621
+ });
622
+ }
599
623
  const runId = await this.#runOfToolCall(ctx, service, body);
600
624
  if (runId === null)
601
625
  return;
@@ -607,6 +631,7 @@ export default class AgentProvider {
607
631
  runId,
608
632
  toolCallId: body.toolCallId,
609
633
  answeredByRef: actor.id,
634
+ answeredVia: via,
610
635
  });
611
636
  }
612
637
  catch (error) {
@@ -631,11 +656,11 @@ export default class AgentProvider {
631
656
  const threadId = await service.threadOfMessage(messageId);
632
657
  if (threadId === undefined) {
633
658
  return ctx.response.status(501).json({
634
- error: 'Message feedback requires an AgentStore that implements threadOfMessage() and setMessageFeedback().',
659
+ message: 'Message feedback requires an AgentStore that implements threadOfMessage() and setMessageFeedback().',
635
660
  });
636
661
  }
637
662
  if (threadId === null) {
638
- return ctx.response.notFound({ error: 'message not found' });
663
+ return ctx.response.notFound({ message: 'message not found' });
639
664
  }
640
665
  const owner = await service.threadOwner(threadId);
641
666
  if (!(await this.#assertOwner(ctx, actor, owner, 'thread', undefined)))
@@ -646,7 +671,7 @@ export default class AgentProvider {
646
671
  ...(body.comment !== undefined ? { comment: body.comment } : {}),
647
672
  });
648
673
  if (!result.ok) {
649
- return ctx.response.status(result.status).json({ error: result.error });
674
+ return ctx.response.status(result.status).json({ message: result.error });
650
675
  }
651
676
  return ctx.response.json({ feedback: result.feedback });
652
677
  });
@@ -664,16 +689,23 @@ export default class AgentProvider {
664
689
  const actor = await this.#resolveActor(ctx, actorResolver);
665
690
  if (actor === null)
666
691
  return;
667
- return ctx.response.json(service.listAgents());
692
+ return ctx.response.json(await service.listAgents(actor));
668
693
  });
669
694
  // 6b. GET /agent/tools?agent= — the tools this caller can reach through an agent, with how a chat
670
695
  // surface should talk about each (`presentation`). The same list the model is offered; nothing the
671
696
  // caller passes widens it — `agent` only narrows to that agent's allow-list, and an unknown name is
672
- // a 404 rather than the widest answer (no allow-list at all).
697
+ // a 404 rather than the widest answer (no allow-list at all). `?agent=*` is the union across every
698
+ // agent: each tool this caller reaches through any of them, once, under the same gates.
673
699
  router.get(p('tools'), async (ctx) => {
674
700
  const agent = ctx.request.qs().agent;
701
+ if (agent === ALL_AGENTS) {
702
+ const actor = await this.#resolveActor(ctx, actorResolver);
703
+ if (actor === null)
704
+ return;
705
+ return ctx.response.json(await service.toolCatalogForAllAgents(actor));
706
+ }
675
707
  if (agent !== undefined && agent !== defaultAgentName && agents.get(agent) === undefined) {
676
- return ctx.response.notFound({ error: `No agent named "${agent}"` });
708
+ return ctx.response.notFound({ message: `No agent named "${agent}"` });
677
709
  }
678
710
  const actor = await this.#resolveActor(ctx, resolveActorResolver(actorResolver, agents.get(agent ?? defaultAgentName)));
679
711
  if (actor === null)
@@ -713,20 +745,20 @@ export default class AgentProvider {
713
745
  return;
714
746
  const body = (ctx.request.body() ?? {});
715
747
  if (body.model !== undefined && body.model !== null && typeof body.model !== 'string') {
716
- return ctx.response.badRequest({ error: 'model must be a string or null' });
748
+ return ctx.response.badRequest({ message: 'model must be a string or null' });
717
749
  }
718
750
  if (body.model !== undefined) {
719
751
  try {
720
752
  const pinned = await service.setThreadModel(actor, threadId, body.model);
721
753
  if (!pinned) {
722
754
  return ctx.response.status(501).json({
723
- error: "Pinning a thread's model requires an AgentStore that implements updateThread().",
755
+ message: "Pinning a thread's model requires an AgentStore that implements updateThread().",
724
756
  });
725
757
  }
726
758
  }
727
759
  catch (error) {
728
760
  if (error instanceof ModelNotAllowedError) {
729
- return ctx.response.badRequest({ error: error.message });
761
+ return ctx.response.badRequest({ message: error.message });
730
762
  }
731
763
  throw error;
732
764
  }
@@ -734,7 +766,7 @@ export default class AgentProvider {
734
766
  if (body.title !== undefined) {
735
767
  const title = typeof body.title === 'string' ? body.title.trim() : '';
736
768
  if (title.length === 0 || title.length > 200) {
737
- return ctx.response.badRequest({ error: 'title must be a string of 1-200 characters' });
769
+ return ctx.response.badRequest({ message: 'title must be a string of 1-200 characters' });
738
770
  }
739
771
  await service.renameThread(threadId, title);
740
772
  }
@@ -799,16 +831,16 @@ export default class AgentProvider {
799
831
  if (actor === null)
800
832
  return;
801
833
  if (memory === undefined) {
802
- return ctx.response.notFound({ error: 'No memory is configured in this deployment.' });
834
+ return ctx.response.notFound({ message: 'No memory is configured in this deployment.' });
803
835
  }
804
836
  const id = String(ctx.params.id);
805
837
  const entry = (await everyMemoryOf(actor)).entries.find((candidate) => candidate.id === id);
806
838
  if (entry === undefined) {
807
- return ctx.response.notFound({ error: `No memory with id "${id}".` });
839
+ return ctx.response.notFound({ message: `No memory with id "${id}".` });
808
840
  }
809
841
  const verdict = memoryForgetVerdict({ record: entry, actor });
810
842
  if (!verdict.allowed) {
811
- return ctx.response.forbidden({ error: verdict.reason });
843
+ return ctx.response.forbidden({ message: verdict.reason });
812
844
  }
813
845
  return ctx.response.json({
814
846
  forgotten: await memory.provider.forget({ id: entry.id, ctx: { actor, threadId: '' } }),
@@ -857,19 +889,19 @@ export default class AgentProvider {
857
889
  return;
858
890
  const file = ctx.request.file('file');
859
891
  if (file === null || file.tmpPath === undefined) {
860
- return ctx.response.status(400).json({ error: 'multipart field "file" is required' });
892
+ return ctx.response.status(400).json({ message: 'multipart field "file" is required' });
861
893
  }
862
894
  const contentType = file.headers?.['content-type']?.split(';')[0]?.trim() ?? `${file.type}/${file.subtype}`;
863
895
  if (!allowedContentTypes.includes(contentType)) {
864
896
  return ctx.response.status(415).json({
865
- error: `content type "${contentType}" is not allowed (allowed: ${allowedContentTypes.join(', ')})`,
897
+ message: `content type "${contentType}" is not allowed (allowed: ${allowedContentTypes.join(', ')})`,
866
898
  });
867
899
  }
868
900
  const sizeBytes = file.size;
869
901
  if (sizeBytes > maxBytes) {
870
902
  return ctx.response
871
903
  .status(413)
872
- .json({ error: `file exceeds the ${maxBytes}-byte limit` });
904
+ .json({ message: `file exceeds the ${maxBytes}-byte limit` });
873
905
  }
874
906
  const { readFile } = await import('node:fs/promises');
875
907
  const data = await readFile(file.tmpPath);
@@ -986,7 +1018,7 @@ export default class AgentProvider {
986
1018
  if (gov.threadDetail === undefined) {
987
1019
  return ctx.response
988
1020
  .status(501)
989
- .json({ error: 'this governance adapter does not support thread detail' });
1021
+ .json({ message: 'this governance adapter does not support thread detail' });
990
1022
  }
991
1023
  const detail = await withActorLabel(await gov.threadDetail(String(ctx.params.id)), actorDirectory);
992
1024
  if (detail === null)
@@ -1102,7 +1134,7 @@ export default class AgentProvider {
1102
1134
  typeof body.inputPricePer1m !== 'number' ||
1103
1135
  typeof body.outputPricePer1m !== 'number') {
1104
1136
  return ctx.response.status(400).json({
1105
- error: 'body must be {modelId: string, inputPricePer1m: number, outputPricePer1m: number, cacheWritePricePer1m?: number, cacheReadPricePer1m?: number}',
1137
+ message: 'body must be {modelId: string, inputPricePer1m: number, outputPricePer1m: number, cacheWritePricePer1m?: number, cacheReadPricePer1m?: number}',
1106
1138
  });
1107
1139
  }
1108
1140
  await pricing.upsertModelPrice({
@@ -1131,7 +1163,7 @@ export default class AgentProvider {
1131
1163
  try {
1132
1164
  const actor = await actorResolver.resolve(ctx);
1133
1165
  if (actor === null || actor === undefined) {
1134
- ctx.response.status(401).json({ error: 'unauthorized' });
1166
+ ctx.response.status(401).json({ message: 'unauthorized', code: 'unauthorized' });
1135
1167
  return null;
1136
1168
  }
1137
1169
  return actor;
@@ -1141,9 +1173,10 @@ export default class AgentProvider {
1141
1173
  // in production, every request here is untrusted by definition, so the detail stays server-side
1142
1174
  // (dev/test keeps it, matching `evaluateDashboardGate`/`evaluateGovernanceGate`'s `debug` knob).
1143
1175
  const debug = !this.app.inProduction;
1144
- ctx.response
1145
- .status(401)
1146
- .json({ error: debug && error instanceof Error ? error.message : 'unauthorized' });
1176
+ ctx.response.status(401).json({
1177
+ message: debug && error instanceof Error ? error.message : 'unauthorized',
1178
+ code: 'unauthorized',
1179
+ });
1147
1180
  return null;
1148
1181
  }
1149
1182
  }
@@ -1159,7 +1192,7 @@ export default class AgentProvider {
1159
1192
  return null;
1160
1193
  const verdict = await evaluateGovernanceGate(actor, ctx, governanceAuthorize, !this.app.inProduction);
1161
1194
  if (!verdict.ok) {
1162
- ctx.response.status(verdict.status).json({ error: verdict.error });
1195
+ ctx.response.status(verdict.status).json({ message: verdict.error });
1163
1196
  return null;
1164
1197
  }
1165
1198
  return actor;
@@ -1181,7 +1214,7 @@ export default class AgentProvider {
1181
1214
  const verdict = evaluateOwnership(actor.id, ownerRef, privileged);
1182
1215
  if (!verdict.ok) {
1183
1216
  const error = verdict.status === 404 ? `${kind} not found` : 'forbidden';
1184
- ctx.response.status(verdict.status).json({ error });
1217
+ ctx.response.status(verdict.status).json({ message: error });
1185
1218
  return false;
1186
1219
  }
1187
1220
  return true;
@@ -1202,7 +1235,7 @@ export default class AgentProvider {
1202
1235
  return body.runId;
1203
1236
  const runId = typeof body.toolCallId === 'string' ? await service.toolCallRun(body.toolCallId) : null;
1204
1237
  if (runId === null) {
1205
- ctx.response.notFound({ error: 'Unknown tool call.' });
1238
+ ctx.response.notFound({ message: 'Unknown tool call.' });
1206
1239
  return null;
1207
1240
  }
1208
1241
  return runId;
@@ -1226,11 +1259,11 @@ export default class AgentProvider {
1226
1259
  }
1227
1260
  else {
1228
1261
  if (owner === null) {
1229
- ctx.response.notFound({ error: 'Unknown run.' });
1262
+ ctx.response.notFound({ message: 'Unknown run.' });
1230
1263
  return false;
1231
1264
  }
1232
1265
  if (!(await service.mayDecide(actor, { toolCallId, approver, requesterRef: owner }))) {
1233
- ctx.response.forbidden({ error: `This approval is for ${approver}.` });
1266
+ ctx.response.forbidden({ message: `This approval is for ${approver}.` });
1234
1267
  return false;
1235
1268
  }
1236
1269
  }
@@ -1240,7 +1273,9 @@ export default class AgentProvider {
1240
1273
  approval.expiresAt !== null &&
1241
1274
  Date.parse(approval.expiresAt) <= Date.now()));
1242
1275
  if (lapsed) {
1243
- ctx.response.gone({ error: `The approval request for tool call ${toolCallId} has expired.` });
1276
+ ctx.response.gone({
1277
+ message: `The approval request for tool call ${toolCallId} has expired.`,
1278
+ });
1244
1279
  return false;
1245
1280
  }
1246
1281
  return true;
@@ -1249,7 +1284,7 @@ export default class AgentProvider {
1249
1284
  if (!(error instanceof HumanReplyMismatchError)) {
1250
1285
  throw error;
1251
1286
  }
1252
- ctx.response.conflict({ error: error.message });
1287
+ ctx.response.conflict({ message: error.message });
1253
1288
  }
1254
1289
  /**
1255
1290
  * Pipe the run's live token stream to the client as SSE: `event: meta` (runId/threadId), the run's
@@ -1257,12 +1292,11 @@ export default class AgentProvider {
1257
1292
  * raw Node response directly (Adonis has no SSE helper) and ends only on stream completion — the
1258
1293
  * sink closes on run finish, not on suspend.
1259
1294
  *
1260
- * The sink carries typed `StreamFrame`s; `protocol` picks the envelope they are written in. Under
1261
- * `'agent'` each frame becomes the `AgentStreamEvent`s it stands for (`AgentSseEncoder`) and a
1262
- * failure ends the stream with `event: error` instead of `done`; under `'legacy'` `frameToSse`
1263
- * writes the original `data: {"delta":...}` / `event: component` envelope byte-for-byte.
1295
+ * The sink carries typed `StreamFrame`s; each becomes the `AgentStreamEvent`s it stands for
1296
+ * (`AgentSseEncoder`, the protocol shared with `@dudousxd/nestjs-agent`), and a failure ends the
1297
+ * stream with `event: error` instead of `done`.
1264
1298
  */
1265
- async #pipe(ctx, service, protocol, runId, threadId, after = 0) {
1299
+ async #pipe(ctx, service, runId, threadId, after = 0) {
1266
1300
  const raw = ctx.response.response;
1267
1301
  const headers = {
1268
1302
  'Content-Type': 'text/event-stream',
@@ -1281,23 +1315,13 @@ export default class AgentProvider {
1281
1315
  }
1282
1316
  raw.writeHead(200, headers);
1283
1317
  raw.write(`event: meta\ndata: ${JSON.stringify({ runId, threadId })}\n\n`);
1284
- if (protocol === 'agent') {
1285
- const encoder = new AgentSseEncoder(after);
1286
- for await (const frame of service.subscribe(runId)) {
1287
- const text = encoder.encode(frame);
1288
- if (text.length > 0)
1289
- raw.write(text);
1290
- }
1291
- raw.write(encoder.close());
1292
- raw.end();
1293
- return;
1294
- }
1318
+ const encoder = new AgentSseEncoder(after);
1295
1319
  for await (const frame of service.subscribe(runId)) {
1296
- const text = frameToSse(frame);
1320
+ const text = encoder.encode(frame);
1297
1321
  if (text.length > 0)
1298
1322
  raw.write(text);
1299
1323
  }
1300
- raw.write('event: done\ndata: {}\n\n');
1324
+ raw.write(encoder.close());
1301
1325
  raw.end();
1302
1326
  }
1303
1327
  }