@agentforge-io/core 2.0.18 → 2.0.19
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.
|
@@ -20,6 +20,20 @@ export interface Message {
|
|
|
20
20
|
content: string;
|
|
21
21
|
toolCalls?: ToolCallRecord[];
|
|
22
22
|
usage?: TokenUsage;
|
|
23
|
+
/**
|
|
24
|
+
* Free-form metadata the host can attach to a message. The runtime is
|
|
25
|
+
* agnostic about its contents — it persists and surfaces it back. Used
|
|
26
|
+
* today by:
|
|
27
|
+
*
|
|
28
|
+
* - `tool_approvals`: synthetic assistant message carries
|
|
29
|
+
* `{ kind: 'awaiting_approval', approvalId, toolName, expiresAt }`
|
|
30
|
+
* so the chat client can render an Approve/Deny card inline
|
|
31
|
+
* instead of plain text.
|
|
32
|
+
*
|
|
33
|
+
* Add new kinds without a schema migration — the field is JSONB in
|
|
34
|
+
* the persistence adapter.
|
|
35
|
+
*/
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
23
37
|
createdAt: Date;
|
|
24
38
|
}
|
|
25
39
|
export type NewConversation = Pick<Conversation, 'userId' | 'agentId'> & Partial<Omit<Conversation, 'id' | 'createdAt' | 'updatedAt'>>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AgentService = exports.AgentForbiddenError = void 0;
|
|
4
|
+
const tool_approval_gate_1 = require("./tool-approval-gate");
|
|
4
5
|
class AgentForbiddenError extends Error {
|
|
5
6
|
constructor(reason) {
|
|
6
7
|
super(`Usage limit exceeded: ${reason}`);
|
|
@@ -240,18 +241,73 @@ class AgentService {
|
|
|
240
241
|
const resolvedExtras = await this.resolveExtraTools(agent.connectorOwnerUserId ?? params.userId);
|
|
241
242
|
const filter = params.overrides?.extraToolsFilter;
|
|
242
243
|
const extraTools = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
244
|
+
try {
|
|
245
|
+
for await (const chunk of this.runner.stream(agent, messages, {
|
|
246
|
+
userId: params.userId,
|
|
247
|
+
conversationId: params.conversationId,
|
|
248
|
+
agentId: conv.agentId,
|
|
249
|
+
messageId: 'streaming',
|
|
250
|
+
agent: { timezone: agent.timezone },
|
|
251
|
+
}, { ...(params.overrides ?? {}), extraTools })) {
|
|
252
|
+
if (chunk.type === 'text_delta')
|
|
253
|
+
fullContent += chunk.delta;
|
|
254
|
+
if (chunk.type === 'usage')
|
|
255
|
+
finalUsage = chunk.usage;
|
|
256
|
+
yield chunk;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
// Tool-approval gate decisions surface as typed exceptions. We
|
|
261
|
+
// intercept them HERE so the SSE consumer sees a structured
|
|
262
|
+
// chunk instead of a torn-down generator. Each branch also
|
|
263
|
+
// persists a synthetic assistant message carrying the metadata
|
|
264
|
+
// the chat client needs to re-render the card on reload — the
|
|
265
|
+
// stream may close, but the conversation history doesn't lose
|
|
266
|
+
// context.
|
|
267
|
+
if ((0, tool_approval_gate_1.isToolApprovalRequired)(err)) {
|
|
268
|
+
yield {
|
|
269
|
+
type: 'awaiting_approval',
|
|
270
|
+
approvalId: err.approvalId,
|
|
271
|
+
toolName: err.toolName,
|
|
272
|
+
expiresAt: err.expiresAt,
|
|
273
|
+
};
|
|
274
|
+
await this.conversations.addMessage({
|
|
275
|
+
conversationId: params.conversationId,
|
|
276
|
+
userId: params.userId,
|
|
277
|
+
role: 'assistant',
|
|
278
|
+
// Plain-text body so legacy clients (or a server reload of
|
|
279
|
+
// history into a non-aware client) still get a readable
|
|
280
|
+
// line. The card lives in `metadata`.
|
|
281
|
+
content: `(waiting for approval to run \`${err.toolName}\`)`,
|
|
282
|
+
metadata: {
|
|
283
|
+
kind: 'awaiting_approval',
|
|
284
|
+
approvalId: err.approvalId,
|
|
285
|
+
toolName: err.toolName,
|
|
286
|
+
expiresAt: err.expiresAt,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if ((0, tool_approval_gate_1.isToolBlockedError)(err)) {
|
|
292
|
+
yield {
|
|
293
|
+
type: 'tool_blocked',
|
|
294
|
+
toolName: err.toolName,
|
|
295
|
+
reason: err.reason,
|
|
296
|
+
};
|
|
297
|
+
await this.conversations.addMessage({
|
|
298
|
+
conversationId: params.conversationId,
|
|
299
|
+
userId: params.userId,
|
|
300
|
+
role: 'assistant',
|
|
301
|
+
content: err.reason ?? `Tool "${err.toolName}" is blocked.`,
|
|
302
|
+
metadata: {
|
|
303
|
+
kind: 'tool_blocked',
|
|
304
|
+
toolName: err.toolName,
|
|
305
|
+
reason: err.reason,
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
throw err;
|
|
255
311
|
}
|
|
256
312
|
if (fullContent) {
|
|
257
313
|
await this.conversations.addMessage({
|
|
@@ -23,6 +23,9 @@ export declare class ConversationService {
|
|
|
23
23
|
content: string;
|
|
24
24
|
toolCalls?: ToolCallRecord[];
|
|
25
25
|
usage?: TokenUsage;
|
|
26
|
+
/** Optional structured payload. See `Message.metadata` in
|
|
27
|
+
* `domain/conversation.ts` for usage. */
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
26
29
|
}): Promise<Message>;
|
|
27
30
|
/** Throws on not-found OR cross-user access. */
|
|
28
31
|
private loadOwned;
|
|
@@ -34,6 +34,7 @@ class ConversationService {
|
|
|
34
34
|
content: params.content,
|
|
35
35
|
toolCalls: params.toolCalls,
|
|
36
36
|
usage: params.usage,
|
|
37
|
+
metadata: params.metadata,
|
|
37
38
|
});
|
|
38
39
|
await this.convRepo.updateStats(params.conversationId, {
|
|
39
40
|
addInputTokens: params.usage?.inputTokens,
|
|
@@ -94,6 +95,7 @@ class ConversationService {
|
|
|
94
95
|
content: msg.content,
|
|
95
96
|
toolCalls: msg.toolCalls,
|
|
96
97
|
usage: msg.usage,
|
|
98
|
+
metadata: msg.metadata,
|
|
97
99
|
createdAt: msg.createdAt,
|
|
98
100
|
};
|
|
99
101
|
}
|
|
@@ -173,6 +173,27 @@ export type StreamChunk = {
|
|
|
173
173
|
} | {
|
|
174
174
|
type: 'usage';
|
|
175
175
|
usage: TokenUsage;
|
|
176
|
+
} | {
|
|
177
|
+
/**
|
|
178
|
+
* Emitted by the runtime when a tool dispatch hit
|
|
179
|
+
* `ToolApprovalGate.check() → { kind: 'approval' }`. The stream
|
|
180
|
+
* ends right after this chunk — the runner is paused until the
|
|
181
|
+
* approval is decided. The chat client renders an Approve/Deny
|
|
182
|
+
* card based on the carried fields; after Approve the client
|
|
183
|
+
* triggers a fresh `sendMessage("continue")` and the gate's
|
|
184
|
+
* fast-path consumes the pending row.
|
|
185
|
+
*/
|
|
186
|
+
type: 'awaiting_approval';
|
|
187
|
+
approvalId: string;
|
|
188
|
+
toolName: string;
|
|
189
|
+
expiresAt: string;
|
|
190
|
+
} | {
|
|
191
|
+
/** Emitted when a tool dispatch hit `kind: 'blocked'`. Mirrors the
|
|
192
|
+
* shape above so the client renders a terminal-error card with
|
|
193
|
+
* the same component. */
|
|
194
|
+
type: 'tool_blocked';
|
|
195
|
+
toolName: string;
|
|
196
|
+
reason?: string;
|
|
176
197
|
} | {
|
|
177
198
|
type: 'done';
|
|
178
199
|
messageId: string;
|
|
@@ -207,5 +228,9 @@ export interface MessageRecord {
|
|
|
207
228
|
content: string;
|
|
208
229
|
toolCalls?: ToolCallRecord[];
|
|
209
230
|
usage?: TokenUsage;
|
|
231
|
+
/** Structured payload mirror of `Message.metadata`. Same kinds, same
|
|
232
|
+
* back-compat semantics. Lets the chat history endpoint pass the
|
|
233
|
+
* approval-card / blocked-tool markers through to the client. */
|
|
234
|
+
metadata?: Record<string, unknown>;
|
|
210
235
|
createdAt: Date;
|
|
211
236
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
4
4
|
"description": "Framework-free AI runtime SDK. Owns: agent loop (Anthropic), conversations, tools, streaming, agent-job queue, SdkHooks. Identity, billing, infra (email/uploads/secrets) live in the host's modules — not here.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|