@agentforge-io/core 2.0.19 → 2.0.21
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/services/agent.service.d.ts +25 -1
- package/dist/services/agent.service.js +94 -6
- package/dist/services/approval-copywriter.service.d.ts +57 -0
- package/dist/services/approval-copywriter.service.js +176 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +3 -1
- package/dist/types/agent.types.d.ts +25 -3
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import type { SdkHooks } from '../types/hooks';
|
|
|
4
4
|
import type { AgentRunnerService } from './agent-runner.service';
|
|
5
5
|
import type { ConversationService } from './conversation.service';
|
|
6
6
|
import type { ConnectorRegistryService } from './connector-registry.service';
|
|
7
|
+
import type { ApprovalCopywriterService } from './approval-copywriter.service';
|
|
7
8
|
/**
|
|
8
9
|
* Minimal record the host's agent-config layer must supply to resolve
|
|
9
10
|
* agents by `(tenantId, slug)` or by id. The SDK doesn't care where this
|
|
@@ -94,6 +95,11 @@ export declare class AgentService {
|
|
|
94
95
|
* authenticated user's connector tools (Gmail, Drive, …) on the fly
|
|
95
96
|
* via `overrides.extraTools`. Optional — connectors are opt-in. */
|
|
96
97
|
private readonly connectorRegistry?;
|
|
98
|
+
/** When wired, the AgentService asks this service to generate the
|
|
99
|
+
* microcopy shown in the in-chat approval bubble. Falls back to a
|
|
100
|
+
* meta-only render on the client when unwired or when generation
|
|
101
|
+
* fails. */
|
|
102
|
+
private readonly copywriter?;
|
|
97
103
|
constructor(agents: AgentDefinition[], runner: AgentRunnerService, conversations: ConversationService,
|
|
98
104
|
/** When wired, agents created via the admin UI are looked up here first;
|
|
99
105
|
* the hardcoded `agents` array remains a fallback for legacy installs. */
|
|
@@ -104,7 +110,25 @@ export declare class AgentService {
|
|
|
104
110
|
/** When wired, every streamMessage / sendMessage call attaches the
|
|
105
111
|
* authenticated user's connector tools (Gmail, Drive, …) on the fly
|
|
106
112
|
* via `overrides.extraTools`. Optional — connectors are opt-in. */
|
|
107
|
-
connectorRegistry?: ConnectorRegistryService | undefined
|
|
113
|
+
connectorRegistry?: ConnectorRegistryService | undefined,
|
|
114
|
+
/** When wired, the AgentService asks this service to generate the
|
|
115
|
+
* microcopy shown in the in-chat approval bubble. Falls back to a
|
|
116
|
+
* meta-only render on the client when unwired or when generation
|
|
117
|
+
* fails. */
|
|
118
|
+
copywriter?: ApprovalCopywriterService | undefined);
|
|
119
|
+
/**
|
|
120
|
+
* Look up the human-friendly connector name + tool description for a
|
|
121
|
+
* given tool slug. Powers the friendly copy in `awaiting_approval` /
|
|
122
|
+
* `tool_blocked` cards: the visitor sees "Granola" instead of
|
|
123
|
+
* `granola_list_meetings`. Returns `undefined` for built-in / MCP
|
|
124
|
+
* tools, or when the registry isn't wired — the chat client falls
|
|
125
|
+
* back to the raw `toolName` in that case.
|
|
126
|
+
*
|
|
127
|
+
* Iterates every registered connector definition. The registry is
|
|
128
|
+
* small (handful of entries) so this is O(connectors × tools);
|
|
129
|
+
* acceptable for the rare per-approval call path.
|
|
130
|
+
*/
|
|
131
|
+
private describeTool;
|
|
108
132
|
/**
|
|
109
133
|
* Fetch the connector tools the user has authorized, swallowing failures.
|
|
110
134
|
* The agent loop must keep working even if a connector's refresh token is
|
|
@@ -21,13 +21,51 @@ class AgentService {
|
|
|
21
21
|
/** When wired, every streamMessage / sendMessage call attaches the
|
|
22
22
|
* authenticated user's connector tools (Gmail, Drive, …) on the fly
|
|
23
23
|
* via `overrides.extraTools`. Optional — connectors are opt-in. */
|
|
24
|
-
connectorRegistry
|
|
24
|
+
connectorRegistry,
|
|
25
|
+
/** When wired, the AgentService asks this service to generate the
|
|
26
|
+
* microcopy shown in the in-chat approval bubble. Falls back to a
|
|
27
|
+
* meta-only render on the client when unwired or when generation
|
|
28
|
+
* fails. */
|
|
29
|
+
copywriter) {
|
|
25
30
|
this.agents = agents;
|
|
26
31
|
this.runner = runner;
|
|
27
32
|
this.conversations = conversations;
|
|
28
33
|
this.resolver = resolver;
|
|
29
34
|
this.hooks = hooks;
|
|
30
35
|
this.connectorRegistry = connectorRegistry;
|
|
36
|
+
this.copywriter = copywriter;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Look up the human-friendly connector name + tool description for a
|
|
40
|
+
* given tool slug. Powers the friendly copy in `awaiting_approval` /
|
|
41
|
+
* `tool_blocked` cards: the visitor sees "Granola" instead of
|
|
42
|
+
* `granola_list_meetings`. Returns `undefined` for built-in / MCP
|
|
43
|
+
* tools, or when the registry isn't wired — the chat client falls
|
|
44
|
+
* back to the raw `toolName` in that case.
|
|
45
|
+
*
|
|
46
|
+
* Iterates every registered connector definition. The registry is
|
|
47
|
+
* small (handful of entries) so this is O(connectors × tools);
|
|
48
|
+
* acceptable for the rare per-approval call path.
|
|
49
|
+
*/
|
|
50
|
+
describeTool(toolName) {
|
|
51
|
+
if (!this.connectorRegistry)
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
for (const def of this.connectorRegistry.list()) {
|
|
55
|
+
for (const factory of def.tools) {
|
|
56
|
+
if (factory.definition.name === toolName) {
|
|
57
|
+
return {
|
|
58
|
+
connectorName: def.name,
|
|
59
|
+
toolDescription: factory.definition.description,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Registry is misconfigured — fall back to bare tool name.
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
31
69
|
}
|
|
32
70
|
/**
|
|
33
71
|
* Fetch the connector tools the user has authorized, swallowing failures.
|
|
@@ -265,44 +303,80 @@ class AgentService {
|
|
|
265
303
|
// stream may close, but the conversation history doesn't lose
|
|
266
304
|
// context.
|
|
267
305
|
if ((0, tool_approval_gate_1.isToolApprovalRequired)(err)) {
|
|
306
|
+
const ctx = this.describeTool(err.toolName);
|
|
307
|
+
const copy = await this.copywriter?.generate({
|
|
308
|
+
kind: 'approval',
|
|
309
|
+
toolName: err.toolName,
|
|
310
|
+
connectorName: ctx?.connectorName,
|
|
311
|
+
toolDescription: ctx?.toolDescription,
|
|
312
|
+
recentMessages: extractRecentForCopywriter(messages),
|
|
313
|
+
agentPersona: agent.systemPrompt,
|
|
314
|
+
});
|
|
268
315
|
yield {
|
|
269
316
|
type: 'awaiting_approval',
|
|
270
317
|
approvalId: err.approvalId,
|
|
271
318
|
toolName: err.toolName,
|
|
272
319
|
expiresAt: err.expiresAt,
|
|
320
|
+
connectorName: ctx?.connectorName,
|
|
321
|
+
toolDescription: ctx?.toolDescription,
|
|
322
|
+
copy,
|
|
273
323
|
};
|
|
274
324
|
await this.conversations.addMessage({
|
|
275
325
|
conversationId: params.conversationId,
|
|
276
326
|
userId: params.userId,
|
|
277
327
|
role: 'assistant',
|
|
278
|
-
// Plain-text
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
content:
|
|
328
|
+
// Plain-text fallback for legacy clients that don't render
|
|
329
|
+
// `metadata.kind`. Capable widgets read the structured card
|
|
330
|
+
// from `metadata.copy`.
|
|
331
|
+
content: copy?.body ??
|
|
332
|
+
(ctx?.connectorName
|
|
333
|
+
? `Necesito tu permiso para usar ${ctx.connectorName}.`
|
|
334
|
+
: `Necesito tu permiso para continuar.`),
|
|
282
335
|
metadata: {
|
|
283
336
|
kind: 'awaiting_approval',
|
|
284
337
|
approvalId: err.approvalId,
|
|
285
338
|
toolName: err.toolName,
|
|
286
339
|
expiresAt: err.expiresAt,
|
|
340
|
+
connectorName: ctx?.connectorName,
|
|
341
|
+
toolDescription: ctx?.toolDescription,
|
|
342
|
+
copy,
|
|
287
343
|
},
|
|
288
344
|
});
|
|
289
345
|
return;
|
|
290
346
|
}
|
|
291
347
|
if ((0, tool_approval_gate_1.isToolBlockedError)(err)) {
|
|
348
|
+
const ctx = this.describeTool(err.toolName);
|
|
349
|
+
const copy = await this.copywriter?.generate({
|
|
350
|
+
kind: 'blocked',
|
|
351
|
+
toolName: err.toolName,
|
|
352
|
+
connectorName: ctx?.connectorName,
|
|
353
|
+
toolDescription: ctx?.toolDescription,
|
|
354
|
+
recentMessages: extractRecentForCopywriter(messages),
|
|
355
|
+
agentPersona: agent.systemPrompt,
|
|
356
|
+
});
|
|
292
357
|
yield {
|
|
293
358
|
type: 'tool_blocked',
|
|
294
359
|
toolName: err.toolName,
|
|
295
360
|
reason: err.reason,
|
|
361
|
+
connectorName: ctx?.connectorName,
|
|
362
|
+
toolDescription: ctx?.toolDescription,
|
|
363
|
+
copy,
|
|
296
364
|
};
|
|
297
365
|
await this.conversations.addMessage({
|
|
298
366
|
conversationId: params.conversationId,
|
|
299
367
|
userId: params.userId,
|
|
300
368
|
role: 'assistant',
|
|
301
|
-
content:
|
|
369
|
+
content: copy?.blockedBody ??
|
|
370
|
+
(ctx?.connectorName
|
|
371
|
+
? `No puedo usar ${ctx.connectorName} en esta cuenta.`
|
|
372
|
+
: `No puedo usar esa herramienta en esta cuenta.`),
|
|
302
373
|
metadata: {
|
|
303
374
|
kind: 'tool_blocked',
|
|
304
375
|
toolName: err.toolName,
|
|
305
376
|
reason: err.reason,
|
|
377
|
+
connectorName: ctx?.connectorName,
|
|
378
|
+
toolDescription: ctx?.toolDescription,
|
|
379
|
+
copy,
|
|
306
380
|
},
|
|
307
381
|
});
|
|
308
382
|
return;
|
|
@@ -383,6 +457,20 @@ class AgentService {
|
|
|
383
457
|
}
|
|
384
458
|
}
|
|
385
459
|
exports.AgentService = AgentService;
|
|
460
|
+
/**
|
|
461
|
+
* Pull the last few text-only turns out of the Anthropic message array
|
|
462
|
+
* for the approval copywriter. We strip tool blocks because they are
|
|
463
|
+
* not useful for language/tone detection and they can be long. Returns
|
|
464
|
+
* the most recent 6 turns, each capped to ~600 chars.
|
|
465
|
+
*/
|
|
466
|
+
function extractRecentForCopywriter(messages) {
|
|
467
|
+
const trimmed = messages
|
|
468
|
+
.map((m) => ({ role: m.role, content: m.content?.trim?.() ?? '' }))
|
|
469
|
+
.filter((m) => m.content.length > 0)
|
|
470
|
+
.slice(-6)
|
|
471
|
+
.map((m) => ({ role: m.role, content: m.content.slice(0, 600) }));
|
|
472
|
+
return trimmed;
|
|
473
|
+
}
|
|
386
474
|
/**
|
|
387
475
|
* Map a persisted `AgentRecord` to the runtime `AgentDefinition` the runner
|
|
388
476
|
* expects. The `context` column (plain-text knowledge) is prepended to the
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface ApprovalCopyBundle {
|
|
2
|
+
title: string;
|
|
3
|
+
body: string;
|
|
4
|
+
approveLabel: string;
|
|
5
|
+
approveBusyLabel: string;
|
|
6
|
+
denyLabel: string;
|
|
7
|
+
denyBusyLabel: string;
|
|
8
|
+
approvedPill: string;
|
|
9
|
+
deniedPill: string;
|
|
10
|
+
readOnlyHint: string;
|
|
11
|
+
blockedTitle: string;
|
|
12
|
+
blockedBody: string;
|
|
13
|
+
/** Formatter hint for "Expires in Xm Ys". The SDK fills the time. */
|
|
14
|
+
expiresPrefix: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ApprovalCopywriterInput {
|
|
17
|
+
toolName: string;
|
|
18
|
+
connectorName?: string;
|
|
19
|
+
toolDescription?: string;
|
|
20
|
+
kind: 'approval' | 'blocked';
|
|
21
|
+
/** Last few turns of the conversation. The model uses these to pick
|
|
22
|
+
* language, tone and any references the user already established. */
|
|
23
|
+
recentMessages?: {
|
|
24
|
+
role: 'user' | 'assistant';
|
|
25
|
+
content: string;
|
|
26
|
+
}[];
|
|
27
|
+
/** Optional agent persona/instructions excerpt so the copy keeps the
|
|
28
|
+
* agent's voice. Trimmed to a few hundred chars by the caller. */
|
|
29
|
+
agentPersona?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generates a natural-language approval copy bundle for the bubble the
|
|
33
|
+
* chat widget renders when the tool-approval gate pauses a run.
|
|
34
|
+
*
|
|
35
|
+
* Why a dedicated service: the SDK used to ship a hardcoded English (then
|
|
36
|
+
* Spanish) string set. That doesn't scale — different tenants speak
|
|
37
|
+
* different languages, and the bubble should sound like the rest of the
|
|
38
|
+
* agent's voice, not a generic system prompt. Delegating copy to the
|
|
39
|
+
* model that's already shaping the conversation keeps tone, locale and
|
|
40
|
+
* register coherent without dictionaries to maintain.
|
|
41
|
+
*
|
|
42
|
+
* The actual model call is Haiku (cheap + ~200ms) with a tight token cap.
|
|
43
|
+
* If anything goes wrong — timeout, parse error, missing API key — the
|
|
44
|
+
* caller gets `undefined` and falls back to a meta-only render. The gate
|
|
45
|
+
* MUST keep working when the copywriter doesn't.
|
|
46
|
+
*/
|
|
47
|
+
export declare class ApprovalCopywriterService {
|
|
48
|
+
private readonly client;
|
|
49
|
+
private readonly cache;
|
|
50
|
+
constructor(opts: {
|
|
51
|
+
apiKey?: string;
|
|
52
|
+
baseURL?: string;
|
|
53
|
+
});
|
|
54
|
+
generate(input: ApprovalCopywriterInput): Promise<ApprovalCopyBundle | undefined>;
|
|
55
|
+
private cacheKey;
|
|
56
|
+
private callModel;
|
|
57
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ApprovalCopywriterService = void 0;
|
|
7
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
10
|
+
const HAIKU_MODEL = 'claude-haiku-4-5-20251001';
|
|
11
|
+
const HAIKU_MAX_TOKENS = 400;
|
|
12
|
+
const HAIKU_TIMEOUT_MS = 2_500;
|
|
13
|
+
/**
|
|
14
|
+
* Generates a natural-language approval copy bundle for the bubble the
|
|
15
|
+
* chat widget renders when the tool-approval gate pauses a run.
|
|
16
|
+
*
|
|
17
|
+
* Why a dedicated service: the SDK used to ship a hardcoded English (then
|
|
18
|
+
* Spanish) string set. That doesn't scale — different tenants speak
|
|
19
|
+
* different languages, and the bubble should sound like the rest of the
|
|
20
|
+
* agent's voice, not a generic system prompt. Delegating copy to the
|
|
21
|
+
* model that's already shaping the conversation keeps tone, locale and
|
|
22
|
+
* register coherent without dictionaries to maintain.
|
|
23
|
+
*
|
|
24
|
+
* The actual model call is Haiku (cheap + ~200ms) with a tight token cap.
|
|
25
|
+
* If anything goes wrong — timeout, parse error, missing API key — the
|
|
26
|
+
* caller gets `undefined` and falls back to a meta-only render. The gate
|
|
27
|
+
* MUST keep working when the copywriter doesn't.
|
|
28
|
+
*/
|
|
29
|
+
class ApprovalCopywriterService {
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
this.cache = new Map();
|
|
32
|
+
if (!opts.apiKey) {
|
|
33
|
+
this.client = undefined;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.client = new sdk_1.default({
|
|
37
|
+
apiKey: opts.apiKey,
|
|
38
|
+
baseURL: opts.baseURL,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async generate(input) {
|
|
42
|
+
if (!this.client)
|
|
43
|
+
return undefined;
|
|
44
|
+
const key = this.cacheKey(input);
|
|
45
|
+
const hit = this.cache.get(key);
|
|
46
|
+
if (hit && hit.expiresAt > Date.now())
|
|
47
|
+
return hit.value;
|
|
48
|
+
try {
|
|
49
|
+
const value = await this.callModel(input);
|
|
50
|
+
if (!value)
|
|
51
|
+
return undefined;
|
|
52
|
+
this.cache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
cacheKey(input) {
|
|
60
|
+
const recent = (input.recentMessages ?? [])
|
|
61
|
+
.slice(-3)
|
|
62
|
+
.map((m) => `${m.role[0]}:${m.content.slice(0, 120)}`)
|
|
63
|
+
.join('|');
|
|
64
|
+
const raw = [
|
|
65
|
+
input.kind,
|
|
66
|
+
input.toolName,
|
|
67
|
+
input.connectorName ?? '',
|
|
68
|
+
input.toolDescription ?? '',
|
|
69
|
+
input.agentPersona?.slice(0, 200) ?? '',
|
|
70
|
+
recent,
|
|
71
|
+
].join('');
|
|
72
|
+
return (0, crypto_1.createHash)('sha1').update(raw).digest('hex');
|
|
73
|
+
}
|
|
74
|
+
async callModel(input) {
|
|
75
|
+
const recent = (input.recentMessages ?? [])
|
|
76
|
+
.slice(-6)
|
|
77
|
+
.map((m) => `${m.role.toUpperCase()}: ${m.content.slice(0, 600)}`)
|
|
78
|
+
.join('\n');
|
|
79
|
+
const targetLabel = input.connectorName ?? input.toolName;
|
|
80
|
+
const whatItDoes = input.toolDescription
|
|
81
|
+
? `It does: ${input.toolDescription}`
|
|
82
|
+
: '';
|
|
83
|
+
const personaLine = input.agentPersona
|
|
84
|
+
? `Agent persona: ${input.agentPersona.slice(0, 400)}`
|
|
85
|
+
: '';
|
|
86
|
+
const guidance = input.kind === 'approval'
|
|
87
|
+
? `The agent paused because it wants to use the "${targetLabel}" capability and needs the user's permission. Write a short, warm bubble that matches the language, tone and register of the conversation so far. Mention "${targetLabel}" naturally. Keep it conversational — not corporate or robotic.`
|
|
88
|
+
: `The agent tried to use the "${targetLabel}" capability but it is blocked on this account. Write a short bubble that tells the user clearly, in the language and tone of the conversation, that this action isn't available. Do not blame them. Do not invite retrying.`;
|
|
89
|
+
const system = [
|
|
90
|
+
'You write microcopy for an in-chat permission card. Output STRICT JSON, no prose, no code fences.',
|
|
91
|
+
'Match the language of the most recent USER message. If the conversation is empty, default to English.',
|
|
92
|
+
'Keep all strings under ~80 chars unless noted. Buttons under ~20 chars.',
|
|
93
|
+
'No emojis. No markdown.',
|
|
94
|
+
].join(' ');
|
|
95
|
+
const userPrompt = [
|
|
96
|
+
guidance,
|
|
97
|
+
whatItDoes,
|
|
98
|
+
personaLine,
|
|
99
|
+
'',
|
|
100
|
+
'Recent conversation (most recent last):',
|
|
101
|
+
recent || '(none)',
|
|
102
|
+
'',
|
|
103
|
+
'Return JSON with exactly these keys (all strings):',
|
|
104
|
+
'{',
|
|
105
|
+
' "title": "headline of the bubble",',
|
|
106
|
+
' "body": "one sentence asking permission or explaining the block",',
|
|
107
|
+
' "approveLabel": "primary button when asking permission",',
|
|
108
|
+
' "approveBusyLabel": "primary button while confirming",',
|
|
109
|
+
' "denyLabel": "secondary button when asking permission",',
|
|
110
|
+
' "denyBusyLabel": "secondary button while confirming",',
|
|
111
|
+
' "approvedPill": "tiny status pill after the user approved",',
|
|
112
|
+
' "deniedPill": "tiny status pill after the user denied",',
|
|
113
|
+
' "readOnlyHint": "tiny line shown when the bubble is read-only (e.g. on history)",',
|
|
114
|
+
' "blockedTitle": "headline when the action is blocked",',
|
|
115
|
+
' "blockedBody": "one sentence explaining the block",',
|
|
116
|
+
' "expiresPrefix": "short prefix shown before the countdown, e.g. \\"Expires in\\""',
|
|
117
|
+
'}',
|
|
118
|
+
].join('\n');
|
|
119
|
+
const ac = new AbortController();
|
|
120
|
+
const timer = setTimeout(() => ac.abort(), HAIKU_TIMEOUT_MS);
|
|
121
|
+
try {
|
|
122
|
+
const res = await this.client.messages.create({
|
|
123
|
+
model: HAIKU_MODEL,
|
|
124
|
+
max_tokens: HAIKU_MAX_TOKENS,
|
|
125
|
+
temperature: 0.4,
|
|
126
|
+
system,
|
|
127
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
128
|
+
}, { signal: ac.signal });
|
|
129
|
+
const text = res.content
|
|
130
|
+
.filter((b) => b.type === 'text')
|
|
131
|
+
.map((b) => b.text)
|
|
132
|
+
.join('')
|
|
133
|
+
.trim();
|
|
134
|
+
return parseBundle(text);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.ApprovalCopywriterService = ApprovalCopywriterService;
|
|
142
|
+
function parseBundle(text) {
|
|
143
|
+
const jsonStart = text.indexOf('{');
|
|
144
|
+
const jsonEnd = text.lastIndexOf('}');
|
|
145
|
+
if (jsonStart < 0 || jsonEnd <= jsonStart)
|
|
146
|
+
return undefined;
|
|
147
|
+
let obj;
|
|
148
|
+
try {
|
|
149
|
+
obj = JSON.parse(text.slice(jsonStart, jsonEnd + 1));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const keys = [
|
|
155
|
+
'title',
|
|
156
|
+
'body',
|
|
157
|
+
'approveLabel',
|
|
158
|
+
'approveBusyLabel',
|
|
159
|
+
'denyLabel',
|
|
160
|
+
'denyBusyLabel',
|
|
161
|
+
'approvedPill',
|
|
162
|
+
'deniedPill',
|
|
163
|
+
'readOnlyHint',
|
|
164
|
+
'blockedTitle',
|
|
165
|
+
'blockedBody',
|
|
166
|
+
'expiresPrefix',
|
|
167
|
+
];
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const k of keys) {
|
|
170
|
+
const v = obj[k];
|
|
171
|
+
if (typeof v !== 'string' || !v.trim())
|
|
172
|
+
return undefined;
|
|
173
|
+
out[k] = v.trim();
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { PreparedStreamService, PreparedStreamError, } from './prepared-stream.s
|
|
|
5
5
|
export { InMemoryPreparedStreamStore } from './in-memory-prepared-stream.store';
|
|
6
6
|
export { ConversationService, ConversationNotFoundError, } from './conversation.service';
|
|
7
7
|
export { AgentService, AgentForbiddenError, type AgentNotFoundError, } from './agent.service';
|
|
8
|
+
export { ApprovalCopywriterService, type ApprovalCopyBundle as ApprovalCopywriterBundle, type ApprovalCopywriterInput, } from './approval-copywriter.service';
|
|
8
9
|
export { OrchestratorService, OrchestratorError, type OrchestratorServiceOptions, } from './orchestrator.service';
|
|
9
10
|
export { AgentJobWorker } from './agent-job.worker';
|
|
10
11
|
export { ChatTokenService, ChatTokenError, type CreateChatTokenInput, } from './chat-token.service';
|
package/dist/services/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InMemoryAuthorizeStateStore = exports.ConnectorError = exports.ConnectorRegistryService = exports.OAuth2Service = exports.McpServerError = exports.McpServerService = exports.McpClientError = exports.McpClientService = exports.ChatTokenError = exports.ChatTokenService = exports.AgentJobWorker = exports.OrchestratorError = exports.OrchestratorService = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.InMemoryPreparedStreamStore = exports.PreparedStreamError = exports.PreparedStreamService = exports.isToolBlockedError = exports.isToolApprovalRequired = exports.ToolBlockedError = exports.ToolApprovalRequired = exports.AgentRunnerService = exports.ToolRegistryService = void 0;
|
|
3
|
+
exports.InMemoryAuthorizeStateStore = exports.ConnectorError = exports.ConnectorRegistryService = exports.OAuth2Service = exports.McpServerError = exports.McpServerService = exports.McpClientError = exports.McpClientService = exports.ChatTokenError = exports.ChatTokenService = exports.AgentJobWorker = exports.OrchestratorError = exports.OrchestratorService = exports.ApprovalCopywriterService = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.InMemoryPreparedStreamStore = exports.PreparedStreamError = exports.PreparedStreamService = exports.isToolBlockedError = exports.isToolApprovalRequired = exports.ToolBlockedError = exports.ToolApprovalRequired = exports.AgentRunnerService = exports.ToolRegistryService = void 0;
|
|
4
4
|
// Public service surface of @agentforge-io/core. AI runtime only — auth,
|
|
5
5
|
// identity, billing, infra, etc. have all moved to the host server.
|
|
6
6
|
var tool_registry_service_1 = require("./tool-registry.service");
|
|
@@ -27,6 +27,8 @@ Object.defineProperty(exports, "ConversationNotFoundError", { enumerable: true,
|
|
|
27
27
|
var agent_service_1 = require("./agent.service");
|
|
28
28
|
Object.defineProperty(exports, "AgentService", { enumerable: true, get: function () { return agent_service_1.AgentService; } });
|
|
29
29
|
Object.defineProperty(exports, "AgentForbiddenError", { enumerable: true, get: function () { return agent_service_1.AgentForbiddenError; } });
|
|
30
|
+
var approval_copywriter_service_1 = require("./approval-copywriter.service");
|
|
31
|
+
Object.defineProperty(exports, "ApprovalCopywriterService", { enumerable: true, get: function () { return approval_copywriter_service_1.ApprovalCopywriterService; } });
|
|
30
32
|
var orchestrator_service_1 = require("./orchestrator.service");
|
|
31
33
|
Object.defineProperty(exports, "OrchestratorService", { enumerable: true, get: function () { return orchestrator_service_1.OrchestratorService; } });
|
|
32
34
|
Object.defineProperty(exports, "OrchestratorError", { enumerable: true, get: function () { return orchestrator_service_1.OrchestratorError; } });
|
|
@@ -151,6 +151,20 @@ export interface ToolExecutionContext {
|
|
|
151
151
|
export type AnthropicMessage = Anthropic.MessageParam;
|
|
152
152
|
export type AnthropicTool = Anthropic.Tool;
|
|
153
153
|
export type AnthropicContentBlock = Anthropic.ContentBlock;
|
|
154
|
+
export interface ApprovalCopyBundle {
|
|
155
|
+
title: string;
|
|
156
|
+
body: string;
|
|
157
|
+
approveLabel: string;
|
|
158
|
+
approveBusyLabel: string;
|
|
159
|
+
denyLabel: string;
|
|
160
|
+
denyBusyLabel: string;
|
|
161
|
+
approvedPill: string;
|
|
162
|
+
deniedPill: string;
|
|
163
|
+
readOnlyHint: string;
|
|
164
|
+
blockedTitle: string;
|
|
165
|
+
blockedBody: string;
|
|
166
|
+
expiresPrefix: string;
|
|
167
|
+
}
|
|
154
168
|
export type StreamChunk = {
|
|
155
169
|
type: 'text_delta';
|
|
156
170
|
delta: string;
|
|
@@ -187,13 +201,21 @@ export type StreamChunk = {
|
|
|
187
201
|
approvalId: string;
|
|
188
202
|
toolName: string;
|
|
189
203
|
expiresAt: string;
|
|
204
|
+
connectorName?: string;
|
|
205
|
+
toolDescription?: string;
|
|
206
|
+
/** AI-generated microcopy for the bubble. Built server-side by
|
|
207
|
+
* the approval copywriter (a small Haiku call) so the language,
|
|
208
|
+
* tone and register match the conversation. Optional — when
|
|
209
|
+
* absent the client falls back to a minimal render derived from
|
|
210
|
+
* `connectorName` / `toolName`. */
|
|
211
|
+
copy?: ApprovalCopyBundle;
|
|
190
212
|
} | {
|
|
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
213
|
type: 'tool_blocked';
|
|
195
214
|
toolName: string;
|
|
196
215
|
reason?: string;
|
|
216
|
+
connectorName?: string;
|
|
217
|
+
toolDescription?: string;
|
|
218
|
+
copy?: ApprovalCopyBundle;
|
|
197
219
|
} | {
|
|
198
220
|
type: 'done';
|
|
199
221
|
messageId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.21",
|
|
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",
|