@agentforge-io/core 2.3.0 → 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.
|
@@ -379,6 +379,11 @@ class AgentService {
|
|
|
379
379
|
const filter = params.overrides?.extraToolsFilter;
|
|
380
380
|
const fromConnectors = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
|
|
381
381
|
const extraTools = mergeExtraTools(params.overrides?.extraTools, fromConnectors);
|
|
382
|
+
// Hoisted accumulators so the post-loop persistence (after the
|
|
383
|
+
// try) can see the final list. Defined here, populated inside
|
|
384
|
+
// the for-await loop below.
|
|
385
|
+
const toolCallStartByUseId = new Map();
|
|
386
|
+
const accumulatedToolCalls = [];
|
|
382
387
|
try {
|
|
383
388
|
// Team orchestrators route through OrchestratorService.stream()
|
|
384
389
|
// so the synthetic `delegate_to_*` tools the orchestrator was
|
|
@@ -403,11 +408,56 @@ class AgentService {
|
|
|
403
408
|
messageId: 'streaming',
|
|
404
409
|
agent: { timezone: agent.timezone },
|
|
405
410
|
}, { ...(params.overrides ?? {}), extraTools });
|
|
411
|
+
// Accumulate tool_use / tool_result chunks during streaming so
|
|
412
|
+
// we can persist them on the assistant message row (line ~700).
|
|
413
|
+
// Without this, `getHistory` returns the assistant's text but
|
|
414
|
+
// loses the tool calls — which means clients that render
|
|
415
|
+
// proposal cards (Recording Assist) or generic tool rows from
|
|
416
|
+
// history have nothing to rehydrate. FIFO matching mirrors the
|
|
417
|
+
// runner's heuristic so the shape stays consistent whether the
|
|
418
|
+
// turn streamed or used `run()`.
|
|
406
419
|
for await (const chunk of stream) {
|
|
407
420
|
if (chunk.type === 'text_delta')
|
|
408
421
|
fullContent += chunk.delta;
|
|
409
422
|
if (chunk.type === 'usage')
|
|
410
423
|
finalUsage = chunk.usage;
|
|
424
|
+
if (chunk.type === 'tool_use_start') {
|
|
425
|
+
toolCallStartByUseId.set(chunk.toolUseId, {
|
|
426
|
+
name: chunk.toolName,
|
|
427
|
+
start: Date.now(),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (chunk.type === 'tool_result') {
|
|
431
|
+
// Match by toolUseId when present; otherwise FIFO on toolName
|
|
432
|
+
// (matches AgentRunner.run's heuristic). Older runners that
|
|
433
|
+
// don't emit `toolUseId` on result chunks still produce a
|
|
434
|
+
// usable record.
|
|
435
|
+
let entry = null;
|
|
436
|
+
const useId = chunk.toolUseId;
|
|
437
|
+
if (useId && toolCallStartByUseId.has(useId)) {
|
|
438
|
+
const v = toolCallStartByUseId.get(useId);
|
|
439
|
+
entry = { id: useId, name: v.name, start: v.start };
|
|
440
|
+
toolCallStartByUseId.delete(useId);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
const fifo = Array.from(toolCallStartByUseId.entries()).find(([, v]) => v.name === chunk.toolName);
|
|
444
|
+
if (fifo) {
|
|
445
|
+
entry = { id: fifo[0], name: fifo[1].name, start: fifo[1].start };
|
|
446
|
+
toolCallStartByUseId.delete(fifo[0]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (entry) {
|
|
450
|
+
accumulatedToolCalls.push({
|
|
451
|
+
toolName: entry.name,
|
|
452
|
+
toolUseId: entry.id,
|
|
453
|
+
input: {},
|
|
454
|
+
output: typeof chunk.result === 'string'
|
|
455
|
+
? chunk.result
|
|
456
|
+
: JSON.stringify(chunk.result),
|
|
457
|
+
durationMs: Date.now() - entry.start,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
411
461
|
yield chunk;
|
|
412
462
|
}
|
|
413
463
|
}
|
|
@@ -507,6 +557,12 @@ class AgentService {
|
|
|
507
557
|
role: 'assistant',
|
|
508
558
|
content: fullContent,
|
|
509
559
|
usage: finalUsage,
|
|
560
|
+
// Persist the accumulated tool calls so `getHistory` can
|
|
561
|
+
// surface them on reload — without this, proposal cards
|
|
562
|
+
// (Recording Assist) and other tool-result-driven UI
|
|
563
|
+
// disappear after refresh. Empty arrays drop to undefined
|
|
564
|
+
// so we don't pollute the column with `[]`.
|
|
565
|
+
toolCalls: accumulatedToolCalls.length > 0 ? accumulatedToolCalls : undefined,
|
|
510
566
|
});
|
|
511
567
|
const now = new Date();
|
|
512
568
|
await this.dispatchUsage({
|
|
@@ -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: {
|
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.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
|
+
}
|