@agentforge-io/core 2.3.1 → 3.0.1
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/builtins/current-time.tool.d.ts +8 -5
- package/dist/builtins/current-time.tool.js +24 -12
- package/dist/index.d.ts +1 -1
- package/dist/services/agent.service.d.ts +42 -0
- package/dist/services/agent.service.js +32 -0
- package/dist/services/orchestrator.service.js +47 -3
- package/dist/types/agent.types.d.ts +11 -0
- package/package.json +2 -2
|
@@ -6,12 +6,15 @@ import type { AgentToolDefinition } from '../types/agent.types';
|
|
|
6
6
|
* date?" / "what time is it in Bogotá?" answers drift to either the
|
|
7
7
|
* training cutoff (months off) or the host server's UTC, neither of
|
|
8
8
|
* which match the user's expectations. This tool anchors every time
|
|
9
|
-
* question to the
|
|
9
|
+
* question to the right IANA timezone via a fallback chain:
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 1. The explicit `timezone` argument if the model passed one.
|
|
12
|
+
* 2. `context.agent.timezone` — set by the runner from the agent
|
|
13
|
+
* record (per-agent override).
|
|
14
|
+
* 3. `context.tenant.timezone` — set by the host's TenantResolver,
|
|
15
|
+
* so the workspace default covers every agent that hasn't been
|
|
16
|
+
* individually configured.
|
|
17
|
+
* 4. `"UTC"` as a last-resort floor so the call always succeeds.
|
|
15
18
|
*
|
|
16
19
|
* Output is a compact JSON object — easier for the model to quote
|
|
17
20
|
* specific pieces (just the weekday, just the ISO) than a natural-
|
|
@@ -9,12 +9,15 @@ exports.currentTimeTool = currentTimeTool;
|
|
|
9
9
|
* date?" / "what time is it in Bogotá?" answers drift to either the
|
|
10
10
|
* training cutoff (months off) or the host server's UTC, neither of
|
|
11
11
|
* which match the user's expectations. This tool anchors every time
|
|
12
|
-
* question to the
|
|
12
|
+
* question to the right IANA timezone via a fallback chain:
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* 1. The explicit `timezone` argument if the model passed one.
|
|
15
|
+
* 2. `context.agent.timezone` — set by the runner from the agent
|
|
16
|
+
* record (per-agent override).
|
|
17
|
+
* 3. `context.tenant.timezone` — set by the host's TenantResolver,
|
|
18
|
+
* so the workspace default covers every agent that hasn't been
|
|
19
|
+
* individually configured.
|
|
20
|
+
* 4. `"UTC"` as a last-resort floor so the call always succeeds.
|
|
18
21
|
*
|
|
19
22
|
* Output is a compact JSON object — easier for the model to quote
|
|
20
23
|
* specific pieces (just the weekday, just the ISO) than a natural-
|
|
@@ -25,18 +28,27 @@ function currentTimeTool() {
|
|
|
25
28
|
return {
|
|
26
29
|
name: exports.CURRENT_TIME_TOOL_NAME,
|
|
27
30
|
alwaysOn: true,
|
|
28
|
-
description:
|
|
29
|
-
'
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
'
|
|
31
|
+
description: 'Look up the current wall-clock date and time silently, for use in ' +
|
|
32
|
+
'your own reasoning. Call this whenever an answer depends on knowing ' +
|
|
33
|
+
'the real-world moment — greetings keyed to time of day, deadlines ' +
|
|
34
|
+
'relative to "today", business-hours decisions, scheduling, or any ' +
|
|
35
|
+
'user phrasing like "now", "today", "this week". ' +
|
|
36
|
+
"The result is anchored to the agent's configured timezone (or the " +
|
|
37
|
+
'workspace default) unless the caller supplies an explicit ' +
|
|
38
|
+
'`timezone` argument. ' +
|
|
39
|
+
'IMPORTANT: do NOT recite the timestamp back to the user verbatim ' +
|
|
40
|
+
'unless they explicitly asked what time or date it is. Use the value ' +
|
|
41
|
+
'to inform your reply (e.g. choose "Good evening" vs "Good morning", ' +
|
|
42
|
+
'pick the right greeting, compute "in 3 days"); quoting the raw ' +
|
|
43
|
+
'clock reading in every message reads as robotic.',
|
|
33
44
|
inputSchema: {
|
|
34
45
|
type: 'object',
|
|
35
46
|
properties: {
|
|
36
47
|
timezone: {
|
|
37
48
|
type: 'string',
|
|
38
49
|
description: 'Optional IANA timezone (e.g. "America/Bogota", "Europe/Madrid", ' +
|
|
39
|
-
'"Asia/Tokyo"). Defaults to the agent\'s configured timezone
|
|
50
|
+
'"Asia/Tokyo"). Defaults to the agent\'s configured timezone, ' +
|
|
51
|
+
"then the tenant's default timezone, then UTC.",
|
|
40
52
|
},
|
|
41
53
|
},
|
|
42
54
|
additionalProperties: false,
|
|
@@ -45,7 +57,7 @@ function currentTimeTool() {
|
|
|
45
57
|
const requested = typeof input.timezone === 'string' && input.timezone.trim()
|
|
46
58
|
? input.timezone.trim()
|
|
47
59
|
: undefined;
|
|
48
|
-
const fallback = context.agent?.timezone || 'UTC';
|
|
60
|
+
const fallback = context.agent?.timezone || context.tenant?.timezone || 'UTC';
|
|
49
61
|
const tz = requested ?? fallback;
|
|
50
62
|
const now = new Date();
|
|
51
63
|
let iso;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export { JOB_QUEUE, type JobQueue, type JobStatus, type JobState, type JobContex
|
|
|
11
11
|
export { InMemoryJobQueue, type InMemoryJobQueueOptions, } from './adapters/job-queue/in-memory';
|
|
12
12
|
export * from './providers';
|
|
13
13
|
export * from './services';
|
|
14
|
-
export type { AgentResolver, AgentRecord, AgentResolveParams, } from './services/agent.service';
|
|
14
|
+
export type { AgentResolver, AgentRecord, AgentResolveParams, TenantResolver, TenantContext, } from './services/agent.service';
|
|
15
15
|
export { toAgentDefinition } from './services/agent.service';
|
|
16
16
|
export { createAgentForge, type CreateAgentForgeOptions, type AgentForgeContainer, type AgentForgeRepositories, type AgentForgeAdapters, } from './factory';
|
|
@@ -76,6 +76,31 @@ export interface AgentResolver {
|
|
|
76
76
|
findByIdForTenant(id: string, tenantId: string): Promise<AgentRecord | null>;
|
|
77
77
|
findById(id: string): Promise<AgentRecord | null>;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Tenant-level context the SDK passes to tools. The host owns tenant
|
|
81
|
+
* records; the SDK doesn't query them directly. Implement this
|
|
82
|
+
* interface in the host and register it via
|
|
83
|
+
* `AgentService.setTenantResolver(...)` so the runner can populate
|
|
84
|
+
* `ToolExecutionContext.tenant` for tools like `current_time` that
|
|
85
|
+
* need a workspace-wide default (e.g. timezone) when the agent itself
|
|
86
|
+
* has none configured.
|
|
87
|
+
*
|
|
88
|
+
* Returning `null` means "no tenant context available" — tools fall
|
|
89
|
+
* through to their own defaults (UTC for `current_time`). Returning a
|
|
90
|
+
* partial object is fine; the SDK only reads the fields tools ask for.
|
|
91
|
+
*/
|
|
92
|
+
export interface TenantResolver {
|
|
93
|
+
findById(tenantId: string): Promise<TenantContext | null>;
|
|
94
|
+
}
|
|
95
|
+
/** Minimal tenant-level context the SDK forwards to tools today. Add
|
|
96
|
+
* fields here as new tools start consuming tenant settings; keep them
|
|
97
|
+
* optional to preserve compatibility with hosts that don't populate
|
|
98
|
+
* every field. */
|
|
99
|
+
export interface TenantContext {
|
|
100
|
+
/** IANA timezone (e.g. `"America/Bogota"`). Used by `current_time`
|
|
101
|
+
* as the workspace default when the agent has none. */
|
|
102
|
+
timezone?: string;
|
|
103
|
+
}
|
|
79
104
|
export interface AgentNotFoundError extends Error {
|
|
80
105
|
status: 404;
|
|
81
106
|
}
|
|
@@ -141,6 +166,23 @@ export declare class AgentService {
|
|
|
141
166
|
* `delegate_to_*` synthetic tools fire. Standalone agents always
|
|
142
167
|
* go straight to the runner. */
|
|
143
168
|
orchestrator?: import("./orchestrator.service").OrchestratorService | undefined);
|
|
169
|
+
/** Optional host-supplied tenant resolver. When wired, every
|
|
170
|
+
* streamMessage / sendMessage attaches `tenant: { timezone }` (and
|
|
171
|
+
* whatever else TenantContext grows) to the ToolExecutionContext so
|
|
172
|
+
* tools like `current_time` can fall back to the workspace default.
|
|
173
|
+
* Left undefined for SDK consumers without a tenant model. */
|
|
174
|
+
private tenantResolver?;
|
|
175
|
+
/** Register the host's tenant resolver. Called once at module init
|
|
176
|
+
* (after construction so we don't bloat the positional constructor
|
|
177
|
+
* arg list for every SDK consumer). Idempotent — calling twice
|
|
178
|
+
* overwrites the previous resolver. */
|
|
179
|
+
setTenantResolver(resolver: TenantResolver): void;
|
|
180
|
+
/** Load the tenant context (currently just timezone) for an agent
|
|
181
|
+
* record. Returns `undefined` when the agent has no tenantId, no
|
|
182
|
+
* resolver is wired, or the lookup throws / returns null — in every
|
|
183
|
+
* one of those cases the tool falls back to its own defaults so a
|
|
184
|
+
* resolver outage never breaks the turn. */
|
|
185
|
+
private loadTenantContext;
|
|
144
186
|
/** Public re-export of `resolveExtraTools` keyed by agentId. The
|
|
145
187
|
* orchestrator uses this via `setExtraToolsResolver` to hydrate
|
|
146
188
|
* sub-agents' connector tools at delegation time. We synthesize a
|
|
@@ -64,6 +64,33 @@ class AgentService {
|
|
|
64
64
|
return this.resolveExtraToolsForAgent(agentId, userId);
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
+
/** Register the host's tenant resolver. Called once at module init
|
|
68
|
+
* (after construction so we don't bloat the positional constructor
|
|
69
|
+
* arg list for every SDK consumer). Idempotent — calling twice
|
|
70
|
+
* overwrites the previous resolver. */
|
|
71
|
+
setTenantResolver(resolver) {
|
|
72
|
+
this.tenantResolver = resolver;
|
|
73
|
+
}
|
|
74
|
+
/** Load the tenant context (currently just timezone) for an agent
|
|
75
|
+
* record. Returns `undefined` when the agent has no tenantId, no
|
|
76
|
+
* resolver is wired, or the lookup throws / returns null — in every
|
|
77
|
+
* one of those cases the tool falls back to its own defaults so a
|
|
78
|
+
* resolver outage never breaks the turn. */
|
|
79
|
+
async loadTenantContext(agent) {
|
|
80
|
+
if (!this.tenantResolver || !agent.tenantId)
|
|
81
|
+
return undefined;
|
|
82
|
+
try {
|
|
83
|
+
const ctx = await this.tenantResolver.findById(agent.tenantId);
|
|
84
|
+
if (!ctx)
|
|
85
|
+
return undefined;
|
|
86
|
+
return { timezone: ctx.timezone };
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Resolver outage shouldn't break the turn — the tool's own
|
|
90
|
+
// fallback (agent.timezone → UTC) still produces a sane answer.
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
67
94
|
/** Public re-export of `resolveExtraTools` keyed by agentId. The
|
|
68
95
|
* orchestrator uses this via `setExtraToolsResolver` to hydrate
|
|
69
96
|
* sub-agents' connector tools at delegation time. We synthesize a
|
|
@@ -313,12 +340,14 @@ class AgentService {
|
|
|
313
340
|
// ChatStreamController). Caller wins on name collisions so an
|
|
314
341
|
// explicit override always trumps an inherited connector tool.
|
|
315
342
|
const extraTools = mergeExtraTools(params.overrides?.extraTools, fromConnectors);
|
|
343
|
+
const tenant = await this.loadTenantContext(agent);
|
|
316
344
|
const response = await this.runner.run(agent, messages, {
|
|
317
345
|
userId: params.userId,
|
|
318
346
|
conversationId: params.conversationId,
|
|
319
347
|
agentId: conv.agentId,
|
|
320
348
|
messageId: 'sync',
|
|
321
349
|
agent: { timezone: agent.timezone },
|
|
350
|
+
...(tenant ? { tenant } : {}),
|
|
322
351
|
}, { ...(params.overrides ?? {}), extraTools });
|
|
323
352
|
await this.conversations.addMessage({
|
|
324
353
|
conversationId: params.conversationId,
|
|
@@ -379,6 +408,7 @@ class AgentService {
|
|
|
379
408
|
const filter = params.overrides?.extraToolsFilter;
|
|
380
409
|
const fromConnectors = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
|
|
381
410
|
const extraTools = mergeExtraTools(params.overrides?.extraTools, fromConnectors);
|
|
411
|
+
const tenant = await this.loadTenantContext(agent);
|
|
382
412
|
// Hoisted accumulators so the post-loop persistence (after the
|
|
383
413
|
// try) can see the final list. Defined here, populated inside
|
|
384
414
|
// the for-await loop below.
|
|
@@ -400,6 +430,7 @@ class AgentService {
|
|
|
400
430
|
agentId: conv.agentId,
|
|
401
431
|
messageId: 'streaming',
|
|
402
432
|
agent: { timezone: agent.timezone },
|
|
433
|
+
...(tenant ? { tenant } : {}),
|
|
403
434
|
})
|
|
404
435
|
: this.runner.stream(agent, messages, {
|
|
405
436
|
userId: params.userId,
|
|
@@ -407,6 +438,7 @@ class AgentService {
|
|
|
407
438
|
agentId: conv.agentId,
|
|
408
439
|
messageId: 'streaming',
|
|
409
440
|
agent: { timezone: agent.timezone },
|
|
441
|
+
...(tenant ? { tenant } : {}),
|
|
410
442
|
}, { ...(params.overrides ?? {}), extraTools });
|
|
411
443
|
// Accumulate tool_use / tool_result chunks during streaming so
|
|
412
444
|
// we can persist them on the assistant message row (line ~700).
|
|
@@ -142,6 +142,11 @@ class OrchestratorService {
|
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
this.logger.debug(`Streaming orchestrator "${agentId}" with subagents: ${agent.subAgents.join(', ')}`);
|
|
145
|
+
// Pre-resolve all sub-agents so `buildDelegationTools` sees their
|
|
146
|
+
// name/description/slug — without this, the agentsMap only has the
|
|
147
|
+
// orchestrator and the tool-name builder falls back to the UUID
|
|
148
|
+
// form, defeating the slug→name match the model relies on.
|
|
149
|
+
await Promise.all(agent.subAgents.map((id) => this.resolveAgentDynamic(id)));
|
|
145
150
|
const delegationTools = this.buildDelegationTools(agent);
|
|
146
151
|
const model = agent.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
|
|
147
152
|
const maxTokens = agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
|
|
@@ -213,8 +218,14 @@ class OrchestratorService {
|
|
|
213
218
|
if (block.type !== 'tool_use')
|
|
214
219
|
continue;
|
|
215
220
|
const delegateTool = delegationTools.find((t) => t.name === block.name);
|
|
216
|
-
if (!delegateTool)
|
|
221
|
+
if (!delegateTool) {
|
|
222
|
+
// The model called a tool name we didn't expose. Log loudly
|
|
223
|
+
// so we can spot drift between the prompt's slug references
|
|
224
|
+
// and the actual tool surface.
|
|
225
|
+
this.logger.warn(`Orchestrator "${agent.id}" called unknown tool "${block.name}". ` +
|
|
226
|
+
`Available tools: ${delegationTools.map((t) => t.name).join(', ')}`);
|
|
217
227
|
continue;
|
|
228
|
+
}
|
|
218
229
|
const { task } = block.input;
|
|
219
230
|
const { subAgentId } = delegateTool;
|
|
220
231
|
const subAgent = await this.resolveAgentDynamic(subAgentId);
|
|
@@ -333,6 +344,9 @@ class OrchestratorService {
|
|
|
333
344
|
}
|
|
334
345
|
async runOrchestratorLoop(orchestrator, messages, context) {
|
|
335
346
|
const delegations = [];
|
|
347
|
+
// Pre-resolve subagents so buildDelegationTools sees their
|
|
348
|
+
// slug/name/description (same fix as the stream path).
|
|
349
|
+
await Promise.all((orchestrator.subAgents ?? []).map((id) => this.resolveAgentDynamic(id)));
|
|
336
350
|
const delegationTools = this.buildDelegationTools(orchestrator);
|
|
337
351
|
const model = orchestrator.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
|
|
338
352
|
const maxTokens = orchestrator.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
|
|
@@ -439,13 +453,43 @@ class OrchestratorService {
|
|
|
439
453
|
}
|
|
440
454
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
441
455
|
buildDelegationTools(orchestrator) {
|
|
456
|
+
// First pass: collect the slug/uuid-name candidates so we can
|
|
457
|
+
// detect collisions across the team. Anthropic requires `tool.name`
|
|
458
|
+
// to be unique within a single request; two subagents with the
|
|
459
|
+
// same slug (rare but legal at the platform layer in different
|
|
460
|
+
// tenant scopes) must not collapse into one tool.
|
|
461
|
+
const namesUsed = new Set();
|
|
442
462
|
return (orchestrator.subAgents ?? []).map((subAgentId) => {
|
|
443
463
|
const sub = this.agentsMap.get(subAgentId);
|
|
464
|
+
const subSlug = sub?.slug;
|
|
465
|
+
// Tool name strategy: prefer `delegate_to_<slug>` because the model
|
|
466
|
+
// already SEES `@<slug>` in its system prompt; matching it to the
|
|
467
|
+
// tool name eliminates the two-namespace gap that caused
|
|
468
|
+
// wrong-tool calls. Fall back to a sanitized UUID-derived name
|
|
469
|
+
// when no slug is available or when the slug name is already
|
|
470
|
+
// taken by an earlier subagent in this build pass.
|
|
471
|
+
const slugName = subSlug
|
|
472
|
+
? `delegate_to_${subSlug.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 50)}`
|
|
473
|
+
: null;
|
|
474
|
+
const uuidName = `delegate_to_${subAgentId.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
475
|
+
let name;
|
|
476
|
+
if (slugName && !namesUsed.has(slugName)) {
|
|
477
|
+
name = slugName;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
name = uuidName;
|
|
481
|
+
}
|
|
482
|
+
namesUsed.add(name);
|
|
483
|
+
// Description still calls out the slug explicitly — belt and
|
|
484
|
+
// suspenders against models that pattern-match on text more
|
|
485
|
+
// than on tool names.
|
|
444
486
|
const description = sub
|
|
445
|
-
? `Delegate a task to the "${sub.name}" specialist agent
|
|
487
|
+
? `Delegate a task to the "${sub.name}" specialist agent` +
|
|
488
|
+
(subSlug ? ` (mention slug: @${subSlug})` : '') +
|
|
489
|
+
`. ${sub.description ?? ''}`
|
|
446
490
|
: `Delegate a task to subagent "${subAgentId}"`;
|
|
447
491
|
return {
|
|
448
|
-
name
|
|
492
|
+
name,
|
|
449
493
|
description,
|
|
450
494
|
subAgentId,
|
|
451
495
|
inputSchema: {
|
|
@@ -147,6 +147,17 @@ export interface ToolExecutionContext {
|
|
|
147
147
|
agent?: {
|
|
148
148
|
timezone?: string;
|
|
149
149
|
};
|
|
150
|
+
/**
|
|
151
|
+
* Tenant-level configuration the tool may consult as a fallback when
|
|
152
|
+
* the agent itself does not specify a value. `current_time` walks the
|
|
153
|
+
* chain `agent.timezone → tenant.timezone → 'UTC'` so a workspace
|
|
154
|
+
* default covers every agent that hasn't been individually configured.
|
|
155
|
+
* Populated by the host's `TenantResolver` hook on AgentService;
|
|
156
|
+
* absent when no resolver is wired (legacy SDK consumers).
|
|
157
|
+
*/
|
|
158
|
+
tenant?: {
|
|
159
|
+
timezone?: string;
|
|
160
|
+
};
|
|
150
161
|
}
|
|
151
162
|
export type AnthropicMessage = Anthropic.MessageParam;
|
|
152
163
|
export type AnthropicTool = Anthropic.Tool;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/core",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Framework-free AI runtime SDK. Owns: agent loop (
|
|
3
|
+
"version": "3.0.1",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.ts",
|