@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.
- package/CHANGELOG.md +27 -0
- package/dist/providers/agent_provider.d.ts.map +1 -1
- package/dist/providers/agent_provider.js +84 -60
- package/dist/providers/agent_provider.js.map +1 -1
- package/dist/providers/dashboard_provider.js +1 -1
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/src/agent-loop.d.ts.map +1 -1
- package/dist/src/agent-loop.js +30 -8
- package/dist/src/agent-loop.js.map +1 -1
- package/dist/src/agent-service.d.ts +33 -9
- package/dist/src/agent-service.d.ts.map +1 -1
- package/dist/src/agent-service.js +58 -4
- package/dist/src/agent-service.js.map +1 -1
- package/dist/src/client/chat-client.d.ts +20 -0
- package/dist/src/client/chat-client.d.ts.map +1 -1
- package/dist/src/client/chat-client.js +38 -2
- package/dist/src/client/chat-client.js.map +1 -1
- package/dist/src/client/index.d.ts +1 -1
- package/dist/src/client/index.d.ts.map +1 -1
- package/dist/src/client/index.js +1 -1
- package/dist/src/client/index.js.map +1 -1
- package/dist/src/client/sse.d.ts +14 -10
- package/dist/src/client/sse.d.ts.map +1 -1
- package/dist/src/client/sse.js +20 -73
- package/dist/src/client/sse.js.map +1 -1
- package/dist/src/define_config.d.ts +3 -14
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/elicitation.d.ts +12 -0
- package/dist/src/elicitation.d.ts.map +1 -1
- package/dist/src/elicitation.js +9 -1
- package/dist/src/elicitation.js.map +1 -1
- package/dist/src/genui/index.d.ts +1 -1
- package/dist/src/genui/index.js +1 -1
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/redis-token-stream-sink.d.ts +2 -3
- package/dist/src/redis-token-stream-sink.d.ts.map +1 -1
- package/dist/src/redis-token-stream-sink.js +2 -3
- package/dist/src/redis-token-stream-sink.js.map +1 -1
- package/dist/src/spi/model-catalog.d.ts +13 -0
- package/dist/src/spi/model-catalog.d.ts.map +1 -1
- package/dist/src/spi/model-catalog.js.map +1 -1
- package/dist/src/spi/quota-provider.d.ts +30 -1
- package/dist/src/spi/quota-provider.d.ts.map +1 -1
- package/dist/src/spi/quota-provider.js +34 -1
- package/dist/src/spi/quota-provider.js.map +1 -1
- package/dist/src/spi/token-stream-sink.d.ts +2 -3
- package/dist/src/spi/token-stream-sink.d.ts.map +1 -1
- package/dist/src/spi/token-stream-sink.js.map +1 -1
- package/dist/src/spi/tool.d.ts +1 -2
- package/dist/src/spi/tool.d.ts.map +1 -1
- package/dist/src/sse.d.ts +0 -20
- package/dist/src/sse.d.ts.map +1 -1
- package/dist/src/sse.js +0 -27
- package/dist/src/sse.js.map +1 -1
- package/dist/src/stores/ledger-quota-provider.d.ts +10 -1
- package/dist/src/stores/ledger-quota-provider.d.ts.map +1 -1
- package/dist/src/stores/ledger-quota-provider.js +14 -3
- package/dist/src/stores/ledger-quota-provider.js.map +1 -1
- package/dist/src/stream-events.d.ts +2 -2
- package/dist/src/tool-presentation.d.ts +5 -0
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -1
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/types.d.ts +19 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- 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;
|
|
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,
|
|
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({
|
|
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
|
|
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({
|
|
472
|
+
return ctx.response.status(error.status).json({ message: error.message });
|
|
465
473
|
}
|
|
466
474
|
if (error instanceof ModelNotAllowedError) {
|
|
467
|
-
return ctx.response.badRequest({
|
|
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,
|
|
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({
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
.
|
|
1146
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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;
|
|
1261
|
-
* `
|
|
1262
|
-
*
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
1320
|
+
const text = encoder.encode(frame);
|
|
1297
1321
|
if (text.length > 0)
|
|
1298
1322
|
raw.write(text);
|
|
1299
1323
|
}
|
|
1300
|
-
raw.write(
|
|
1324
|
+
raw.write(encoder.close());
|
|
1301
1325
|
raw.end();
|
|
1302
1326
|
}
|
|
1303
1327
|
}
|