@agentforge-io/core 2.0.17 → 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.
@@ -28,6 +28,24 @@ export interface ConnectorToolFactory {
28
28
  definition: Omit<AgentToolDefinition, 'execute'>;
29
29
  build: (ctx: ConnectorToolContext) => AgentToolDefinition['execute'];
30
30
  }
31
+ /**
32
+ * Recommended initial state for a single connector tool when the tenant
33
+ * has no override yet.
34
+ *
35
+ * Same shape as the platform's `ToolPermission` (which is the canonical
36
+ * type stored on `agent.tools` / `automation.tools` / `skill.tools`
37
+ * JSONB). We redeclare it here so `@agentforge-io/core` stays free of any
38
+ * dependency on the host's `platform/` module — connectors are core's
39
+ * concern, ToolPermission is the host's. The shape must stay in sync;
40
+ * platform-side `hydrateToolList` accepts either.
41
+ */
42
+ export type ConnectorToolMode = 'allow' | 'approval' | 'blocked';
43
+ export interface ConnectorToolDefault {
44
+ /** Tool name as it appears in the connector's `tools[]` factory. */
45
+ name: string;
46
+ /** What the runtime should do by default when the LLM invokes this. */
47
+ mode: ConnectorToolMode;
48
+ }
31
49
  /**
32
50
  * UI hints for connectors that authenticate via a user-pasted API key
33
51
  * instead of the OAuth2 dance (Granola — and any future provider whose
@@ -86,4 +104,17 @@ export interface ConnectorDefinition {
86
104
  /** Tools this connector contributes to the agent's toolbelt once a user
87
105
  * has authorized. */
88
106
  tools: ConnectorToolFactory[];
107
+ /**
108
+ * Connector-author's recommended initial mode for each tool. The
109
+ * platform uses this as the fallback when the tenant has not saved a
110
+ * per-tool override yet (no row in `af_connector_tool_defaults`). It
111
+ * also seeds the `/connectors/:id` admin page on first visit.
112
+ *
113
+ * Tools not listed here are assumed `allow`. Tools listed that don't
114
+ * match any `tools[i].definition.name` are ignored by the resolver.
115
+ *
116
+ * Optional. When omitted, every tool defaults to `allow` (matches the
117
+ * historical whitelist semantics).
118
+ */
119
+ defaultToolPermissions?: ConnectorToolDefault[];
89
120
  }
@@ -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
- for await (const chunk of this.runner.stream(agent, messages, {
244
- userId: params.userId,
245
- conversationId: params.conversationId,
246
- agentId: conv.agentId,
247
- messageId: 'streaming',
248
- agent: { timezone: agent.timezone },
249
- }, { ...(params.overrides ?? {}), extraTools })) {
250
- if (chunk.type === 'text_delta')
251
- fullContent += chunk.delta;
252
- if (chunk.type === 'usage')
253
- finalUsage = chunk.usage;
254
- yield chunk;
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.17",
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",