@agentforge-io/core 2.3.1 → 3.0.0

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.
@@ -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. ${sub.description ?? ''}`
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: `delegate_to_${subAgentId.replace(/[^a-zA-Z0-9]/g, '_')}`,
492
+ name,
449
493
  description,
450
494
  subAgentId,
451
495
  inputSchema: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentforge-io/core",
3
- "version": "2.3.1",
4
- "description": "Framework-free AI runtime SDK. Owns: agent loop (pluggable LLM provider — Anthropic by default, LangChain-backed providers as drop-ins), conversations, tools, streaming, agent-job queue, SdkHooks. Identity, billing, infra (email/uploads/secrets) live in the host's modules not here.",
3
+ "version": "3.0.0",
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 \u2014 not here.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -33,4 +33,4 @@
33
33
  "tsx": "^4.19.0",
34
34
  "typescript": "^5.0.0"
35
35
  }
36
- }
36
+ }