@adonis-agora/agent 0.48.0 → 0.50.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 +29 -0
- package/dist/providers/agent_provider.d.ts.map +1 -1
- package/dist/providers/agent_provider.js +225 -46
- 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 +18 -0
- package/dist/src/agent-loop.d.ts.map +1 -1
- package/dist/src/agent-loop.js +52 -8
- package/dist/src/agent-loop.js.map +1 -1
- package/dist/src/agent-service.d.ts +129 -9
- package/dist/src/agent-service.d.ts.map +1 -1
- package/dist/src/agent-service.js +284 -8
- package/dist/src/agent-service.js.map +1 -1
- package/dist/src/chat-queue-service.d.ts +153 -0
- package/dist/src/chat-queue-service.d.ts.map +1 -0
- package/dist/src/chat-queue-service.js +360 -0
- package/dist/src/chat-queue-service.js.map +1 -0
- package/dist/src/client/chat-client.d.ts +52 -0
- package/dist/src/client/chat-client.d.ts.map +1 -1
- package/dist/src/client/chat-client.js +51 -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 +5 -0
- package/dist/src/client/sse.d.ts.map +1 -1
- package/dist/src/client/sse.js +5 -1
- package/dist/src/client/sse.js.map +1 -1
- package/dist/src/define_config.d.ts +3 -1
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/durable/agent-run-context.d.ts +6 -0
- package/dist/src/durable/agent-run-context.d.ts.map +1 -1
- package/dist/src/durable/agent-run-context.js.map +1 -1
- package/dist/src/durable/agent-run-workflow.d.ts.map +1 -1
- package/dist/src/durable/agent-run-workflow.js +94 -6
- package/dist/src/durable/agent-run-workflow.js.map +1 -1
- package/dist/src/durable/durable-agent-runner.d.ts +19 -3
- package/dist/src/durable/durable-agent-runner.d.ts.map +1 -1
- package/dist/src/durable/durable-agent-runner.js +62 -3
- package/dist/src/durable/durable-agent-runner.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/index.d.ts +8 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/react/use-agent-chat.d.ts.map +1 -1
- package/dist/src/react/use-agent-chat.js +15 -1
- package/dist/src/react/use-agent-chat.js.map +1 -1
- package/dist/src/runners/inline-agent-runner.d.ts +30 -6
- package/dist/src/runners/inline-agent-runner.d.ts.map +1 -1
- package/dist/src/runners/inline-agent-runner.js +117 -21
- package/dist/src/runners/inline-agent-runner.js.map +1 -1
- package/dist/src/spi/agent-runner.d.ts +20 -1
- package/dist/src/spi/agent-runner.d.ts.map +1 -1
- package/dist/src/spi/chat-queue.d.ts +133 -0
- package/dist/src/spi/chat-queue.d.ts.map +1 -0
- package/dist/src/spi/chat-queue.js +48 -0
- package/dist/src/spi/chat-queue.js.map +1 -0
- 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/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/stores/lucid-schema.d.ts +8 -6
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +34 -7
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +23 -1
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +192 -0
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/stream-events.d.ts +17 -0
- package/dist/src/stream-events.d.ts.map +1 -1
- package/dist/src/testing/chat-queue-store-contract.d.ts +26 -0
- package/dist/src/testing/chat-queue-store-contract.d.ts.map +1 -0
- package/dist/src/testing/chat-queue-store-contract.js +162 -0
- package/dist/src/testing/chat-queue-store-contract.js.map +1 -0
- package/dist/src/testing/in-memory-store.d.ts +20 -1
- package/dist/src/testing/in-memory-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-store.js +125 -0
- package/dist/src/testing/in-memory-store.js.map +1 -1
- package/dist/src/testing/index.d.ts +1 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- 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 +25 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/stubs/database/migrations/create_agent_tables.stub +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @adonis-agora/agent
|
|
2
2
|
|
|
3
|
+
## 0.50.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#224](https://github.com/DavideCarvalho/adonis-agora-agent/pull/224) [`3ed6b2d`](https://github.com/DavideCarvalho/adonis-agora-agent/commit/3ed6b2dbcdceef26ea7c262edec5cacffb5d6c05) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Message queue — sending while a turn is still running, ported from `@dudousxd/nestjs-agent` (its `docs/stream-protocol.md`, *Message queue*), so `chat.queue` in `@dudousxd/nestjs-agent-react` works against an Adonis server.
|
|
8
|
+
|
|
9
|
+
- **A send on a busy thread is queued, not run concurrently (BREAKING behaviour).** `POST <path>/chat` on a thread whose turn is still running now answers `202 { queued: true, threadId, messageId, position, queue, runId?, interrupting? }` (JSON, no stream) and the message — stored server-side, per thread, FIFO — starts when that turn settles, under its own `messageId` as run id. `mode: 'auto' | 'queue' | 'interrupt'` (`interrupt: true` is the shorthand): `'queue'` always answers `202`, `'interrupt'` queues at the head and cancels the running turn. `regenerate` on a busy thread is `409 run_active`. Before, such a send started a second, concurrent turn on the same thread.
|
|
10
|
+
- **Drain policy.** Completed → the head starts; failed → the queue pauses (`run_failed`); Stop → pauses (`cancelled`); an interrupt's cancel → its message starts; the head's actor over quota → pauses (`quota_exceeded`). The decision is the settling stream's last event before its terminal: `{ kind: 'queue', queue, started?: { messageId, runId } }`. Every queue change while a turn holds the thread is a `queue` event too.
|
|
11
|
+
- **Routes.** `GET` / `DELETE <path>/threads/:id/queue`, `POST <path>/threads/:id/queue/resume`, `PATCH` / `DELETE <path>/queue/:messageId` (owner-scoped; `404` unknown, `403` another actor's, `410` already started or removed, `501` on a store without a queue). `GET <path>/threads/:id` carries `queue`.
|
|
12
|
+
- **One turn per thread.** Admission is a compare-and-set on the thread's active run. New SPI `ChatQueueStore` (`enqueueMessage`, `listQueue`, …, `claimActiveStream`, `releaseActiveStream`, probed by `isChatQueueStore`), implemented by the Lucid store (new table `agent_queued_message`, new column `agent_thread.queue_pause` — both from `createAgentTables`; with `autoCreateTables: false`, add a migration that calls it again) and the in-memory store. `CHAT_QUEUE_STORE_CONTRACT` (`./testing`) checks a store of your own. A store without it keeps the old behaviour.
|
|
13
|
+
- **`AgentService.send()`** queues; **`AgentService.chat()` stays start-or-refuse** and now throws `ChatQueueError` (`409 run_active`) on a busy thread. `ChatQueueService` (drain/kick/launch/publish) is exported; `getQueue`, `updateQueuedMessage`, `removeQueuedMessage`, `clearQueue`, `resumeQueue` on the service.
|
|
14
|
+
- **Runners.** `AgentRunner.start(input, { runId? })` and optional `isRunActive(runId)`. Inline: drains before the stream ends; a cancelled loop now actually stops at its next safe point (new `AgentLoopHooks.cancelled`, `RunCancelledError`) instead of running on in the background, and a parked wait is unwound. Durable: the handoff is journaled (`queue:completed` / `queue:failed` behind `ctx.patched('agent:chat-queue')`, then `ctx.startChild` under the message id — a run in flight at upgrade replays the history it has); `cancel` now frees the thread, hands it to the queue and ends the stream with a `cancelled` frame; a thread turn stopped from outside no longer writes its late answer.
|
|
15
|
+
- **Client.** `@adonis-agora/agent/client` `send` returns `{ queued }` for a `202` (`AgentChatQueued`), takes `mode`; `./react`'s hook follows a queued send that started at once.
|
|
16
|
+
|
|
17
|
+
## 0.49.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- [#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:
|
|
22
|
+
|
|
23
|
+
- **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).')`.
|
|
24
|
+
- **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).
|
|
25
|
+
- **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.
|
|
26
|
+
- **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.
|
|
27
|
+
- **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`.
|
|
28
|
+
- **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.
|
|
29
|
+
- **`GET tools?agent=*`** (`ALL_AGENTS`) answers the union across every agent, each tool once, under the same gates; an unknown name is still `404`.
|
|
30
|
+
- **Frame ids.** `SseEvent.id` (the client parser now keeps the `id:` line). Ids stay strictly increasing within a run; readers must tolerate gaps.
|
|
31
|
+
|
|
3
32
|
## 0.48.0
|
|
4
33
|
|
|
5
34
|
### 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;AAiL/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,kBA+JT;IAEK,QAAQ,kBASb;CAmyCF"}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
-
import { AgentDepsFactory, AgentGenui, AgentRegistry, AgentService, AgentSseEncoder, AnonymousActorResolver, AttachmentRefusedError, DefaultToolAuthorizer, discoverTools, evaluateGovernanceGate, evaluateOwnership, 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, ChatQueueError, ChatQueueService, 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
|
+
/** A send's `mode`, or `null` when it names none of the three. `interrupt: true` is `'interrupt'`. */
|
|
6
|
+
function sendMode(body) {
|
|
7
|
+
if (body.mode === undefined) {
|
|
8
|
+
return body.interrupt === true ? 'interrupt' : 'auto';
|
|
9
|
+
}
|
|
10
|
+
return body.mode === 'auto' || body.mode === 'queue' || body.mode === 'interrupt'
|
|
11
|
+
? body.mode
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
5
14
|
/** The catalog a model provider carries (`aiSdkModels(…).catalog`), if it carries one. */
|
|
6
15
|
function catalogOf(model) {
|
|
7
16
|
const catalog = model?.catalog;
|
|
@@ -174,7 +183,7 @@ export default class AgentProvider {
|
|
|
174
183
|
? undefined
|
|
175
184
|
: isQuotaProvider(config.quota)
|
|
176
185
|
? config.quota
|
|
177
|
-
: new LedgerQuotaProvider(store, undefined, config.quota.limits);
|
|
186
|
+
: new LedgerQuotaProvider(store, undefined, config.quota.limits, config.quota.warnAt !== undefined ? { warnAt: config.quota.warnAt } : {});
|
|
178
187
|
const pricingStore = await this.#resolvePricing(config);
|
|
179
188
|
// Governance read-model is resolved after pricing so the Lucid read-model prices its rollups
|
|
180
189
|
// against the same live prices the loop's cost fold uses.
|
|
@@ -227,11 +236,20 @@ export default class AgentProvider {
|
|
|
227
236
|
});
|
|
228
237
|
// `durable: true` runs each turn as a replay-safe `@adonis-agora/durable` workflow; it degrades
|
|
229
238
|
// gracefully to the in-process runner when the durable peer isn't installed/configured.
|
|
239
|
+
// The thread message queue: a send on a thread whose turn is still running waits here and
|
|
240
|
+
// starts when that turn settles. One instance, shared by the service (which queues) and the
|
|
241
|
+
// runner (which drains); inert on a store that is not a `ChatQueueStore`.
|
|
242
|
+
const queue = new ChatQueueService(store, sink, {
|
|
243
|
+
...(quotaProvider !== undefined ? { quota: quotaProvider } : {}),
|
|
244
|
+
...(attachmentStaging !== undefined ? { attachments: attachmentStaging } : {}),
|
|
245
|
+
personaFor: (agentName) => service.resolvePersona(agentName),
|
|
246
|
+
});
|
|
230
247
|
const runner = config.durable === true
|
|
231
|
-
? ((await this.#resolveDurableRunner(factory, store)) ??
|
|
232
|
-
new InlineAgentRunner(factory, store))
|
|
233
|
-
: new InlineAgentRunner(factory, store);
|
|
248
|
+
? ((await this.#resolveDurableRunner(factory, store, queue, sink)) ??
|
|
249
|
+
new InlineAgentRunner(factory, store, queue))
|
|
250
|
+
: new InlineAgentRunner(factory, store, queue);
|
|
234
251
|
const service = new AgentService(runner, store, factory, {
|
|
252
|
+
queue,
|
|
235
253
|
// No `models` → the catalog the model provider carries (`aiSdkModels`), else none.
|
|
236
254
|
...(config.models !== undefined
|
|
237
255
|
? { models: toModelCatalog(config.models) }
|
|
@@ -379,14 +397,14 @@ export default class AgentProvider {
|
|
|
379
397
|
* unresolvable engine, or any wiring error logs a warning and degrades to the in-process runner
|
|
380
398
|
* rather than breaking boot.
|
|
381
399
|
*/
|
|
382
|
-
async #resolveDurableRunner(factory, store) {
|
|
400
|
+
async #resolveDurableRunner(factory, store, queue, sink) {
|
|
383
401
|
try {
|
|
384
402
|
const durable = await import('@adonis-agora/durable');
|
|
385
403
|
const engine = await this.app.container.make(durable.WorkflowEngine);
|
|
386
404
|
const { DurableAgentRunner, registerAgentWorkflow, setDurableAgentContext } = await import('../src/durable/index.js');
|
|
387
|
-
setDurableAgentContext({ factory, store });
|
|
405
|
+
setDurableAgentContext({ factory, store, queue });
|
|
388
406
|
registerAgentWorkflow(engine);
|
|
389
|
-
return new DurableAgentRunner(engine, store);
|
|
407
|
+
return new DurableAgentRunner(engine, store, queue, sink);
|
|
390
408
|
}
|
|
391
409
|
catch (error) {
|
|
392
410
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -441,15 +459,29 @@ export default class AgentProvider {
|
|
|
441
459
|
if (!(await this.#assertOwner(ctx, actor, owner, 'thread', governanceAuthorize)))
|
|
442
460
|
return;
|
|
443
461
|
}
|
|
462
|
+
const regenerate = body.regenerate === true;
|
|
463
|
+
if (regenerate && body.threadId === undefined) {
|
|
464
|
+
return ctx.response.badRequest({
|
|
465
|
+
message: 'regenerate requires an existing threadId',
|
|
466
|
+
code: 'thread_required',
|
|
467
|
+
});
|
|
468
|
+
}
|
|
444
469
|
const refs = attachmentRefs(body.attachments);
|
|
445
470
|
if (typeof refs === 'string') {
|
|
446
|
-
return ctx.response.badRequest({
|
|
471
|
+
return ctx.response.badRequest({ message: refs });
|
|
472
|
+
}
|
|
473
|
+
const mode = sendMode(body);
|
|
474
|
+
if (mode === null) {
|
|
475
|
+
return ctx.response.badRequest({ message: "mode must be 'auto', 'queue' or 'interrupt'" });
|
|
447
476
|
}
|
|
448
477
|
let started;
|
|
449
478
|
try {
|
|
450
|
-
started = await service.
|
|
479
|
+
started = await service.send({
|
|
451
480
|
actor,
|
|
452
|
-
|
|
481
|
+
mode,
|
|
482
|
+
// A regenerate answers the stored user message; whatever `message` says is ignored.
|
|
483
|
+
message: regenerate ? '' : body.message,
|
|
484
|
+
...(regenerate ? { regenerate: true } : {}),
|
|
453
485
|
...(typeof body.model === 'string' && body.model.length > 0 ? { model: body.model } : {}),
|
|
454
486
|
...(body.threadId !== undefined ? { threadId: body.threadId } : {}),
|
|
455
487
|
...(body.agent !== undefined ? { agentName: body.agent } : {}),
|
|
@@ -460,10 +492,16 @@ export default class AgentProvider {
|
|
|
460
492
|
}
|
|
461
493
|
catch (error) {
|
|
462
494
|
if (error instanceof AttachmentRefusedError) {
|
|
463
|
-
return ctx.response.status(error.status).json({
|
|
495
|
+
return ctx.response.status(error.status).json({ message: error.message });
|
|
464
496
|
}
|
|
465
497
|
if (error instanceof ModelNotAllowedError) {
|
|
466
|
-
return ctx.response.badRequest({
|
|
498
|
+
return ctx.response.badRequest({ message: error.message, code: 'model_not_allowed' });
|
|
499
|
+
}
|
|
500
|
+
if (error instanceof RegenerateNeedsThreadError) {
|
|
501
|
+
return ctx.response.badRequest({ message: error.message, code: 'thread_required' });
|
|
502
|
+
}
|
|
503
|
+
if (error instanceof ChatQueueError) {
|
|
504
|
+
return this.#refuseQueue(ctx, error);
|
|
467
505
|
}
|
|
468
506
|
if (error instanceof QuotaBlockedError) {
|
|
469
507
|
return ctx.response.status(429).json({
|
|
@@ -474,6 +512,11 @@ export default class AgentProvider {
|
|
|
474
512
|
}
|
|
475
513
|
throw error;
|
|
476
514
|
}
|
|
515
|
+
if (started.queued === true) {
|
|
516
|
+
// The thread already has a turn running: the message waits in its queue (`202`, JSON — no
|
|
517
|
+
// stream). `runId` is there when it started straight away; attach to it with `GET …/stream`.
|
|
518
|
+
return ctx.response.status(202).json(started);
|
|
519
|
+
}
|
|
477
520
|
await this.#pipe(ctx, service, started.runId, started.threadId);
|
|
478
521
|
});
|
|
479
522
|
// 2. GET /agent/chat/:runId/stream — re-attach SSE. Authenticated: the actor resolver reads the
|
|
@@ -490,7 +533,7 @@ export default class AgentProvider {
|
|
|
490
533
|
// Nothing buffered under the run — it ended while the client was away, or its buffer went with
|
|
491
534
|
// a restarted process. A 404 reads as "nothing to resume"; subscribing would wait forever.
|
|
492
535
|
if (!(await service.hasStream(runId))) {
|
|
493
|
-
return ctx.response.notFound({
|
|
536
|
+
return ctx.response.notFound({ message: 'Nothing is streaming under that run.' });
|
|
494
537
|
}
|
|
495
538
|
// `?after=<seq>` (or the `Last-Event-ID` a browser EventSource sends on its own) skips the
|
|
496
539
|
// events a reconnecting client already has; `after` wins when both are present.
|
|
@@ -524,7 +567,7 @@ export default class AgentProvider {
|
|
|
524
567
|
const via = decisionVia(body.via);
|
|
525
568
|
if (remember === null || via === null) {
|
|
526
569
|
return ctx.response.badRequest({
|
|
527
|
-
|
|
570
|
+
message: `remember must be a boolean and via a string of 1-${MAX_VIA_LENGTH} characters`,
|
|
528
571
|
});
|
|
529
572
|
}
|
|
530
573
|
const runId = await this.#runOfToolCall(ctx, service, body);
|
|
@@ -544,7 +587,7 @@ export default class AgentProvider {
|
|
|
544
587
|
const via = decisionVia(body.via);
|
|
545
588
|
if (via === null) {
|
|
546
589
|
return ctx.response.badRequest({
|
|
547
|
-
|
|
590
|
+
message: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
|
|
548
591
|
});
|
|
549
592
|
}
|
|
550
593
|
const runId = await this.#runOfToolCall(ctx, service, body);
|
|
@@ -563,6 +606,12 @@ export default class AgentProvider {
|
|
|
563
606
|
if (actor === null)
|
|
564
607
|
return;
|
|
565
608
|
const body = (ctx.request.body() ?? {});
|
|
609
|
+
const via = decisionVia(body.via);
|
|
610
|
+
if (via === null) {
|
|
611
|
+
return ctx.response.badRequest({
|
|
612
|
+
message: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
566
615
|
const runId = await this.#runOfToolCall(ctx, service, body);
|
|
567
616
|
if (runId === null)
|
|
568
617
|
return;
|
|
@@ -573,7 +622,7 @@ export default class AgentProvider {
|
|
|
573
622
|
// the person who typed it hears why — rather than signalled and quietly dropped by the loop.
|
|
574
623
|
const problem = await service.answerProblem(body.toolCallId, body.answers ?? {});
|
|
575
624
|
if (problem !== null) {
|
|
576
|
-
return ctx.response.badRequest({
|
|
625
|
+
return ctx.response.badRequest({ message: problem });
|
|
577
626
|
}
|
|
578
627
|
try {
|
|
579
628
|
await service.answer({
|
|
@@ -581,6 +630,7 @@ export default class AgentProvider {
|
|
|
581
630
|
toolCallId: body.toolCallId,
|
|
582
631
|
answers: body.answers ?? {},
|
|
583
632
|
answeredByRef: actor.id,
|
|
633
|
+
answeredVia: via,
|
|
584
634
|
});
|
|
585
635
|
}
|
|
586
636
|
catch (error) {
|
|
@@ -595,6 +645,12 @@ export default class AgentProvider {
|
|
|
595
645
|
if (actor === null)
|
|
596
646
|
return;
|
|
597
647
|
const body = (ctx.request.body() ?? {});
|
|
648
|
+
const via = decisionVia(body.via);
|
|
649
|
+
if (via === null) {
|
|
650
|
+
return ctx.response.badRequest({
|
|
651
|
+
message: `via must be a string of 1-${MAX_VIA_LENGTH} characters`,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
598
654
|
const runId = await this.#runOfToolCall(ctx, service, body);
|
|
599
655
|
if (runId === null)
|
|
600
656
|
return;
|
|
@@ -606,6 +662,7 @@ export default class AgentProvider {
|
|
|
606
662
|
runId,
|
|
607
663
|
toolCallId: body.toolCallId,
|
|
608
664
|
answeredByRef: actor.id,
|
|
665
|
+
answeredVia: via,
|
|
609
666
|
});
|
|
610
667
|
}
|
|
611
668
|
catch (error) {
|
|
@@ -630,11 +687,11 @@ export default class AgentProvider {
|
|
|
630
687
|
const threadId = await service.threadOfMessage(messageId);
|
|
631
688
|
if (threadId === undefined) {
|
|
632
689
|
return ctx.response.status(501).json({
|
|
633
|
-
|
|
690
|
+
message: 'Message feedback requires an AgentStore that implements threadOfMessage() and setMessageFeedback().',
|
|
634
691
|
});
|
|
635
692
|
}
|
|
636
693
|
if (threadId === null) {
|
|
637
|
-
return ctx.response.notFound({
|
|
694
|
+
return ctx.response.notFound({ message: 'message not found' });
|
|
638
695
|
}
|
|
639
696
|
const owner = await service.threadOwner(threadId);
|
|
640
697
|
if (!(await this.#assertOwner(ctx, actor, owner, 'thread', undefined)))
|
|
@@ -645,7 +702,7 @@ export default class AgentProvider {
|
|
|
645
702
|
...(body.comment !== undefined ? { comment: body.comment } : {}),
|
|
646
703
|
});
|
|
647
704
|
if (!result.ok) {
|
|
648
|
-
return ctx.response.status(result.status).json({
|
|
705
|
+
return ctx.response.status(result.status).json({ message: result.error });
|
|
649
706
|
}
|
|
650
707
|
return ctx.response.json({ feedback: result.feedback });
|
|
651
708
|
});
|
|
@@ -663,16 +720,23 @@ export default class AgentProvider {
|
|
|
663
720
|
const actor = await this.#resolveActor(ctx, actorResolver);
|
|
664
721
|
if (actor === null)
|
|
665
722
|
return;
|
|
666
|
-
return ctx.response.json(service.listAgents());
|
|
723
|
+
return ctx.response.json(await service.listAgents(actor));
|
|
667
724
|
});
|
|
668
725
|
// 6b. GET /agent/tools?agent= — the tools this caller can reach through an agent, with how a chat
|
|
669
726
|
// surface should talk about each (`presentation`). The same list the model is offered; nothing the
|
|
670
727
|
// caller passes widens it — `agent` only narrows to that agent's allow-list, and an unknown name is
|
|
671
|
-
// a 404 rather than the widest answer (no allow-list at all).
|
|
728
|
+
// a 404 rather than the widest answer (no allow-list at all). `?agent=*` is the union across every
|
|
729
|
+
// agent: each tool this caller reaches through any of them, once, under the same gates.
|
|
672
730
|
router.get(p('tools'), async (ctx) => {
|
|
673
731
|
const agent = ctx.request.qs().agent;
|
|
732
|
+
if (agent === ALL_AGENTS) {
|
|
733
|
+
const actor = await this.#resolveActor(ctx, actorResolver);
|
|
734
|
+
if (actor === null)
|
|
735
|
+
return;
|
|
736
|
+
return ctx.response.json(await service.toolCatalogForAllAgents(actor));
|
|
737
|
+
}
|
|
674
738
|
if (agent !== undefined && agent !== defaultAgentName && agents.get(agent) === undefined) {
|
|
675
|
-
return ctx.response.notFound({
|
|
739
|
+
return ctx.response.notFound({ message: `No agent named "${agent}"` });
|
|
676
740
|
}
|
|
677
741
|
const actor = await this.#resolveActor(ctx, resolveActorResolver(actorResolver, agents.get(agent ?? defaultAgentName)));
|
|
678
742
|
if (actor === null)
|
|
@@ -712,20 +776,20 @@ export default class AgentProvider {
|
|
|
712
776
|
return;
|
|
713
777
|
const body = (ctx.request.body() ?? {});
|
|
714
778
|
if (body.model !== undefined && body.model !== null && typeof body.model !== 'string') {
|
|
715
|
-
return ctx.response.badRequest({
|
|
779
|
+
return ctx.response.badRequest({ message: 'model must be a string or null' });
|
|
716
780
|
}
|
|
717
781
|
if (body.model !== undefined) {
|
|
718
782
|
try {
|
|
719
783
|
const pinned = await service.setThreadModel(actor, threadId, body.model);
|
|
720
784
|
if (!pinned) {
|
|
721
785
|
return ctx.response.status(501).json({
|
|
722
|
-
|
|
786
|
+
message: "Pinning a thread's model requires an AgentStore that implements updateThread().",
|
|
723
787
|
});
|
|
724
788
|
}
|
|
725
789
|
}
|
|
726
790
|
catch (error) {
|
|
727
791
|
if (error instanceof ModelNotAllowedError) {
|
|
728
|
-
return ctx.response.badRequest({
|
|
792
|
+
return ctx.response.badRequest({ message: error.message });
|
|
729
793
|
}
|
|
730
794
|
throw error;
|
|
731
795
|
}
|
|
@@ -733,12 +797,68 @@ export default class AgentProvider {
|
|
|
733
797
|
if (body.title !== undefined) {
|
|
734
798
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
735
799
|
if (title.length === 0 || title.length > 200) {
|
|
736
|
-
return ctx.response.badRequest({
|
|
800
|
+
return ctx.response.badRequest({ message: 'title must be a string of 1-200 characters' });
|
|
737
801
|
}
|
|
738
802
|
await service.renameThread(threadId, title);
|
|
739
803
|
}
|
|
740
804
|
return ctx.response.json({ ok: true });
|
|
741
805
|
});
|
|
806
|
+
// 8d. The thread message queue — messages sent while a turn was running, waiting to run after
|
|
807
|
+
// it. Owner-scoped like the thread routes; each answers the queue (`{ items, paused }`) and
|
|
808
|
+
// publishes it into the stream of the run holding the thread. `501` on a store without a queue.
|
|
809
|
+
router.get(p('threads/:id/queue'), async (ctx) => {
|
|
810
|
+
const threadId = await this.#ownedThread(ctx, service, actorResolver, governanceAuthorize);
|
|
811
|
+
if (threadId === null)
|
|
812
|
+
return;
|
|
813
|
+
return this.#queueAnswer(ctx, () => service.getQueue(threadId));
|
|
814
|
+
});
|
|
815
|
+
router.delete(p('threads/:id/queue'), async (ctx) => {
|
|
816
|
+
const threadId = await this.#ownedThread(ctx, service, actorResolver, governanceAuthorize);
|
|
817
|
+
if (threadId === null)
|
|
818
|
+
return;
|
|
819
|
+
return this.#queueAnswer(ctx, () => service.clearQueue(threadId));
|
|
820
|
+
});
|
|
821
|
+
router.post(p('threads/:id/queue/resume'), async (ctx) => {
|
|
822
|
+
const threadId = await this.#ownedThread(ctx, service, actorResolver, governanceAuthorize);
|
|
823
|
+
if (threadId === null)
|
|
824
|
+
return;
|
|
825
|
+
return this.#queueAnswer(ctx, () => service.resumeQueue(threadId));
|
|
826
|
+
});
|
|
827
|
+
router.patch(p('queue/:messageId'), async (ctx) => {
|
|
828
|
+
const actor = await this.#resolveActor(ctx, actorResolver);
|
|
829
|
+
if (actor === null)
|
|
830
|
+
return;
|
|
831
|
+
const messageId = String(ctx.params.messageId);
|
|
832
|
+
if (!(await this.#ownsQueuedMessage(ctx, service, actor, messageId, governanceAuthorize)))
|
|
833
|
+
return;
|
|
834
|
+
const body = (ctx.request.body() ?? {});
|
|
835
|
+
if (body.message !== undefined && typeof body.message !== 'string') {
|
|
836
|
+
return ctx.response.badRequest({ message: 'message must be a string' });
|
|
837
|
+
}
|
|
838
|
+
if (body.position !== undefined && typeof body.position !== 'number') {
|
|
839
|
+
return ctx.response.badRequest({ message: 'position must be a non-negative integer' });
|
|
840
|
+
}
|
|
841
|
+
const refs = body.attachments === undefined || body.attachments === null
|
|
842
|
+
? body.attachments
|
|
843
|
+
: attachmentRefs(body.attachments);
|
|
844
|
+
if (typeof refs === 'string') {
|
|
845
|
+
return ctx.response.badRequest({ message: refs });
|
|
846
|
+
}
|
|
847
|
+
return this.#queueAnswer(ctx, () => service.updateQueuedMessage(actor, messageId, {
|
|
848
|
+
...(typeof body.message === 'string' ? { message: body.message } : {}),
|
|
849
|
+
...(refs !== undefined ? { attachments: refs } : {}),
|
|
850
|
+
...(typeof body.position === 'number' ? { position: body.position } : {}),
|
|
851
|
+
}));
|
|
852
|
+
});
|
|
853
|
+
router.delete(p('queue/:messageId'), async (ctx) => {
|
|
854
|
+
const actor = await this.#resolveActor(ctx, actorResolver);
|
|
855
|
+
if (actor === null)
|
|
856
|
+
return;
|
|
857
|
+
const messageId = String(ctx.params.messageId);
|
|
858
|
+
if (!(await this.#ownsQueuedMessage(ctx, service, actor, messageId, governanceAuthorize)))
|
|
859
|
+
return;
|
|
860
|
+
return this.#queueAnswer(ctx, () => service.removeQueuedMessage(messageId));
|
|
861
|
+
});
|
|
742
862
|
// 9. DELETE /agent/threads/:id. Authenticated + owner-scoped.
|
|
743
863
|
router.delete(p('threads/:id'), async (ctx) => {
|
|
744
864
|
const actor = await this.#resolveActor(ctx, actorResolver);
|
|
@@ -798,16 +918,16 @@ export default class AgentProvider {
|
|
|
798
918
|
if (actor === null)
|
|
799
919
|
return;
|
|
800
920
|
if (memory === undefined) {
|
|
801
|
-
return ctx.response.notFound({
|
|
921
|
+
return ctx.response.notFound({ message: 'No memory is configured in this deployment.' });
|
|
802
922
|
}
|
|
803
923
|
const id = String(ctx.params.id);
|
|
804
924
|
const entry = (await everyMemoryOf(actor)).entries.find((candidate) => candidate.id === id);
|
|
805
925
|
if (entry === undefined) {
|
|
806
|
-
return ctx.response.notFound({
|
|
926
|
+
return ctx.response.notFound({ message: `No memory with id "${id}".` });
|
|
807
927
|
}
|
|
808
928
|
const verdict = memoryForgetVerdict({ record: entry, actor });
|
|
809
929
|
if (!verdict.allowed) {
|
|
810
|
-
return ctx.response.forbidden({
|
|
930
|
+
return ctx.response.forbidden({ message: verdict.reason });
|
|
811
931
|
}
|
|
812
932
|
return ctx.response.json({
|
|
813
933
|
forgotten: await memory.provider.forget({ id: entry.id, ctx: { actor, threadId: '' } }),
|
|
@@ -856,19 +976,19 @@ export default class AgentProvider {
|
|
|
856
976
|
return;
|
|
857
977
|
const file = ctx.request.file('file');
|
|
858
978
|
if (file === null || file.tmpPath === undefined) {
|
|
859
|
-
return ctx.response.status(400).json({
|
|
979
|
+
return ctx.response.status(400).json({ message: 'multipart field "file" is required' });
|
|
860
980
|
}
|
|
861
981
|
const contentType = file.headers?.['content-type']?.split(';')[0]?.trim() ?? `${file.type}/${file.subtype}`;
|
|
862
982
|
if (!allowedContentTypes.includes(contentType)) {
|
|
863
983
|
return ctx.response.status(415).json({
|
|
864
|
-
|
|
984
|
+
message: `content type "${contentType}" is not allowed (allowed: ${allowedContentTypes.join(', ')})`,
|
|
865
985
|
});
|
|
866
986
|
}
|
|
867
987
|
const sizeBytes = file.size;
|
|
868
988
|
if (sizeBytes > maxBytes) {
|
|
869
989
|
return ctx.response
|
|
870
990
|
.status(413)
|
|
871
|
-
.json({
|
|
991
|
+
.json({ message: `file exceeds the ${maxBytes}-byte limit` });
|
|
872
992
|
}
|
|
873
993
|
const { readFile } = await import('node:fs/promises');
|
|
874
994
|
const data = await readFile(file.tmpPath);
|
|
@@ -985,7 +1105,7 @@ export default class AgentProvider {
|
|
|
985
1105
|
if (gov.threadDetail === undefined) {
|
|
986
1106
|
return ctx.response
|
|
987
1107
|
.status(501)
|
|
988
|
-
.json({
|
|
1108
|
+
.json({ message: 'this governance adapter does not support thread detail' });
|
|
989
1109
|
}
|
|
990
1110
|
const detail = await withActorLabel(await gov.threadDetail(String(ctx.params.id)), actorDirectory);
|
|
991
1111
|
if (detail === null)
|
|
@@ -1101,7 +1221,7 @@ export default class AgentProvider {
|
|
|
1101
1221
|
typeof body.inputPricePer1m !== 'number' ||
|
|
1102
1222
|
typeof body.outputPricePer1m !== 'number') {
|
|
1103
1223
|
return ctx.response.status(400).json({
|
|
1104
|
-
|
|
1224
|
+
message: 'body must be {modelId: string, inputPricePer1m: number, outputPricePer1m: number, cacheWritePricePer1m?: number, cacheReadPricePer1m?: number}',
|
|
1105
1225
|
});
|
|
1106
1226
|
}
|
|
1107
1227
|
await pricing.upsertModelPrice({
|
|
@@ -1130,7 +1250,7 @@ export default class AgentProvider {
|
|
|
1130
1250
|
try {
|
|
1131
1251
|
const actor = await actorResolver.resolve(ctx);
|
|
1132
1252
|
if (actor === null || actor === undefined) {
|
|
1133
|
-
ctx.response.status(401).json({
|
|
1253
|
+
ctx.response.status(401).json({ message: 'unauthorized', code: 'unauthorized' });
|
|
1134
1254
|
return null;
|
|
1135
1255
|
}
|
|
1136
1256
|
return actor;
|
|
@@ -1140,9 +1260,10 @@ export default class AgentProvider {
|
|
|
1140
1260
|
// in production, every request here is untrusted by definition, so the detail stays server-side
|
|
1141
1261
|
// (dev/test keeps it, matching `evaluateDashboardGate`/`evaluateGovernanceGate`'s `debug` knob).
|
|
1142
1262
|
const debug = !this.app.inProduction;
|
|
1143
|
-
ctx.response
|
|
1144
|
-
.
|
|
1145
|
-
|
|
1263
|
+
ctx.response.status(401).json({
|
|
1264
|
+
message: debug && error instanceof Error ? error.message : 'unauthorized',
|
|
1265
|
+
code: 'unauthorized',
|
|
1266
|
+
});
|
|
1146
1267
|
return null;
|
|
1147
1268
|
}
|
|
1148
1269
|
}
|
|
@@ -1158,7 +1279,7 @@ export default class AgentProvider {
|
|
|
1158
1279
|
return null;
|
|
1159
1280
|
const verdict = await evaluateGovernanceGate(actor, ctx, governanceAuthorize, !this.app.inProduction);
|
|
1160
1281
|
if (!verdict.ok) {
|
|
1161
|
-
ctx.response.status(verdict.status).json({
|
|
1282
|
+
ctx.response.status(verdict.status).json({ message: verdict.error });
|
|
1162
1283
|
return null;
|
|
1163
1284
|
}
|
|
1164
1285
|
return actor;
|
|
@@ -1180,7 +1301,7 @@ export default class AgentProvider {
|
|
|
1180
1301
|
const verdict = evaluateOwnership(actor.id, ownerRef, privileged);
|
|
1181
1302
|
if (!verdict.ok) {
|
|
1182
1303
|
const error = verdict.status === 404 ? `${kind} not found` : 'forbidden';
|
|
1183
|
-
ctx.response.status(verdict.status).json({ error });
|
|
1304
|
+
ctx.response.status(verdict.status).json({ message: error });
|
|
1184
1305
|
return false;
|
|
1185
1306
|
}
|
|
1186
1307
|
return true;
|
|
@@ -1201,7 +1322,7 @@ export default class AgentProvider {
|
|
|
1201
1322
|
return body.runId;
|
|
1202
1323
|
const runId = typeof body.toolCallId === 'string' ? await service.toolCallRun(body.toolCallId) : null;
|
|
1203
1324
|
if (runId === null) {
|
|
1204
|
-
ctx.response.notFound({
|
|
1325
|
+
ctx.response.notFound({ message: 'Unknown tool call.' });
|
|
1205
1326
|
return null;
|
|
1206
1327
|
}
|
|
1207
1328
|
return runId;
|
|
@@ -1225,11 +1346,11 @@ export default class AgentProvider {
|
|
|
1225
1346
|
}
|
|
1226
1347
|
else {
|
|
1227
1348
|
if (owner === null) {
|
|
1228
|
-
ctx.response.notFound({
|
|
1349
|
+
ctx.response.notFound({ message: 'Unknown run.' });
|
|
1229
1350
|
return false;
|
|
1230
1351
|
}
|
|
1231
1352
|
if (!(await service.mayDecide(actor, { toolCallId, approver, requesterRef: owner }))) {
|
|
1232
|
-
ctx.response.forbidden({
|
|
1353
|
+
ctx.response.forbidden({ message: `This approval is for ${approver}.` });
|
|
1233
1354
|
return false;
|
|
1234
1355
|
}
|
|
1235
1356
|
}
|
|
@@ -1239,16 +1360,74 @@ export default class AgentProvider {
|
|
|
1239
1360
|
approval.expiresAt !== null &&
|
|
1240
1361
|
Date.parse(approval.expiresAt) <= Date.now()));
|
|
1241
1362
|
if (lapsed) {
|
|
1242
|
-
ctx.response.gone({
|
|
1363
|
+
ctx.response.gone({
|
|
1364
|
+
message: `The approval request for tool call ${toolCallId} has expired.`,
|
|
1365
|
+
});
|
|
1243
1366
|
return false;
|
|
1244
1367
|
}
|
|
1245
1368
|
return true;
|
|
1246
1369
|
}
|
|
1370
|
+
/** A refused queue request: the error's own status, as `{ message, code? }`. */
|
|
1371
|
+
#refuseQueue(ctx, error) {
|
|
1372
|
+
ctx.response
|
|
1373
|
+
.status(error.status)
|
|
1374
|
+
.json({ message: error.message, ...(error.code !== undefined ? { code: error.code } : {}) });
|
|
1375
|
+
}
|
|
1376
|
+
/** Answer a queue route: the service's result, or the refusal it threw. */
|
|
1377
|
+
async #queueAnswer(ctx, work) {
|
|
1378
|
+
try {
|
|
1379
|
+
ctx.response.json(await work());
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
if (error instanceof ChatQueueError) {
|
|
1383
|
+
return this.#refuseQueue(ctx, error);
|
|
1384
|
+
}
|
|
1385
|
+
if (error instanceof AttachmentRefusedError) {
|
|
1386
|
+
ctx.response.status(error.status).json({ message: error.message });
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
throw error;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
/** The `:id` thread once the caller is shown to own it; `null` after replying 401/403/404. */
|
|
1393
|
+
async #ownedThread(ctx, service, actorResolver, governanceAuthorize) {
|
|
1394
|
+
const actor = await this.#resolveActor(ctx, actorResolver);
|
|
1395
|
+
if (actor === null)
|
|
1396
|
+
return null;
|
|
1397
|
+
const threadId = String(ctx.params.id);
|
|
1398
|
+
const owner = await service.threadOwner(threadId);
|
|
1399
|
+
return (await this.#assertOwner(ctx, actor, owner, 'thread', governanceAuthorize))
|
|
1400
|
+
? threadId
|
|
1401
|
+
: null;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Does `actor` own the thread a queued message waits on? Replies `404` for a message that does
|
|
1405
|
+
* not exist (or a store without a queue answers `501`), `403` for another actor's.
|
|
1406
|
+
*/
|
|
1407
|
+
async #ownsQueuedMessage(ctx, service, actor, messageId, governanceAuthorize) {
|
|
1408
|
+
let threadId;
|
|
1409
|
+
try {
|
|
1410
|
+
threadId = await service.queuedMessageThread(messageId);
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
if (error instanceof ChatQueueError) {
|
|
1414
|
+
this.#refuseQueue(ctx, error);
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
throw error;
|
|
1418
|
+
}
|
|
1419
|
+
if (threadId === null) {
|
|
1420
|
+
ctx.response.notFound({ message: `queued message ${messageId} not found` });
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
const owner = await service.threadOwner(threadId);
|
|
1424
|
+
return this.#assertOwner(ctx, actor, owner, 'thread', governanceAuthorize);
|
|
1425
|
+
}
|
|
1247
1426
|
#conflictOnMismatch(ctx, error) {
|
|
1248
1427
|
if (!(error instanceof HumanReplyMismatchError)) {
|
|
1249
1428
|
throw error;
|
|
1250
1429
|
}
|
|
1251
|
-
ctx.response.conflict({
|
|
1430
|
+
ctx.response.conflict({ message: error.message });
|
|
1252
1431
|
}
|
|
1253
1432
|
/**
|
|
1254
1433
|
* Pipe the run's live token stream to the client as SSE: `event: meta` (runId/threadId), the run's
|