@dudousxd/nestjs-agent 1.27.1 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2ui/index.cjs +465 -0
- package/dist/a2ui/index.cjs.map +1 -0
- package/dist/a2ui/index.d.cts +9 -0
- package/dist/a2ui/index.d.ts +9 -0
- package/dist/a2ui/index.js +439 -0
- package/dist/a2ui/index.js.map +1 -0
- package/dist/{chat-queue.service-uD1vR8fs.d.ts → chat-queue.service-B-fVOJGf.d.ts} +3 -2
- package/dist/{chat-queue.service-Cu2nJpXz.d.cts → chat-queue.service-Bk7DCKGV.d.cts} +3 -2
- package/dist/durable/index.cjs +18 -2
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +1 -1
- package/dist/durable/index.d.ts +1 -1
- package/dist/durable/index.js +19 -3
- package/dist/durable/index.js.map +1 -1
- package/dist/genui/index.cjs +10 -2
- package/dist/genui/index.cjs.map +1 -1
- package/dist/genui/index.d.cts +9 -1
- package/dist/genui/index.d.ts +9 -1
- package/dist/genui/index.js +11 -3
- package/dist/genui/index.js.map +1 -1
- package/dist/index-C-Zo8rTL.d.cts +619 -0
- package/dist/index-CBO7egb7.d.ts +619 -0
- package/dist/index.cjs +354 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -562
- package/dist/index.d.ts +30 -562
- package/dist/index.js +347 -26
- package/dist/index.js.map +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { A2uiStreamOptions } from '@dudousxd/nestjs-agent-core/a2ui';
|
|
2
|
+
import { AgentAttachmentConfig, AttachmentStagingStore, AgentStore, Actor, ListActionProposals, ActionProposalView, ActionProposalMutationView, TextActionProposalVocabulary, AgentRunner, ModelCatalog, QuotaProvider, ActionApprovalMode, MessageAttachment, QuotaReport, ModelCatalogView, AgentHostContext, AttachmentRef, PageContext, ChatQueueState, HumanReply, ThreadSummary, ThreadDetail, UpdateThreadInput, MessageFeedbackValue, MessageFeedback, StagedAttachment, ActorResolver } from '@dudousxd/nestjs-agent-core';
|
|
3
|
+
import { Request, Response } from 'express';
|
|
4
|
+
import { UiCapabilities } from '@dudousxd/nestjs-agent-core/genui';
|
|
5
|
+
import { b as AgentModuleOptions, A as AgentDepsFactory, C as ChatQueueService, d as AgentProtocolAdapter } from './chat-queue.service-B-fVOJGf.js';
|
|
6
|
+
|
|
7
|
+
/** Default per-file size cap (20 MiB). */
|
|
8
|
+
declare const DEFAULT_MAX_ATTACHMENT_BYTES: number;
|
|
9
|
+
/** Default allowlist: what multimodal model providers commonly accept as native image/file parts. */
|
|
10
|
+
declare const DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES: readonly string[];
|
|
11
|
+
/** How many attachments one message may name. */
|
|
12
|
+
declare const MAX_ATTACHMENTS_PER_MESSAGE = 10;
|
|
13
|
+
/** The attachment rules in force — `AgentAttachmentConfig`, as `GET <base>/config` serves it. */
|
|
14
|
+
type AttachmentLimits = AgentAttachmentConfig;
|
|
15
|
+
/**
|
|
16
|
+
* One source for attachment limits: the staging store's own `describe()` when it declares them
|
|
17
|
+
* (`AgentMediaAttachmentsModule` does), else `AgentModule`'s `attachments` option, else the defaults.
|
|
18
|
+
*/
|
|
19
|
+
declare function attachmentLimits(options: Pick<AgentModuleOptions, 'attachments'> | undefined, staging: AttachmentStagingStore | undefined): AttachmentLimits;
|
|
20
|
+
|
|
21
|
+
declare class ActionProposalService {
|
|
22
|
+
private readonly store;
|
|
23
|
+
private readonly options;
|
|
24
|
+
constructor(store: AgentStore, options: Pick<AgentModuleOptions, 'approvalPolicy' | 'actionProposalText'>);
|
|
25
|
+
/** The words a text decision is made of, and what the agent answers — English unless configured. */
|
|
26
|
+
private readonly vocabulary;
|
|
27
|
+
private readonly replies;
|
|
28
|
+
private hasCapability;
|
|
29
|
+
private capability;
|
|
30
|
+
private scope;
|
|
31
|
+
private mayDecide;
|
|
32
|
+
/**
|
|
33
|
+
* One page of a thread's proposals this actor may see. A store without the proposal capability
|
|
34
|
+
* can only run blocking approvals, so a thread there has no proposals: an empty, final page rather
|
|
35
|
+
* than `501` — a client that lists by default (React's `useAgentChat`) needs no flag. Deciding one
|
|
36
|
+
* still answers `501`.
|
|
37
|
+
*/
|
|
38
|
+
listPage(threadId: string, actor: Actor, query?: Pick<ListActionProposals, 'after'>): Promise<{
|
|
39
|
+
items: ActionProposalView[];
|
|
40
|
+
next?: NonNullable<ListActionProposals['after']>;
|
|
41
|
+
}>;
|
|
42
|
+
list(threadId: string, actor: Actor): Promise<ActionProposalView[]>;
|
|
43
|
+
decide(threadId: string, proposalId: string, actor: Actor, command: {
|
|
44
|
+
decision: 'approved' | 'rejected';
|
|
45
|
+
remember?: boolean;
|
|
46
|
+
reason?: string;
|
|
47
|
+
via?: string;
|
|
48
|
+
}): Promise<ActionProposalMutationView>;
|
|
49
|
+
handleTextDecision(threadId: string, actor: Actor, text: string): Promise<{
|
|
50
|
+
status: "unmatched";
|
|
51
|
+
} | {
|
|
52
|
+
threadId: string;
|
|
53
|
+
proposalDecision: ActionProposalMutationView;
|
|
54
|
+
text: string;
|
|
55
|
+
} | {
|
|
56
|
+
threadId: string;
|
|
57
|
+
proposalDecision: {
|
|
58
|
+
status: "ambiguous";
|
|
59
|
+
proposalIds: string[];
|
|
60
|
+
};
|
|
61
|
+
text: string;
|
|
62
|
+
}>;
|
|
63
|
+
private textReceipt;
|
|
64
|
+
/** The configured words a text decision is made of (`actionProposalText.vocabulary`). */
|
|
65
|
+
textVocabulary(): TextActionProposalVocabulary;
|
|
66
|
+
/** The configured reply to a decision the store answered with `result`. */
|
|
67
|
+
reply(result: Pick<ActionProposalMutationView, 'status' | 'proposal'>, decision: 'approved' | 'rejected'): string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What a send does when its thread already has a turn running:
|
|
72
|
+
* - `'auto'` (default) — run now when the thread is idle, else wait in the thread's queue.
|
|
73
|
+
* - `'queue'` — always wait in the queue, behind whatever is already waiting (it still starts at
|
|
74
|
+
* once when nothing is running and nothing is ahead of it).
|
|
75
|
+
* - `'interrupt'` — cancel the running turn and run this one next, ahead of the queue.
|
|
76
|
+
*/
|
|
77
|
+
type ChatSendMode = 'auto' | 'queue' | 'interrupt';
|
|
78
|
+
/** A send that is waiting in its thread's queue instead of running. */
|
|
79
|
+
interface QueuedSend {
|
|
80
|
+
threadId: string;
|
|
81
|
+
queued: true;
|
|
82
|
+
/** The queued message's id — also the run id it will start under. */
|
|
83
|
+
messageId: string;
|
|
84
|
+
/** 0-based place in the queue at the time it was queued (`0` → next). */
|
|
85
|
+
position: number;
|
|
86
|
+
queue: ChatQueueState;
|
|
87
|
+
/** The turn it started under, when it started straight away (an idle thread). */
|
|
88
|
+
runId?: string;
|
|
89
|
+
/** The run an interrupt cancelled to make room for it. */
|
|
90
|
+
interrupting?: string;
|
|
91
|
+
}
|
|
92
|
+
/** What {@link AgentService.send} did: started a turn, or queued the message. */
|
|
93
|
+
type ChatSendResult = {
|
|
94
|
+
runId: string;
|
|
95
|
+
threadId: string;
|
|
96
|
+
queued?: undefined;
|
|
97
|
+
} | QueuedSend | {
|
|
98
|
+
threadId: string;
|
|
99
|
+
proposalDecision: unknown;
|
|
100
|
+
text: string;
|
|
101
|
+
queued?: undefined;
|
|
102
|
+
};
|
|
103
|
+
/** The code of the `400` a send with nothing to answer gets (also AG-UI's, for the same case). */
|
|
104
|
+
declare const NO_USER_MESSAGE_CODE = "no_user_message";
|
|
105
|
+
interface ChatParams {
|
|
106
|
+
uiCapabilities?: UiCapabilities;
|
|
107
|
+
actor: Actor;
|
|
108
|
+
message: string;
|
|
109
|
+
/** The host's own facts about this send — see `AgentRunInput.hostContext`. */
|
|
110
|
+
hostContext?: AgentHostContext;
|
|
111
|
+
/**
|
|
112
|
+
* The HOST already decided this actor may write this thread — a chat-app thread several people
|
|
113
|
+
* share, where anyone in it may continue the conversation. Skips the thread-ownership check of a
|
|
114
|
+
* send; never set it from a request's own claim.
|
|
115
|
+
*/
|
|
116
|
+
authorized?: boolean;
|
|
117
|
+
threadId?: string;
|
|
118
|
+
agentName?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Run this turn under one of the agent's personas (`@Agent({ personas })`), and pin it on the
|
|
121
|
+
* thread so later sends that name none keep it. Refused (400 `persona_not_found`) unless the agent
|
|
122
|
+
* declares it. Omitted → the thread's pinned persona, else the agent's `defaultPersona`, else none.
|
|
123
|
+
*/
|
|
124
|
+
personaId?: string;
|
|
125
|
+
/**
|
|
126
|
+
* Files attached to this message (image/PDF) for a vision-capable model, named by the `mediaId`
|
|
127
|
+
* the upload returned. Only the id is read: the url, content type and name are resolved from the
|
|
128
|
+
* bound {@link AttachmentStagingStore}, so nothing a caller says decides what the model fetches.
|
|
129
|
+
*/
|
|
130
|
+
attachments?: AttachmentRef[];
|
|
131
|
+
pageContext?: PageContext;
|
|
132
|
+
/** Re-run the last exchange instead of adding a new message. Requires an existing `threadId`. */
|
|
133
|
+
regenerate?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Run this turn on a catalog model instead of the thread's pinned one (or the provider default).
|
|
136
|
+
* Refused (400) unless the bound `ModelCatalog` lists it as available to this actor and agent.
|
|
137
|
+
*/
|
|
138
|
+
model?: string;
|
|
139
|
+
/**
|
|
140
|
+
* When creating a thread (no `threadId`), start it transient — a scratch conversation hidden from
|
|
141
|
+
* the thread list until the caller promotes it. Ignored when `threadId` is set.
|
|
142
|
+
*/
|
|
143
|
+
transient?: boolean;
|
|
144
|
+
/** What to do when the thread already has a turn running — see {@link ChatSendMode}. */
|
|
145
|
+
mode?: ChatSendMode;
|
|
146
|
+
/**
|
|
147
|
+
* When creating a thread (no `threadId`), ask the store to create it under this id — the id an
|
|
148
|
+
* AG-UI consumer named the conversation with. A store may ignore it (`CreateThreadInput.id`); the
|
|
149
|
+
* thread's real id is always the one on the result.
|
|
150
|
+
*/
|
|
151
|
+
newThreadId?: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A store that can answer a thread's default agent on its own, without materializing the thread.
|
|
155
|
+
*
|
|
156
|
+
* Probed STRUCTURALLY rather than declared on `AgentStore`: the projection is an optimization a
|
|
157
|
+
* store either offers or doesn't, and a store that predates it still answers correctly through
|
|
158
|
+
* `getThread`. Every adapter in this repo implements it.
|
|
159
|
+
*/
|
|
160
|
+
interface ThreadDefaultAgentReader {
|
|
161
|
+
defaultAgentForThread(threadId: string): Promise<string | null>;
|
|
162
|
+
}
|
|
163
|
+
/** Like {@link ThreadDefaultAgentReader}, for the thread's pinned model. */
|
|
164
|
+
interface ThreadModelReader {
|
|
165
|
+
modelForThread(threadId: string): Promise<string | null>;
|
|
166
|
+
}
|
|
167
|
+
/** The orchestration facade the controllers call. */
|
|
168
|
+
declare class AgentService {
|
|
169
|
+
private readonly runner;
|
|
170
|
+
private readonly store;
|
|
171
|
+
private readonly deps;
|
|
172
|
+
private readonly staging?;
|
|
173
|
+
private readonly models?;
|
|
174
|
+
private readonly quotaProvider?;
|
|
175
|
+
private readonly options?;
|
|
176
|
+
private readonly queue?;
|
|
177
|
+
private readonly proposals?;
|
|
178
|
+
constructor(runner: AgentRunner, store: AgentStore, deps: AgentDepsFactory, staging?: AttachmentStagingStore | undefined, models?: ModelCatalog | undefined, quotaProvider?: QuotaProvider | undefined, options?: Pick<AgentModuleOptions, "quota" | "actionApprovalMode" | "attachments"> | undefined, queue?: ChatQueueService | undefined, proposals?: ActionProposalService | undefined);
|
|
179
|
+
/**
|
|
180
|
+
* Decide one independent proposal through the scoped proposal service — what the native
|
|
181
|
+
* `action-proposals/:id/approve|reject` routes call — and the reply to show for it. `404` when
|
|
182
|
+
* proposals are not on, `403`/`404` when this actor may not decide it.
|
|
183
|
+
*/
|
|
184
|
+
decideActionProposal(actor: Actor, threadId: string, proposalId: string, command: {
|
|
185
|
+
decision: 'approved' | 'rejected';
|
|
186
|
+
remember?: boolean;
|
|
187
|
+
reason?: string;
|
|
188
|
+
via: string;
|
|
189
|
+
}): Promise<{
|
|
190
|
+
proposalDecision: ActionProposalMutationView;
|
|
191
|
+
text: string;
|
|
192
|
+
}>;
|
|
193
|
+
handleTextDecision(threadId: string, actor: Actor, text: string): Promise<{
|
|
194
|
+
status: "unmatched";
|
|
195
|
+
} | {
|
|
196
|
+
threadId: string;
|
|
197
|
+
proposalDecision: ActionProposalMutationView;
|
|
198
|
+
text: string;
|
|
199
|
+
} | {
|
|
200
|
+
threadId: string;
|
|
201
|
+
proposalDecision: {
|
|
202
|
+
status: "ambiguous";
|
|
203
|
+
proposalIds: string[];
|
|
204
|
+
};
|
|
205
|
+
text: string;
|
|
206
|
+
}>;
|
|
207
|
+
/** How `action` tools wait for their approval here — `AgentModule.forRoot({ actionApprovalMode })`. */
|
|
208
|
+
actionApprovalMode(): ActionApprovalMode;
|
|
209
|
+
/**
|
|
210
|
+
* The words a text decision is made of (`actionProposalText.vocabulary`, English by default) — what
|
|
211
|
+
* a surface without buttons tells the person to reply.
|
|
212
|
+
*/
|
|
213
|
+
actionProposalVocabulary(): TextActionProposalVocabulary;
|
|
214
|
+
/** The configured reply (`actionProposalText.replies`) to a decision the store answered with `result`. */
|
|
215
|
+
actionProposalReply(result: Pick<ActionProposalMutationView, 'status' | 'proposal'>, decision: 'approved' | 'rejected'): string;
|
|
216
|
+
/**
|
|
217
|
+
* The proposals of a thread this actor may see — the thread's own actor, or one the policy lets
|
|
218
|
+
* decide them; what `GET <base>/threads/:threadId/action-proposals` serves (its first page).
|
|
219
|
+
* Without independent approvals (the default blocking mode) there are none, so the read answers an
|
|
220
|
+
* empty list rather than refusing; deciding one still refuses.
|
|
221
|
+
*/
|
|
222
|
+
listActionProposals(actor: Actor, threadId: string): Promise<ActionProposalView[]>;
|
|
223
|
+
/**
|
|
224
|
+
* The attachment rules in force — what `GET <base>/config` serves and `POST <base>/attachments`
|
|
225
|
+
* enforces. `enabled: false` when no `AGENT_ATTACHMENT_STAGING` provider is bound.
|
|
226
|
+
*/
|
|
227
|
+
attachmentLimits(): AttachmentLimits;
|
|
228
|
+
/**
|
|
229
|
+
* Stage a file for a later turn, held to the same limits as `POST <base>/attachments` — for a
|
|
230
|
+
* surface that receives files itself (a messaging webhook). Refuses with `501` when attachments are
|
|
231
|
+
* off, `415` for a type outside the allowlist and `413` past the size cap; send the returned
|
|
232
|
+
* `mediaId` as `attachments: [{ mediaId }]`.
|
|
233
|
+
*/
|
|
234
|
+
stageAttachment(actor: Actor, file: {
|
|
235
|
+
data: Buffer;
|
|
236
|
+
contentType: string;
|
|
237
|
+
filename: string;
|
|
238
|
+
}): Promise<MessageAttachment>;
|
|
239
|
+
/** The queue service when the bound store can hold a queue, else `undefined`. */
|
|
240
|
+
private queueing;
|
|
241
|
+
/** The queue service, or a 501 naming what is missing. */
|
|
242
|
+
private requireQueue;
|
|
243
|
+
/**
|
|
244
|
+
* The actor's budget across windows (`GET <base>/quota`) — the bound {@link QuotaProvider}'s
|
|
245
|
+
* report. Without one (a service built outside the module), a day window from the ledger.
|
|
246
|
+
*/
|
|
247
|
+
quotaReport(actor: Actor): Promise<QuotaReport>;
|
|
248
|
+
/**
|
|
249
|
+
* Refuse a turn the actor's budget no longer covers — only when the host configured one
|
|
250
|
+
* (`quota`): the default report is informational.
|
|
251
|
+
*/
|
|
252
|
+
private assertWithinQuota;
|
|
253
|
+
/**
|
|
254
|
+
* The models `actor` may pick for `agent` (`GET <base>/models`). An empty catalog when none is
|
|
255
|
+
* bound, so a picker simply has nothing to offer.
|
|
256
|
+
*/
|
|
257
|
+
listModels(actor: Actor, agent?: string): Promise<ModelCatalogView>;
|
|
258
|
+
/**
|
|
259
|
+
* `model` if the catalog offers it to this actor and agent right now, else a 400 naming why. The
|
|
260
|
+
* one gate every model choice passes — a send's own, and a thread's pinned one at the moment it is
|
|
261
|
+
* pinned and again at every turn that runs on it (availability can change in between).
|
|
262
|
+
*/
|
|
263
|
+
private assertModelAllowed;
|
|
264
|
+
/**
|
|
265
|
+
* The model a turn runs on: the send's own (that turn only — it is never stored on the thread),
|
|
266
|
+
* else the thread's pinned one, else none. An agent the catalog locks to one model runs on it
|
|
267
|
+
* whatever was sent or pinned, and a send naming another model is refused.
|
|
268
|
+
*/
|
|
269
|
+
private resolveModel;
|
|
270
|
+
private threadModel;
|
|
271
|
+
/**
|
|
272
|
+
* Start a turn — for in-process callers that need a run id back. A send that would have to wait
|
|
273
|
+
* (its thread already has a turn running) is refused with `409` instead; {@link send} is the form
|
|
274
|
+
* that queues it.
|
|
275
|
+
*/
|
|
276
|
+
chat(params: ChatParams): Promise<{
|
|
277
|
+
runId: string;
|
|
278
|
+
threadId: string;
|
|
279
|
+
}>;
|
|
280
|
+
/**
|
|
281
|
+
* Send a message: start a turn, or — when its thread already has one running — queue the message
|
|
282
|
+
* to run after it (see {@link ChatSendMode}). What `POST <base>/chat` calls.
|
|
283
|
+
*/
|
|
284
|
+
send(claimed: ChatParams, sendOptions?: {
|
|
285
|
+
textDecisions?: boolean;
|
|
286
|
+
}): Promise<ChatSendResult>;
|
|
287
|
+
/**
|
|
288
|
+
* Start a turn the thread is already claimed for, under the claimed id. A runner that minted an
|
|
289
|
+
* id of its own anyway gets the thread re-pointed at it; one that fails to start frees the thread.
|
|
290
|
+
*/
|
|
291
|
+
private startClaimed;
|
|
292
|
+
/** Put a send in its thread's queue, then drain it if the thread turned out to be free. */
|
|
293
|
+
private enqueue;
|
|
294
|
+
/** `GET <base>/threads/:id/queue` — the thread's waiting messages and whether it drains. */
|
|
295
|
+
getQueue(actor: Actor, threadId: string): Promise<ChatQueueState>;
|
|
296
|
+
/**
|
|
297
|
+
* `PATCH <base>/queue/:messageId` — change a waiting message's text and/or attachments, and/or move
|
|
298
|
+
* it to `position` in the queue. Answers the thread's queue.
|
|
299
|
+
*/
|
|
300
|
+
updateQueuedMessage(actor: Actor, messageId: string, patch: {
|
|
301
|
+
message?: string;
|
|
302
|
+
attachments?: AttachmentRef[] | null;
|
|
303
|
+
position?: number;
|
|
304
|
+
}): Promise<ChatQueueState>;
|
|
305
|
+
/** `DELETE <base>/queue/:messageId` — drop a waiting message. Answers the thread's queue. */
|
|
306
|
+
removeQueuedMessage(actor: Actor, messageId: string): Promise<ChatQueueState>;
|
|
307
|
+
/**
|
|
308
|
+
* `POST <base>/queue/:messageId/interrupt` — run a message that is already waiting NOW: it moves
|
|
309
|
+
* to the head of its queue marked as an interrupt, any pause is lifted, and the running turn is
|
|
310
|
+
* cancelled (its settle starts this message — the same path a `mode: 'interrupt'` send takes).
|
|
311
|
+
* With nothing running, the message starts at once.
|
|
312
|
+
*
|
|
313
|
+
* One request, and the message never leaves the queue: a client that removed it and sent it again
|
|
314
|
+
* would lose it if the second call failed, and would run it twice if another tab's drain started
|
|
315
|
+
* it in between. Answers the queue, plus `interrupting` (the run that was cancelled) or `runId`
|
|
316
|
+
* (the run this message started).
|
|
317
|
+
*/
|
|
318
|
+
interruptQueuedMessage(actor: Actor, messageId: string): Promise<ChatQueueState & {
|
|
319
|
+
runId?: string;
|
|
320
|
+
interrupting?: string;
|
|
321
|
+
}>;
|
|
322
|
+
/** `DELETE <base>/threads/:id/queue` — drop every waiting message (and any pause). */
|
|
323
|
+
clearQueue(actor: Actor, threadId: string): Promise<ChatQueueState>;
|
|
324
|
+
/**
|
|
325
|
+
* `POST <base>/threads/:id/queue/resume` — lift a pause and start the head when nothing is
|
|
326
|
+
* running. Answers the queue, and the run the head started under, if it did.
|
|
327
|
+
*/
|
|
328
|
+
resumeQueue(actor: Actor, threadId: string): Promise<ChatQueueState & {
|
|
329
|
+
runId?: string;
|
|
330
|
+
}>;
|
|
331
|
+
/** The thread a queued message waits on, once the caller is shown to own it. */
|
|
332
|
+
private assertOwnsQueuedMessage;
|
|
333
|
+
/**
|
|
334
|
+
* Rebuild this turn's attachments from the staging store, keyed by the ids the caller claimed.
|
|
335
|
+
* The model provider fetches {@link MessageAttachment.url} verbatim, so that url is only ever
|
|
336
|
+
* allowed to come from the host's own store — a caller that could name it could point the server
|
|
337
|
+
* at anything it can reach (cloud metadata, internal services) and read the response back out of
|
|
338
|
+
* the model's answer.
|
|
339
|
+
*
|
|
340
|
+
* No store bound means no way to tell an id the actor owns from one they don't, so a turn simply
|
|
341
|
+
* cannot carry attachments: 501 rather than trusting the request.
|
|
342
|
+
*/
|
|
343
|
+
private resolveAttachments;
|
|
344
|
+
/**
|
|
345
|
+
* The run's live token stream, WITHOUT an ownership check — for in-process callers that are
|
|
346
|
+
* already authorized (the MCP bridge, a host's own job code), the same split as
|
|
347
|
+
* {@link signalToolCall} vs {@link approve}. Anything reachable from a request must go through
|
|
348
|
+
* {@link subscribeAs} instead: a runId is the only thing standing between a caller and another
|
|
349
|
+
* actor's answer, tool arguments and tool results.
|
|
350
|
+
*/
|
|
351
|
+
subscribe(runId: string): AsyncIterable<Uint8Array>;
|
|
352
|
+
/**
|
|
353
|
+
* {@link subscribe}, gated the way {@link cancel} is: only the actor whose thread is currently
|
|
354
|
+
* streaming this run may read it. A run that has already finished is reported missing rather than
|
|
355
|
+
* replayed — the runner clears the thread's active stream as the run ends, and that field is what
|
|
356
|
+
* ownership is derived from.
|
|
357
|
+
*/
|
|
358
|
+
subscribeAs(actor: Actor, runId: string): Promise<AsyncIterable<Uint8Array>>;
|
|
359
|
+
/**
|
|
360
|
+
* Approve a parked action call as `actor`. Who may is the call's recorded approver's business
|
|
361
|
+
* (see {@link assertMayDecide}); `remember` approves later calls of the same tool in the same
|
|
362
|
+
* thread, and `via` names the surface the decision came through (`'web'`, `'slack'`, …) — both
|
|
363
|
+
* persisted with the call.
|
|
364
|
+
*/
|
|
365
|
+
approve(actor: Actor, toolCallId: string, opts?: {
|
|
366
|
+
remember?: boolean;
|
|
367
|
+
via?: string;
|
|
368
|
+
}): Promise<void>;
|
|
369
|
+
reject(actor: Actor, toolCallId: string, reason?: string, opts?: {
|
|
370
|
+
via?: string;
|
|
371
|
+
}): Promise<void>;
|
|
372
|
+
/**
|
|
373
|
+
* Answer a parked question set. An omitted question takes the request's own pre-picked default —
|
|
374
|
+
* resolved by the LOOP against the request it already holds, so "the user just submitted" and
|
|
375
|
+
* "the user picked exactly the defaults" persist as the same answers, and no client has to
|
|
376
|
+
* re-send what it was shown.
|
|
377
|
+
*/
|
|
378
|
+
answer(actor: Actor, toolCallId: string, answers?: Record<string, string[]>, opts?: {
|
|
379
|
+
via?: string;
|
|
380
|
+
}): Promise<void>;
|
|
381
|
+
/**
|
|
382
|
+
* Decline to answer and let the agent proceed on its own assumptions. NOT the same as confirming
|
|
383
|
+
* them: the run records this as a rejection, so a reader auditing what the agent was told can
|
|
384
|
+
* tell a choice the user made from one they refused to make.
|
|
385
|
+
*/
|
|
386
|
+
skip(actor: Actor, toolCallId: string, opts?: {
|
|
387
|
+
via?: string;
|
|
388
|
+
}): Promise<void>;
|
|
389
|
+
/**
|
|
390
|
+
* The decision core behind approve/reject, WITHOUT an ownership check: resolves the run
|
|
391
|
+
* currently awaiting `toolCallId` and signals it. `approve`/`reject` above call this after their
|
|
392
|
+
* own `assertOwnsToolCall` gate; the console `AgentApprovalPort` adapter (bound by `AgentModule`)
|
|
393
|
+
* calls it directly — a console caller is already authorized by the dashboard's own guards, and
|
|
394
|
+
* re-deriving thread ownership here would reject a legitimate operator decision (an operator is
|
|
395
|
+
* not the thread's own actor).
|
|
396
|
+
*/
|
|
397
|
+
signalToolCall(toolCallId: string, reply: HumanReply): Promise<void>;
|
|
398
|
+
/**
|
|
399
|
+
* Refuse a decision addressed at a run that is over (`409 run_not_active`). The runtime would take
|
|
400
|
+
* the signal and buffer it for a run that never comes back — the person's "yes" is accepted, the
|
|
401
|
+
* card says so, and nothing runs. Settles the calls the dead run left awaiting a decision on the
|
|
402
|
+
* way out, so the card stops asking. A runner that cannot say whether a run is alive is taken at
|
|
403
|
+
* its word that it is, and so is one that fails to answer.
|
|
404
|
+
*/
|
|
405
|
+
private assertRunWaiting;
|
|
406
|
+
/** Who owns the thread, or `null` when there is no such (live) thread. */
|
|
407
|
+
threadOwner(threadId: string): Promise<string | null>;
|
|
408
|
+
/**
|
|
409
|
+
* May `actor` continue the run whose stream is `runId` — the AG-UI resume? Only the actor whose
|
|
410
|
+
* thread is streaming it (`403` otherwise), and only while it still is: a run that is over is
|
|
411
|
+
* waiting on no one (`409 run_not_active`), whatever an old interrupt id says.
|
|
412
|
+
*/
|
|
413
|
+
assertResumable(actor: Actor, runId: string): Promise<void>;
|
|
414
|
+
/**
|
|
415
|
+
* Everything {@link approve} / {@link reject} check before they signal — who may decide, and
|
|
416
|
+
* that the request has not lapsed — without signalling. For a caller that settles several
|
|
417
|
+
* decisions at once and must refuse all of them if one is refused (an AG-UI resume).
|
|
418
|
+
*/
|
|
419
|
+
checkDecision(actor: Actor, toolCallId: string): Promise<void>;
|
|
420
|
+
/** Everything {@link answer} checks before it signals, without signalling — see {@link checkDecision}. */
|
|
421
|
+
checkAnswer(actor: Actor, toolCallId: string, answers: Record<string, string[]>): Promise<void>;
|
|
422
|
+
cancel(actor: Actor, runId: string): Promise<void>;
|
|
423
|
+
listThreads(actorRef: string): Promise<ThreadSummary[]>;
|
|
424
|
+
getThread(actor: Actor, threadId: string): Promise<ThreadDetail | null>;
|
|
425
|
+
deleteThread(actor: Actor, threadId: string): Promise<void>;
|
|
426
|
+
forkThread(actor: Actor, threadId: string, fromMessageId: string): Promise<ThreadSummary>;
|
|
427
|
+
/** Kept for source compatibility — a thin wrapper over the more general `updateThread`. */
|
|
428
|
+
renameThread(actor: Actor, threadId: string, title: string): Promise<void>;
|
|
429
|
+
/**
|
|
430
|
+
* Rename a thread and/or set its default agent. Title-only patches work against ANY store (via
|
|
431
|
+
* the required `setTitle`); a `defaultAgent` change requires the bound store to implement the
|
|
432
|
+
* optional `updateThread` — 501 with a clear message when it doesn't, rather than silently
|
|
433
|
+
* dropping the field.
|
|
434
|
+
*/
|
|
435
|
+
updateThread(actor: Actor, threadId: string, patch: UpdateThreadInput): Promise<void>;
|
|
436
|
+
/**
|
|
437
|
+
* The persona this send runs under — see {@link AgentDepsFactory.resolvePersona}. The thread's
|
|
438
|
+
* pinned persona is read only when the send names none AND the agent has personas to pick from.
|
|
439
|
+
*/
|
|
440
|
+
private resolvePersona;
|
|
441
|
+
/** Pin `persona` on the thread when it is not already — a no-op on a store that cannot. */
|
|
442
|
+
private pinThreadPersona;
|
|
443
|
+
private validateTitle;
|
|
444
|
+
/**
|
|
445
|
+
* Agent-selection precedence for a `chat()` call with no explicit `agentName`: the thread's own
|
|
446
|
+
* `defaultAgent` (if the thread exists and one is set) wins over the module's configured default.
|
|
447
|
+
* Skips the store entirely when the caller already named an agent or there's no thread yet.
|
|
448
|
+
*
|
|
449
|
+
* What this needs is one nullable scalar, so it asks for one. `getThread` returns the entire
|
|
450
|
+
* transcript — every message, every persisted tool output — and this discards all of it; on a
|
|
451
|
+
* long thread with large tool results that is hundreds of kilobytes read per turn, outside the
|
|
452
|
+
* workflow, for a string. A store predating {@link ThreadDefaultAgentReader} still answers, via
|
|
453
|
+
* the full read.
|
|
454
|
+
*/
|
|
455
|
+
private resolveAgentName;
|
|
456
|
+
private threadDefaultAgent;
|
|
457
|
+
private toSummaryView;
|
|
458
|
+
private toDetailView;
|
|
459
|
+
/**
|
|
460
|
+
* Re-mint each replayed attachment's url from the staging store, by `mediaId`: the url persisted
|
|
461
|
+
* with the message was minted for THAT turn (a presigned url with an expiry), so an old turn would
|
|
462
|
+
* otherwise show a dead link. The store's own access check applies — an attachment it will not
|
|
463
|
+
* resolve for this actor keeps the url it was stored with.
|
|
464
|
+
*/
|
|
465
|
+
private freshAttachmentUrls;
|
|
466
|
+
promoteThread(actor: Actor, threadId: string): Promise<void>;
|
|
467
|
+
/**
|
|
468
|
+
* Drop a message and everything after it — the "edit and resend" / "delete from here" primitive.
|
|
469
|
+
* The client then sends a fresh turn on the truncated thread. Ownership-gated like the other
|
|
470
|
+
* thread-scoped mutations.
|
|
471
|
+
*/
|
|
472
|
+
truncateThreadFrom(actor: Actor, threadId: string, messageId: string): Promise<void>;
|
|
473
|
+
/**
|
|
474
|
+
* Rate a message in one of `actor`'s threads: `'up'`/`'down'` with an optional comment, or `null`
|
|
475
|
+
* to clear the rating. Answers `404` for an unknown message, `403` for another actor's, and `501`
|
|
476
|
+
* on a store without `threadOfMessage` + `setMessageFeedback`.
|
|
477
|
+
*/
|
|
478
|
+
setMessageFeedback(actor: Actor, messageId: string, input: {
|
|
479
|
+
value: MessageFeedbackValue | null;
|
|
480
|
+
comment?: string;
|
|
481
|
+
}): Promise<MessageFeedback | null>;
|
|
482
|
+
/**
|
|
483
|
+
* Every file this actor has staged, newest first — across threads, including uploads that were
|
|
484
|
+
* never sent, which nothing else on this surface can see.
|
|
485
|
+
*
|
|
486
|
+
* Metadata only. A url is minted per turn by `resolve` so it can be short-lived, and handing one
|
|
487
|
+
* back per entry would undo that for the price of rendering a list.
|
|
488
|
+
*/
|
|
489
|
+
listAttachments(actor: Actor, options?: {
|
|
490
|
+
limit?: number;
|
|
491
|
+
}): Promise<StagedAttachment[]>;
|
|
492
|
+
/**
|
|
493
|
+
* This actor's staged media that no live message references and that is old enough not to be an
|
|
494
|
+
* upload in flight — the candidate set for a sweep. Returns them; never deletes them. The bytes
|
|
495
|
+
* are the host's, and so is the decision.
|
|
496
|
+
*
|
|
497
|
+
* `olderThan` is required and has no default. How long a composer may sit open with a file
|
|
498
|
+
* attached is the host's knowledge, and a library-chosen grace period would eventually delete a
|
|
499
|
+
* file someone was about to send. The staging store is asked to apply it, and the result is
|
|
500
|
+
* filtered again here: a store that ignores the hint would otherwise turn this into exactly that
|
|
501
|
+
* bug, silently.
|
|
502
|
+
*
|
|
503
|
+
* Both halves must be answerable or this refuses. An unanswerable reference query means "cannot
|
|
504
|
+
* tell", and treating it as "nothing is referenced" would hand back every attachment the actor
|
|
505
|
+
* ever sent, marked safe to delete.
|
|
506
|
+
*/
|
|
507
|
+
collectableAttachments(actor: Actor, options: {
|
|
508
|
+
olderThan: Date;
|
|
509
|
+
limit?: number;
|
|
510
|
+
}): Promise<StagedAttachment[]>;
|
|
511
|
+
/**
|
|
512
|
+
* The bound staging store's `list`, or a refusal. Absence is never answered with an empty list:
|
|
513
|
+
* "this host keeps no inventory" and "this actor has no files" are opposite facts that would
|
|
514
|
+
* otherwise render identically — as a clean file list, and as a sweep that collects nothing.
|
|
515
|
+
*/
|
|
516
|
+
private attachmentInventory;
|
|
517
|
+
/**
|
|
518
|
+
* Authorization seam for thread-scoped endpoints: the caller must own the thread. A missing
|
|
519
|
+
* thread is a 404; someone else's thread is a 403 — one actor never touches another's thread.
|
|
520
|
+
*/
|
|
521
|
+
private assertOwnsThread;
|
|
522
|
+
/**
|
|
523
|
+
* Authorization seam for HITL approve/reject: the caller must own the tool call's thread. Split
|
|
524
|
+
* out from run resolution (see {@link resolveRunForToolCall}) so the console `AgentApprovalPort`
|
|
525
|
+
* adapter can resolve+signal a decision WITHOUT this check — it is authorized upstream instead.
|
|
526
|
+
*/
|
|
527
|
+
private assertOwnsToolCall;
|
|
528
|
+
/**
|
|
529
|
+
* Check a reply against the questions it answers before it is signalled: every submitted value a
|
|
530
|
+
* question's own rules accept (a typed input's type, bounds and pattern; a pick from the offered
|
|
531
|
+
* options), and every `required` question answered — by the reply, or by the defaults an omitted
|
|
532
|
+
* question falls back to. A reply the loop would have to drop parts of is refused here instead, so
|
|
533
|
+
* the person who typed it hears why. Needs the store to hand back the call's recorded questions;
|
|
534
|
+
* without that seam the loop's own filtering is the only check.
|
|
535
|
+
*/
|
|
536
|
+
private assertAnswersFit;
|
|
537
|
+
/**
|
|
538
|
+
* Authorization seam for approve/reject. The call's recorded approver decides who may settle it:
|
|
539
|
+
* the requester (the default, and every call recorded before approvers existed) is the thread's
|
|
540
|
+
* own actor — the ownership check this always was; any other approver goes through the policy's
|
|
541
|
+
* `canDecide`, which by default asks whether the actor holds that role.
|
|
542
|
+
*/
|
|
543
|
+
private assertMayDecide;
|
|
544
|
+
/**
|
|
545
|
+
* Refuse a decision on a request that has already lapsed — recorded `expired`, or past its
|
|
546
|
+
* `expiresAt` with the run's own timer about to fire. Signalling it anyway would race the timeout:
|
|
547
|
+
* a durable runtime buffers a signal nobody is waiting on, and a late approval must not be what a
|
|
548
|
+
* later replay of the run reads back.
|
|
549
|
+
*/
|
|
550
|
+
private assertNotExpired;
|
|
551
|
+
/**
|
|
552
|
+
* Resolve the run awaiting `toolCallId`. Derived server-side from the tool call alone — the client
|
|
553
|
+
* never knows or supplies a runId, and that run is the sub-agent's own child run when the pending
|
|
554
|
+
* call belongs to a delegated agent. The store answers from the CALL'S OWN `runId` where it has
|
|
555
|
+
* one; see `AgentStore.runForToolCall` for why that matters once a thread can hold more than one
|
|
556
|
+
* live run.
|
|
557
|
+
*/
|
|
558
|
+
private resolveRunForToolCall;
|
|
559
|
+
/** Authorization seam for `cancel`: the caller must own the thread currently streaming this run. */
|
|
560
|
+
private assertOwnsActiveStream;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
interface A2uiAdapterOptions extends A2uiStreamOptions {
|
|
564
|
+
/** Where the route mounts, under the agent's path. Default `'a2ui'` → `POST <path>/a2ui`. */
|
|
565
|
+
path?: string;
|
|
566
|
+
/** As `agUiAdapter`'s: how long a run waiting on a person may stay silent before the stream ends. */
|
|
567
|
+
quietMs?: number;
|
|
568
|
+
/** Largest action accepted, as JSON, in bytes. Default 8192. */
|
|
569
|
+
maxActionBytes?: number;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Serves the agent as an A2UI agent. Not mounted directly — {@link a2uiAdapter} mounts a subclass at
|
|
573
|
+
* the configured path — but exported so a host can extend it under a route of its own.
|
|
574
|
+
*/
|
|
575
|
+
declare class A2uiRunHandler {
|
|
576
|
+
protected readonly agent: AgentService;
|
|
577
|
+
protected readonly actorResolver: ActorResolver;
|
|
578
|
+
protected readonly projection: A2uiStreamOptions;
|
|
579
|
+
protected readonly quietMs: number | undefined;
|
|
580
|
+
protected readonly maxActionBytes: number | undefined;
|
|
581
|
+
constructor(agent: AgentService, actorResolver: ActorResolver, projection: A2uiStreamOptions, quietMs: number | undefined, maxActionBytes: number | undefined);
|
|
582
|
+
handle(req: Request, res: Response, raw: unknown): Promise<void>;
|
|
583
|
+
/** Open the JSON Lines response. */
|
|
584
|
+
protected writeHead(res: Response, extra: Record<string, string>): void;
|
|
585
|
+
/** Write one run as A2UI JSON Lines, until it ends or stops to ask. */
|
|
586
|
+
protected stream(res: Response, runId: string, options: {
|
|
587
|
+
threadId?: string;
|
|
588
|
+
skip?: number;
|
|
589
|
+
answered?: string[];
|
|
590
|
+
}): Promise<void>;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Serve the agent as an A2UI (https://a2ui.org, v0.9) agent: `POST <path>/a2ui` answers with the
|
|
594
|
+
* run as a JSON Lines stream of A2UI server-to-client messages — `createSurface`,
|
|
595
|
+
* `updateComponents`, `updateDataModel`, `deleteSurface` — that any A2UI renderer draws.
|
|
596
|
+
*
|
|
597
|
+
* ```ts
|
|
598
|
+
* import { a2uiAdapter } from '@dudousxd/nestjs-agent/a2ui'
|
|
599
|
+
*
|
|
600
|
+
* AgentModule.forRoot({ model, adapters: [a2uiAdapter()] })
|
|
601
|
+
* ```
|
|
602
|
+
*
|
|
603
|
+
* The body is `{ threadId?, message?, action?, uiCapabilities?, agent? }`:
|
|
604
|
+
* - `message` — the user's text;
|
|
605
|
+
* - `action` — an A2UI client message (`{ version: 'v0.9', action: { name, surfaceId,
|
|
606
|
+
* sourceComponentId, timestamp, context } }`, or v0.8's `{ userAction }`): it becomes the turn
|
|
607
|
+
* (`uiActionText`). `agora.approve` / `agora.reject` with `context.interruptId` decide the
|
|
608
|
+
* approval a previous stream ended on, and stream the rest of that run (an independent
|
|
609
|
+
* proposal is decided through the proposal service and answered with its reply);
|
|
610
|
+
* - `threadId` — continue that thread (its owner is checked), or start one under that id.
|
|
611
|
+
*
|
|
612
|
+
* Response headers name the library's ids: `X-Agent-Thread-Id`, `X-Agent-Run-Id`. Authenticated by
|
|
613
|
+
* the module's `actorResolver`, guarded by its `guards` and owner-scoped exactly as `chat` is.
|
|
614
|
+
* Components nothing maps are drawn as their text, from the `AgentGenuiModule` catalog when there
|
|
615
|
+
* is one (or `catalog` here).
|
|
616
|
+
*/
|
|
617
|
+
declare function a2uiAdapter(options?: A2uiAdapterOptions): AgentProtocolAdapter;
|
|
618
|
+
|
|
619
|
+
export { AgentService as A, type ChatParams as C, DEFAULT_ALLOWED_ATTACHMENT_CONTENT_TYPES as D, MAX_ATTACHMENTS_PER_MESSAGE as M, NO_USER_MESSAGE_CODE as N, type QueuedSend as Q, type ThreadDefaultAgentReader as T, ActionProposalService as a, type A2uiAdapterOptions as b, A2uiRunHandler as c, type AttachmentLimits as d, type ChatSendMode as e, type ChatSendResult as f, DEFAULT_MAX_ATTACHMENT_BYTES as g, type ThreadModelReader as h, a2uiAdapter as i, attachmentLimits as j };
|