@agentforge-io/core 2.0.20 → 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.
@@ -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,12 @@ 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);
108
119
  /**
109
120
  * Look up the human-friendly connector name + tool description for a
110
121
  * given tool slug. Powers the friendly copy in `awaiting_approval` /
@@ -21,13 +21,19 @@ 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;
31
37
  }
32
38
  /**
33
39
  * Look up the human-friendly connector name + tool description for a
@@ -298,6 +304,14 @@ class AgentService {
298
304
  // context.
299
305
  if ((0, tool_approval_gate_1.isToolApprovalRequired)(err)) {
300
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
+ });
301
315
  yield {
302
316
  type: 'awaiting_approval',
303
317
  approvalId: err.approvalId,
@@ -305,17 +319,19 @@ class AgentService {
305
319
  expiresAt: err.expiresAt,
306
320
  connectorName: ctx?.connectorName,
307
321
  toolDescription: ctx?.toolDescription,
322
+ copy,
308
323
  };
309
324
  await this.conversations.addMessage({
310
325
  conversationId: params.conversationId,
311
326
  userId: params.userId,
312
327
  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.`,
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.`),
319
335
  metadata: {
320
336
  kind: 'awaiting_approval',
321
337
  approvalId: err.approvalId,
@@ -323,32 +339,44 @@ class AgentService {
323
339
  expiresAt: err.expiresAt,
324
340
  connectorName: ctx?.connectorName,
325
341
  toolDescription: ctx?.toolDescription,
342
+ copy,
326
343
  },
327
344
  });
328
345
  return;
329
346
  }
330
347
  if ((0, tool_approval_gate_1.isToolBlockedError)(err)) {
331
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
+ });
332
357
  yield {
333
358
  type: 'tool_blocked',
334
359
  toolName: err.toolName,
335
360
  reason: err.reason,
336
361
  connectorName: ctx?.connectorName,
337
362
  toolDescription: ctx?.toolDescription,
363
+ copy,
338
364
  };
339
365
  await this.conversations.addMessage({
340
366
  conversationId: params.conversationId,
341
367
  userId: params.userId,
342
368
  role: 'assistant',
343
- content: ctx?.connectorName
344
- ? `No puedo usar ${ctx.connectorName} en esta cuenta.`
345
- : `No puedo usar esa herramienta en esta cuenta.`,
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.`),
346
373
  metadata: {
347
374
  kind: 'tool_blocked',
348
375
  toolName: err.toolName,
349
376
  reason: err.reason,
350
377
  connectorName: ctx?.connectorName,
351
378
  toolDescription: ctx?.toolDescription,
379
+ copy,
352
380
  },
353
381
  });
354
382
  return;
@@ -429,6 +457,20 @@ class AgentService {
429
457
  }
430
458
  }
431
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
+ }
432
474
  /**
433
475
  * Map a persisted `AgentRecord` to the runtime `AgentDefinition` the runner
434
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
+ }
@@ -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';
@@ -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,25 +201,21 @@ export type StreamChunk = {
187
201
  approvalId: string;
188
202
  toolName: string;
189
203
  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
204
  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
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;
200
212
  } | {
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
213
  type: 'tool_blocked';
205
214
  toolName: string;
206
215
  reason?: string;
207
216
  connectorName?: string;
208
217
  toolDescription?: string;
218
+ copy?: ApprovalCopyBundle;
209
219
  } | {
210
220
  type: 'done';
211
221
  messageId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/core",
3
- "version": "2.0.20",
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",