@agentforge-io/core 2.0.18 → 2.0.20
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/domain/conversation.d.ts +14 -0
- package/dist/services/agent.service.d.ts +13 -0
- package/dist/services/agent.service.js +114 -12
- package/dist/services/conversation.service.d.ts +3 -0
- package/dist/services/conversation.service.js +2 -0
- package/dist/types/agent.types.d.ts +37 -0
- package/package.json +1 -1
|
@@ -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'>>;
|
|
@@ -105,6 +105,19 @@ export declare class AgentService {
|
|
|
105
105
|
* authenticated user's connector tools (Gmail, Drive, …) on the fly
|
|
106
106
|
* via `overrides.extraTools`. Optional — connectors are opt-in. */
|
|
107
107
|
connectorRegistry?: ConnectorRegistryService | undefined);
|
|
108
|
+
/**
|
|
109
|
+
* Look up the human-friendly connector name + tool description for a
|
|
110
|
+
* given tool slug. Powers the friendly copy in `awaiting_approval` /
|
|
111
|
+
* `tool_blocked` cards: the visitor sees "Granola" instead of
|
|
112
|
+
* `granola_list_meetings`. Returns `undefined` for built-in / MCP
|
|
113
|
+
* tools, or when the registry isn't wired — the chat client falls
|
|
114
|
+
* back to the raw `toolName` in that case.
|
|
115
|
+
*
|
|
116
|
+
* Iterates every registered connector definition. The registry is
|
|
117
|
+
* small (handful of entries) so this is O(connectors × tools);
|
|
118
|
+
* acceptable for the rare per-approval call path.
|
|
119
|
+
*/
|
|
120
|
+
private describeTool;
|
|
108
121
|
/**
|
|
109
122
|
* Fetch the connector tools the user has authorized, swallowing failures.
|
|
110
123
|
* The agent loop must keep working even if a connector's refresh token is
|
|
@@ -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}`);
|
|
@@ -28,6 +29,38 @@ class AgentService {
|
|
|
28
29
|
this.hooks = hooks;
|
|
29
30
|
this.connectorRegistry = connectorRegistry;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Look up the human-friendly connector name + tool description for a
|
|
34
|
+
* given tool slug. Powers the friendly copy in `awaiting_approval` /
|
|
35
|
+
* `tool_blocked` cards: the visitor sees "Granola" instead of
|
|
36
|
+
* `granola_list_meetings`. Returns `undefined` for built-in / MCP
|
|
37
|
+
* tools, or when the registry isn't wired — the chat client falls
|
|
38
|
+
* back to the raw `toolName` in that case.
|
|
39
|
+
*
|
|
40
|
+
* Iterates every registered connector definition. The registry is
|
|
41
|
+
* small (handful of entries) so this is O(connectors × tools);
|
|
42
|
+
* acceptable for the rare per-approval call path.
|
|
43
|
+
*/
|
|
44
|
+
describeTool(toolName) {
|
|
45
|
+
if (!this.connectorRegistry)
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
for (const def of this.connectorRegistry.list()) {
|
|
49
|
+
for (const factory of def.tools) {
|
|
50
|
+
if (factory.definition.name === toolName) {
|
|
51
|
+
return {
|
|
52
|
+
connectorName: def.name,
|
|
53
|
+
toolDescription: factory.definition.description,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Registry is misconfigured — fall back to bare tool name.
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
31
64
|
/**
|
|
32
65
|
* Fetch the connector tools the user has authorized, swallowing failures.
|
|
33
66
|
* The agent loop must keep working even if a connector's refresh token is
|
|
@@ -240,18 +273,87 @@ class AgentService {
|
|
|
240
273
|
const resolvedExtras = await this.resolveExtraTools(agent.connectorOwnerUserId ?? params.userId);
|
|
241
274
|
const filter = params.overrides?.extraToolsFilter;
|
|
242
275
|
const extraTools = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
276
|
+
try {
|
|
277
|
+
for await (const chunk of this.runner.stream(agent, messages, {
|
|
278
|
+
userId: params.userId,
|
|
279
|
+
conversationId: params.conversationId,
|
|
280
|
+
agentId: conv.agentId,
|
|
281
|
+
messageId: 'streaming',
|
|
282
|
+
agent: { timezone: agent.timezone },
|
|
283
|
+
}, { ...(params.overrides ?? {}), extraTools })) {
|
|
284
|
+
if (chunk.type === 'text_delta')
|
|
285
|
+
fullContent += chunk.delta;
|
|
286
|
+
if (chunk.type === 'usage')
|
|
287
|
+
finalUsage = chunk.usage;
|
|
288
|
+
yield chunk;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
// Tool-approval gate decisions surface as typed exceptions. We
|
|
293
|
+
// intercept them HERE so the SSE consumer sees a structured
|
|
294
|
+
// chunk instead of a torn-down generator. Each branch also
|
|
295
|
+
// persists a synthetic assistant message carrying the metadata
|
|
296
|
+
// the chat client needs to re-render the card on reload — the
|
|
297
|
+
// stream may close, but the conversation history doesn't lose
|
|
298
|
+
// context.
|
|
299
|
+
if ((0, tool_approval_gate_1.isToolApprovalRequired)(err)) {
|
|
300
|
+
const ctx = this.describeTool(err.toolName);
|
|
301
|
+
yield {
|
|
302
|
+
type: 'awaiting_approval',
|
|
303
|
+
approvalId: err.approvalId,
|
|
304
|
+
toolName: err.toolName,
|
|
305
|
+
expiresAt: err.expiresAt,
|
|
306
|
+
connectorName: ctx?.connectorName,
|
|
307
|
+
toolDescription: ctx?.toolDescription,
|
|
308
|
+
};
|
|
309
|
+
await this.conversations.addMessage({
|
|
310
|
+
conversationId: params.conversationId,
|
|
311
|
+
userId: params.userId,
|
|
312
|
+
role: 'assistant',
|
|
313
|
+
// Plain-text fallback. Friendly enough for legacy clients
|
|
314
|
+
// that don't render `metadata.kind`. The structured card
|
|
315
|
+
// lives in `metadata` for capable widgets.
|
|
316
|
+
content: ctx?.connectorName
|
|
317
|
+
? `Necesito tu permiso para usar ${ctx.connectorName}.`
|
|
318
|
+
: `Necesito tu permiso para continuar.`,
|
|
319
|
+
metadata: {
|
|
320
|
+
kind: 'awaiting_approval',
|
|
321
|
+
approvalId: err.approvalId,
|
|
322
|
+
toolName: err.toolName,
|
|
323
|
+
expiresAt: err.expiresAt,
|
|
324
|
+
connectorName: ctx?.connectorName,
|
|
325
|
+
toolDescription: ctx?.toolDescription,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if ((0, tool_approval_gate_1.isToolBlockedError)(err)) {
|
|
331
|
+
const ctx = this.describeTool(err.toolName);
|
|
332
|
+
yield {
|
|
333
|
+
type: 'tool_blocked',
|
|
334
|
+
toolName: err.toolName,
|
|
335
|
+
reason: err.reason,
|
|
336
|
+
connectorName: ctx?.connectorName,
|
|
337
|
+
toolDescription: ctx?.toolDescription,
|
|
338
|
+
};
|
|
339
|
+
await this.conversations.addMessage({
|
|
340
|
+
conversationId: params.conversationId,
|
|
341
|
+
userId: params.userId,
|
|
342
|
+
role: 'assistant',
|
|
343
|
+
content: ctx?.connectorName
|
|
344
|
+
? `No puedo usar ${ctx.connectorName} en esta cuenta.`
|
|
345
|
+
: `No puedo usar esa herramienta en esta cuenta.`,
|
|
346
|
+
metadata: {
|
|
347
|
+
kind: 'tool_blocked',
|
|
348
|
+
toolName: err.toolName,
|
|
349
|
+
reason: err.reason,
|
|
350
|
+
connectorName: ctx?.connectorName,
|
|
351
|
+
toolDescription: ctx?.toolDescription,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
255
357
|
}
|
|
256
358
|
if (fullContent) {
|
|
257
359
|
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,39 @@ 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
|
+
/** Human-friendly connector name (`'Granola'`, `'Notion'`). The
|
|
191
|
+
* runner enriches it from the registry so the chat widget can
|
|
192
|
+
* render a sentence like "I need permission to use Granola"
|
|
193
|
+
* instead of the raw tool slug. Optional — clients fall back
|
|
194
|
+
* to `toolName` when the runtime doesn't supply this. */
|
|
195
|
+
connectorName?: string;
|
|
196
|
+
/** Human-friendly first sentence of the tool's description.
|
|
197
|
+
* Same story: enables the widget to say what the tool DOES in
|
|
198
|
+
* natural language. Optional. */
|
|
199
|
+
toolDescription?: string;
|
|
200
|
+
} | {
|
|
201
|
+
/** Emitted when a tool dispatch hit `kind: 'blocked'`. Mirrors the
|
|
202
|
+
* shape above so the client renders a terminal-error card with
|
|
203
|
+
* the same component. */
|
|
204
|
+
type: 'tool_blocked';
|
|
205
|
+
toolName: string;
|
|
206
|
+
reason?: string;
|
|
207
|
+
connectorName?: string;
|
|
208
|
+
toolDescription?: string;
|
|
176
209
|
} | {
|
|
177
210
|
type: 'done';
|
|
178
211
|
messageId: string;
|
|
@@ -207,5 +240,9 @@ export interface MessageRecord {
|
|
|
207
240
|
content: string;
|
|
208
241
|
toolCalls?: ToolCallRecord[];
|
|
209
242
|
usage?: TokenUsage;
|
|
243
|
+
/** Structured payload mirror of `Message.metadata`. Same kinds, same
|
|
244
|
+
* back-compat semantics. Lets the chat history endpoint pass the
|
|
245
|
+
* approval-card / blocked-tool markers through to the client. */
|
|
246
|
+
metadata?: Record<string, unknown>;
|
|
210
247
|
createdAt: Date;
|
|
211
248
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
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",
|