@agentforge-io/core 2.0.24 → 2.1.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.
Files changed (88) hide show
  1. package/dist/factory.js +56 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +7 -1
  4. package/dist/services/agent-runner.service.js +18 -4
  5. package/dist/services/agent.service.d.ts +21 -1
  6. package/dist/services/agent.service.js +42 -8
  7. package/dist/services/orchestrator.service.d.ts +40 -1
  8. package/dist/services/orchestrator.service.js +220 -0
  9. package/dist/types/agent.types.d.ts +31 -6
  10. package/package.json +1 -1
  11. package/dist/adapters/billing/billing-adapter.interface.d.ts +0 -41
  12. package/dist/adapters/billing/billing-adapter.interface.js +0 -5
  13. package/dist/adapters/billing/stripe/stripe.adapter.d.ts +0 -30
  14. package/dist/adapters/billing/stripe/stripe.adapter.js +0 -122
  15. package/dist/adapters/email/email-adapter.interface.d.ts +0 -25
  16. package/dist/adapters/email/email-adapter.interface.js +0 -6
  17. package/dist/adapters/email/noop.adapter.d.ts +0 -10
  18. package/dist/adapters/email/noop.adapter.js +0 -15
  19. package/dist/adapters/email/resend.adapter.d.ts +0 -8
  20. package/dist/adapters/email/resend.adapter.js +0 -39
  21. package/dist/adapters/upload/noop.adapter.d.ts +0 -9
  22. package/dist/adapters/upload/noop.adapter.js +0 -14
  23. package/dist/adapters/upload/s3.adapter.d.ts +0 -38
  24. package/dist/adapters/upload/s3.adapter.js +0 -69
  25. package/dist/adapters/upload/upload-adapter.interface.d.ts +0 -37
  26. package/dist/adapters/upload/upload-adapter.interface.js +0 -15
  27. package/dist/billing/index.d.ts +0 -12
  28. package/dist/billing/index.js +0 -28
  29. package/dist/domain/agent.d.ts +0 -59
  30. package/dist/domain/agent.js +0 -2
  31. package/dist/domain/api-key.d.ts +0 -28
  32. package/dist/domain/api-key.js +0 -2
  33. package/dist/domain/auth-identity.d.ts +0 -10
  34. package/dist/domain/auth-identity.js +0 -2
  35. package/dist/domain/email-token.d.ts +0 -11
  36. package/dist/domain/email-token.js +0 -2
  37. package/dist/domain/external-user.d.ts +0 -23
  38. package/dist/domain/external-user.js +0 -2
  39. package/dist/domain/plan.d.ts +0 -20
  40. package/dist/domain/plan.js +0 -2
  41. package/dist/domain/platform-secret.d.ts +0 -24
  42. package/dist/domain/platform-secret.js +0 -8
  43. package/dist/domain/refresh-token.d.ts +0 -15
  44. package/dist/domain/refresh-token.js +0 -2
  45. package/dist/domain/subscription.d.ts +0 -21
  46. package/dist/domain/subscription.js +0 -2
  47. package/dist/domain/tenant.d.ts +0 -21
  48. package/dist/domain/tenant.js +0 -2
  49. package/dist/domain/usage-record.d.ts +0 -15
  50. package/dist/domain/usage-record.js +0 -2
  51. package/dist/domain/user.d.ts +0 -43
  52. package/dist/domain/user.js +0 -2
  53. package/dist/services/agent-config.service.d.ts +0 -45
  54. package/dist/services/agent-config.service.js +0 -114
  55. package/dist/services/api-key.service.d.ts +0 -41
  56. package/dist/services/api-key.service.js +0 -80
  57. package/dist/services/auth.service.d.ts +0 -133
  58. package/dist/services/auth.service.js +0 -411
  59. package/dist/services/billing.service.d.ts +0 -67
  60. package/dist/services/billing.service.js +0 -254
  61. package/dist/services/email-templates.d.ts +0 -18
  62. package/dist/services/email-templates.js +0 -39
  63. package/dist/services/email.service.d.ts +0 -26
  64. package/dist/services/email.service.js +0 -42
  65. package/dist/services/errors.d.ts +0 -7
  66. package/dist/services/errors.js +0 -27
  67. package/dist/services/oauth.service.d.ts +0 -73
  68. package/dist/services/oauth.service.js +0 -174
  69. package/dist/services/plan.service.d.ts +0 -54
  70. package/dist/services/plan.service.js +0 -120
  71. package/dist/services/refresh-token.service.d.ts +0 -38
  72. package/dist/services/refresh-token.service.js +0 -73
  73. package/dist/services/secrets/crypto.d.ts +0 -37
  74. package/dist/services/secrets/crypto.js +0 -110
  75. package/dist/services/secrets/known-keys.d.ts +0 -38
  76. package/dist/services/secrets/known-keys.js +0 -50
  77. package/dist/services/secrets.service.d.ts +0 -91
  78. package/dist/services/secrets.service.js +0 -193
  79. package/dist/services/tenant-billing.service.d.ts +0 -121
  80. package/dist/services/tenant-billing.service.js +0 -290
  81. package/dist/services/tenant.service.d.ts +0 -54
  82. package/dist/services/tenant.service.js +0 -96
  83. package/dist/services/upload.service.d.ts +0 -37
  84. package/dist/services/upload.service.js +0 -84
  85. package/dist/services/usage.service.d.ts +0 -34
  86. package/dist/services/usage.service.js +0 -108
  87. package/dist/types/billing.types.d.ts +0 -82
  88. package/dist/types/billing.types.js +0 -3
package/dist/factory.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.createAgentForge = createAgentForge;
4
37
  const in_memory_1 = require("./adapters/rate-limiter/in-memory");
@@ -22,9 +55,29 @@ function createAgentForge(opts) {
22
55
  // ─── Tool registry + runner + orchestrator ───────────────────────────────
23
56
  const toolRegistry = new tool_registry_service_1.ToolRegistryService({ logger, initialTools: tools });
24
57
  const runner = new agent_runner_service_1.AgentRunnerService(config.anthropic, toolRegistry, { logger });
58
+ // Bridge the host-supplied AgentResolver into the orchestrator's
59
+ // dynamic-lookup hook. The orchestrator only needs `AgentDefinition`,
60
+ // so we wrap the resolver's record-shaped result with the same
61
+ // `toAgentDefinition` adapter the AgentService uses internally.
62
+ // When no resolver is wired the orchestrator only sees the static
63
+ // `config.agents` map — preserving legacy behaviour.
64
+ const orchestratorResolver = adapters.agentResolver
65
+ ? async (id) => {
66
+ const rec = await adapters.agentResolver.findById(id);
67
+ if (!rec || !rec.isActive)
68
+ return null;
69
+ // `toAgentDefinition` is the internal adapter — exposing it
70
+ // via the agent.service module keeps it private to the SDK.
71
+ // We dynamically require it here to dodge a circular import
72
+ // between factory.ts and agent.service.ts.
73
+ const { toAgentDefinition } = await Promise.resolve().then(() => __importStar(require('./services/agent.service')));
74
+ return toAgentDefinition(rec);
75
+ }
76
+ : undefined;
25
77
  const orchestrator = new orchestrator_service_1.OrchestratorService(config.anthropic, runner, {
26
78
  agents: config.agents ?? [],
27
79
  logger,
80
+ resolveAgent: orchestratorResolver,
28
81
  });
29
82
  // ─── Prepared-stream store + service ─────────────────────────────────────
30
83
  const preparedStreamStore = adapters.preparedStreamStore ?? new in_memory_prepared_stream_store_1.InMemoryPreparedStreamStore();
@@ -33,7 +86,9 @@ function createAgentForge(opts) {
33
86
  const rateLimiter = adapters.rateLimiter ?? new in_memory_1.InMemoryRateLimiter();
34
87
  // ─── Conversations + agents ──────────────────────────────────────────────
35
88
  const conversations = new conversation_service_1.ConversationService(repositories.conversations, repositories.messages);
36
- const agents = new agent_service_1.AgentService(config.agents ?? [], runner, conversations, adapters.agentResolver, hooks);
89
+ const agents = new agent_service_1.AgentService(config.agents ?? [], runner, conversations, adapters.agentResolver, hooks, undefined, // connectorRegistry — wired by Nest binding when present
90
+ undefined, // copywriter — same
91
+ orchestrator);
37
92
  // ─── Background-job worker + queue (in-memory default) ───────────────────
38
93
  const agentJobWorker = new agent_job_worker_1.AgentJobWorker(orchestrator, conversations, {
39
94
  logger,
package/dist/index.d.ts CHANGED
@@ -11,4 +11,5 @@ 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 './services';
13
13
  export type { AgentResolver, AgentRecord, AgentResolveParams, } from './services/agent.service';
14
+ export { toAgentDefinition } from './services/agent.service';
14
15
  export { createAgentForge, type CreateAgentForgeOptions, type AgentForgeContainer, type AgentForgeRepositories, type AgentForgeAdapters, } from './factory';
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
27
27
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
28
28
  };
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.createAgentForge = exports.InMemoryJobQueue = exports.JOB_QUEUE = exports.RedisRateLimiter = exports.InMemoryRateLimiter = exports.RATE_LIMITER = exports.PREPARED_STREAM_STORE = exports.CURRENT_USER = exports.AGENT_QUEUE_NAME = exports.AGENT_FORGE_CONFIG = void 0;
30
+ exports.createAgentForge = exports.toAgentDefinition = exports.InMemoryJobQueue = exports.JOB_QUEUE = exports.RedisRateLimiter = exports.InMemoryRateLimiter = exports.RATE_LIMITER = exports.PREPARED_STREAM_STORE = exports.CURRENT_USER = exports.AGENT_QUEUE_NAME = exports.AGENT_FORGE_CONFIG = void 0;
31
31
  // ─── Constants ──────────────────────────────────────────────────────────────
32
32
  var constants_1 = require("./constants");
33
33
  Object.defineProperty(exports, "AGENT_FORGE_CONFIG", { enumerable: true, get: function () { return constants_1.AGENT_FORGE_CONFIG; } });
@@ -54,6 +54,12 @@ var in_memory_2 = require("./adapters/job-queue/in-memory");
54
54
  Object.defineProperty(exports, "InMemoryJobQueue", { enumerable: true, get: function () { return in_memory_2.InMemoryJobQueue; } });
55
55
  // ─── Services (framework-free) ──────────────────────────────────────────────
56
56
  __exportStar(require("./services"), exports);
57
+ // `toAgentDefinition` is the adapter from the host's `AgentRecord` shape
58
+ // to the SDK's runtime `AgentDefinition`. Exposed so the Nest binding
59
+ // can bridge an `AgentResolver` into the orchestrator's dynamic-lookup
60
+ // hook for Team flows.
61
+ var agent_service_1 = require("./services/agent.service");
62
+ Object.defineProperty(exports, "toAgentDefinition", { enumerable: true, get: function () { return agent_service_1.toAgentDefinition; } });
57
63
  // ─── Container factory ──────────────────────────────────────────────────────
58
64
  var factory_1 = require("./factory");
59
65
  Object.defineProperty(exports, "createAgentForge", { enumerable: true, get: function () { return factory_1.createAgentForge; } });
@@ -33,7 +33,13 @@ class AgentRunnerService {
33
33
  const runnerDefault = this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
34
34
  const baseModel = overrides?.model ?? agent.model ?? runnerDefault;
35
35
  const maxTokens = overrides?.maxTokens ?? agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
36
- const temperature = overrides?.temperature ?? agent.temperature ?? 1;
36
+ // Anthropic's newer models (Sonnet 4.6+, Haiku 4.5+) reject
37
+ // `temperature` when tools are present — they auto-tune sampling for
38
+ // tool use. Only forward it when the operator/caller declared one
39
+ // explicitly; never inject a default. Old models that required it
40
+ // accept its absence too (they fall back to their own internal
41
+ // default of 1.0).
42
+ const temperature = overrides?.temperature ?? agent.temperature;
37
43
  const { tools, extras } = this.buildToolList(agent, overrides);
38
44
  const systemPrompt = this.buildSystemPrompt(agent, tools, overrides);
39
45
  const toolCalls = [];
@@ -71,7 +77,9 @@ class AgentRunnerService {
71
77
  const response = await this.client.messages.create({
72
78
  model,
73
79
  max_tokens: maxTokens,
74
- temperature,
80
+ // Only include temperature when explicitly declared — newer
81
+ // models 400 on `temperature` when tools are present.
82
+ ...(typeof temperature === 'number' ? { temperature } : {}),
75
83
  system: systemPrompt,
76
84
  messages: currentMessages,
77
85
  tools: tools,
@@ -160,7 +168,13 @@ class AgentRunnerService {
160
168
  const runnerDefault = this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
161
169
  const baseModel = overrides?.model ?? agent.model ?? runnerDefault;
162
170
  const maxTokens = overrides?.maxTokens ?? agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
163
- const temperature = overrides?.temperature ?? agent.temperature ?? 1;
171
+ // Anthropic's newer models (Sonnet 4.6+, Haiku 4.5+) reject
172
+ // `temperature` when tools are present — they auto-tune sampling for
173
+ // tool use. Only forward it when the operator/caller declared one
174
+ // explicitly; never inject a default. Old models that required it
175
+ // accept its absence too (they fall back to their own internal
176
+ // default of 1.0).
177
+ const temperature = overrides?.temperature ?? agent.temperature;
164
178
  const { tools, extras } = this.buildToolList(agent, overrides);
165
179
  const systemPrompt = this.buildSystemPrompt(agent, tools, overrides);
166
180
  let currentMessages = [...messages];
@@ -182,7 +196,7 @@ class AgentRunnerService {
182
196
  const stream = this.client.messages.stream({
183
197
  model,
184
198
  max_tokens: maxTokens,
185
- temperature,
199
+ ...(typeof temperature === 'number' ? { temperature } : {}),
186
200
  system: systemPrompt,
187
201
  messages: currentMessages,
188
202
  tools: tools,
@@ -115,6 +115,11 @@ export declare class AgentService {
115
115
  * meta-only render on the client when unwired or when generation
116
116
  * fails. */
117
117
  private readonly copywriter?;
118
+ /** When wired, agents flagged `canOrchestrate=true` route through
119
+ * the orchestrator's `stream()` instead of the bare runner so the
120
+ * `delegate_to_*` synthetic tools fire. Standalone agents always
121
+ * go straight to the runner. */
122
+ private readonly orchestrator?;
118
123
  constructor(agents: AgentDefinition[], runner: AgentRunnerService, conversations: ConversationService,
119
124
  /** When wired, agents created via the admin UI are looked up here first;
120
125
  * the hardcoded `agents` array remains a fallback for legacy installs. */
@@ -130,7 +135,12 @@ export declare class AgentService {
130
135
  * microcopy shown in the in-chat approval bubble. Falls back to a
131
136
  * meta-only render on the client when unwired or when generation
132
137
  * fails. */
133
- copywriter?: ApprovalCopywriterService | undefined);
138
+ copywriter?: ApprovalCopywriterService | undefined,
139
+ /** When wired, agents flagged `canOrchestrate=true` route through
140
+ * the orchestrator's `stream()` instead of the bare runner so the
141
+ * `delegate_to_*` synthetic tools fire. Standalone agents always
142
+ * go straight to the runner. */
143
+ orchestrator?: import("./orchestrator.service").OrchestratorService | undefined);
134
144
  /**
135
145
  * Look up the human-friendly connector name + tool description for a
136
146
  * given tool slug. Powers the friendly copy in `awaiting_approval` /
@@ -227,3 +237,13 @@ export declare class AgentService {
227
237
  chunk: StreamChunk;
228
238
  }>;
229
239
  }
240
+ /**
241
+ * Map a persisted `AgentRecord` to the runtime `AgentDefinition` the runner
242
+ * expects. The `context` column (plain-text knowledge) is prepended to the
243
+ * system prompt — the cheapest path before RAG is implemented.
244
+ *
245
+ * Extra host fields (`appearance`, `slug`) are passed through as opaque
246
+ * properties so callers like `PublicChatController` can surface them to the
247
+ * widget. The runner ignores anything it doesn't recognize.
248
+ */
249
+ export declare function toAgentDefinition(record: AgentRecord): AgentDefinition;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AgentService = exports.AgentForbiddenError = void 0;
4
+ exports.toAgentDefinition = toAgentDefinition;
4
5
  const tool_approval_gate_1 = require("./tool-approval-gate");
5
6
  class AgentForbiddenError extends Error {
6
7
  constructor(reason) {
@@ -26,7 +27,12 @@ class AgentService {
26
27
  * microcopy shown in the in-chat approval bubble. Falls back to a
27
28
  * meta-only render on the client when unwired or when generation
28
29
  * fails. */
29
- copywriter) {
30
+ copywriter,
31
+ /** When wired, agents flagged `canOrchestrate=true` route through
32
+ * the orchestrator's `stream()` instead of the bare runner so the
33
+ * `delegate_to_*` synthetic tools fire. Standalone agents always
34
+ * go straight to the runner. */
35
+ orchestrator) {
30
36
  this.agents = agents;
31
37
  this.runner = runner;
32
38
  this.conversations = conversations;
@@ -34,6 +40,7 @@ class AgentService {
34
40
  this.hooks = hooks;
35
41
  this.connectorRegistry = connectorRegistry;
36
42
  this.copywriter = copywriter;
43
+ this.orchestrator = orchestrator;
37
44
  }
38
45
  /**
39
46
  * Look up the human-friendly connector name + tool description for a
@@ -286,13 +293,30 @@ class AgentService {
286
293
  const fromConnectors = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
287
294
  const extraTools = mergeExtraTools(params.overrides?.extraTools, fromConnectors);
288
295
  try {
289
- for await (const chunk of this.runner.stream(agent, messages, {
290
- userId: params.userId,
291
- conversationId: params.conversationId,
292
- agentId: conv.agentId,
293
- messageId: 'streaming',
294
- agent: { timezone: agent.timezone },
295
- }, { ...(params.overrides ?? {}), extraTools })) {
296
+ // Team orchestrators route through OrchestratorService.stream()
297
+ // so the synthetic `delegate_to_*` tools the orchestrator was
298
+ // built with can fire. Standalone agents (or any orchestrator
299
+ // without an SDK that wired the service) go straight to the
300
+ // runner — the legacy path stays untouched for them.
301
+ const useOrchestrator = this.orchestrator &&
302
+ agent.canOrchestrate &&
303
+ (agent.subAgents?.length ?? 0) > 0;
304
+ const stream = useOrchestrator
305
+ ? this.orchestrator.stream(agent.id, messages, {
306
+ userId: params.userId,
307
+ conversationId: params.conversationId,
308
+ agentId: conv.agentId,
309
+ messageId: 'streaming',
310
+ agent: { timezone: agent.timezone },
311
+ })
312
+ : this.runner.stream(agent, messages, {
313
+ userId: params.userId,
314
+ conversationId: params.conversationId,
315
+ agentId: conv.agentId,
316
+ messageId: 'streaming',
317
+ agent: { timezone: agent.timezone },
318
+ }, { ...(params.overrides ?? {}), extraTools });
319
+ for await (const chunk of stream) {
296
320
  if (chunk.type === 'text_delta')
297
321
  fullContent += chunk.delta;
298
322
  if (chunk.type === 'usage')
@@ -511,6 +535,16 @@ function toAgentDefinition(record) {
511
535
  // from the host's resolver to the SDK and every agent reads as
512
536
  // `undefined` (i.e. public).
513
537
  visibility: record.visibility,
538
+ // Team orchestrators need these to drive the delegation tool list
539
+ // the runner injects at run time. Standalone agents won't have
540
+ // them; the orchestrator service treats absence as "not an
541
+ // orchestrator" and falls through to the runner directly.
542
+ ...(extra.canOrchestrate !== undefined
543
+ ? { canOrchestrate: extra.canOrchestrate }
544
+ : {}),
545
+ ...(Array.isArray(extra.subAgents)
546
+ ? { subAgents: extra.subAgents }
547
+ : {}),
514
548
  ...(record.slug !== undefined ? { slug: record.slug } : {}),
515
549
  ...(extra.appearance !== undefined ? { appearance: extra.appearance } : {}),
516
550
  };
@@ -1,4 +1,4 @@
1
- import type { AgentResponse, AnthropicMessage, SubAgentDelegation, ToolExecutionContext } from '../types/agent.types';
1
+ import type { AgentResponse, AnthropicMessage, StreamChunk, SubAgentDelegation, ToolExecutionContext } from '../types/agent.types';
2
2
  import type { AgentDefinition, AnthropicConfig } from '../types/config.types';
3
3
  import type { AgentRunnerService } from './agent-runner.service';
4
4
  import type { Logger } from './tool-registry.service';
@@ -13,6 +13,19 @@ export declare class OrchestratorError extends Error {
13
13
  export interface OrchestratorServiceOptions {
14
14
  agents: AgentDefinition[];
15
15
  logger?: Logger;
16
+ /**
17
+ * Optional dynamic resolver. When the orchestrator references a
18
+ * sub-agent that wasn't in the constructor's `agents[]` (typical for
19
+ * Team orchestrators whose members live in the database and change
20
+ * per-tenant), the service calls this to load it on demand. Returning
21
+ * `null` is treated as "member is gone" — the orchestrator emits an
22
+ * apology delegation_result and continues.
23
+ *
24
+ * The framework-free SDK doesn't know about the host's persistence,
25
+ * so this hook is the seam where the platform wires
26
+ * `AgentConfigService` + `toAgentDefinition` adapter in.
27
+ */
28
+ resolveAgent?(agentId: string): Promise<AgentDefinition | null> | AgentDefinition | null;
16
29
  }
17
30
  /**
18
31
  * Multi-agent workflows. Orchestrator agents can delegate tasks to specialized
@@ -29,7 +42,18 @@ export declare class OrchestratorService {
29
42
  private readonly agentsMap;
30
43
  private readonly client;
31
44
  private readonly logger;
45
+ private readonly resolveAgentHook?;
32
46
  constructor(anthropicConfig: AnthropicConfig, runner: AgentRunnerService, opts: OrchestratorServiceOptions);
47
+ /**
48
+ * Lookup with dynamic-resolver fallback. Hits the static map first
49
+ * (built from the constructor's `agents` list — covers the bootstrap
50
+ * use case), then falls back to the host-supplied `resolveAgent`
51
+ * hook (used by Team orchestrators whose members are loaded from
52
+ * the database per-tenant). Resolved agents are cached in the map
53
+ * for the lifetime of the service to avoid re-fetching across a
54
+ * multi-turn conversation.
55
+ */
56
+ private resolveAgentDynamic;
33
57
  /**
34
58
  * Run an agent. Orchestrators automatically get delegation tools injected.
35
59
  * Non-orchestrator agents fall straight through to the runner.
@@ -37,6 +61,21 @@ export declare class OrchestratorService {
37
61
  run(agentId: string, messages: AnthropicMessage[], context: ToolExecutionContext): Promise<AgentResponse & {
38
62
  delegations?: SubAgentDelegation[];
39
63
  }>;
64
+ /**
65
+ * Streaming variant. Orchestrators emit `delegation_start` /
66
+ * `delegation_result` chunks around each sub-agent invocation; the
67
+ * sub-agent's own chunks are forwarded byte-by-byte with their
68
+ * `actingAgentId` set so the client renders the member's avatar /
69
+ * name on the right bubble. Non-orchestrator agents short-circuit
70
+ * to the runner's stream.
71
+ *
72
+ * Implementation note: we drive the same Anthropic agentic loop as
73
+ * `runOrchestratorLoop` (no shortcut — the orchestrator's reasoning
74
+ * about WHO to delegate to is still a non-streamed messages.create
75
+ * call). The streaming part is the SUB-AGENT'S response, which is
76
+ * the part the visitor actually cares about seeing in real time.
77
+ */
78
+ stream(agentId: string, messages: AnthropicMessage[], context: ToolExecutionContext): AsyncGenerator<StreamChunk>;
40
79
  private runOrchestratorLoop;
41
80
  private buildDelegationTools;
42
81
  private getAgent;
@@ -39,10 +39,32 @@ class OrchestratorService {
39
39
  baseURL: anthropicConfig.baseURL,
40
40
  });
41
41
  this.logger = opts.logger ?? noopLogger;
42
+ this.resolveAgentHook = opts.resolveAgent;
42
43
  for (const agent of opts.agents) {
43
44
  this.agentsMap.set(agent.id, agent);
44
45
  }
45
46
  }
47
+ /**
48
+ * Lookup with dynamic-resolver fallback. Hits the static map first
49
+ * (built from the constructor's `agents` list — covers the bootstrap
50
+ * use case), then falls back to the host-supplied `resolveAgent`
51
+ * hook (used by Team orchestrators whose members are loaded from
52
+ * the database per-tenant). Resolved agents are cached in the map
53
+ * for the lifetime of the service to avoid re-fetching across a
54
+ * multi-turn conversation.
55
+ */
56
+ async resolveAgentDynamic(agentId) {
57
+ const cached = this.agentsMap.get(agentId);
58
+ if (cached)
59
+ return cached;
60
+ if (!this.resolveAgentHook)
61
+ return undefined;
62
+ const resolved = await this.resolveAgentHook(agentId);
63
+ if (!resolved)
64
+ return undefined;
65
+ this.agentsMap.set(resolved.id, resolved);
66
+ return resolved;
67
+ }
46
68
  /**
47
69
  * Run an agent. Orchestrators automatically get delegation tools injected.
48
70
  * Non-orchestrator agents fall straight through to the runner.
@@ -55,6 +77,204 @@ class OrchestratorService {
55
77
  this.logger.debug(`Running orchestrator "${agentId}" with subagents: ${agent.subAgents.join(', ')}`);
56
78
  return this.runOrchestratorLoop(agent, messages, context);
57
79
  }
80
+ /**
81
+ * Streaming variant. Orchestrators emit `delegation_start` /
82
+ * `delegation_result` chunks around each sub-agent invocation; the
83
+ * sub-agent's own chunks are forwarded byte-by-byte with their
84
+ * `actingAgentId` set so the client renders the member's avatar /
85
+ * name on the right bubble. Non-orchestrator agents short-circuit
86
+ * to the runner's stream.
87
+ *
88
+ * Implementation note: we drive the same Anthropic agentic loop as
89
+ * `runOrchestratorLoop` (no shortcut — the orchestrator's reasoning
90
+ * about WHO to delegate to is still a non-streamed messages.create
91
+ * call). The streaming part is the SUB-AGENT'S response, which is
92
+ * the part the visitor actually cares about seeing in real time.
93
+ */
94
+ async *stream(agentId, messages, context) {
95
+ const agent = (await this.resolveAgentDynamic(agentId)) ?? null;
96
+ if (!agent) {
97
+ throw new OrchestratorError('agent_not_found', `Agent "${agentId}" not found`);
98
+ }
99
+ if (!agent.canOrchestrate || !agent.subAgents?.length) {
100
+ // No-op orchestration — fall straight through. The runner's
101
+ // chunks won't carry `actingAgentId`, which is exactly what we
102
+ // want: this conversation is bound to one agent.
103
+ yield* this.runner.stream(agent, messages, context);
104
+ return;
105
+ }
106
+ this.logger.debug(`Streaming orchestrator "${agentId}" with subagents: ${agent.subAgents.join(', ')}`);
107
+ const delegationTools = this.buildDelegationTools(agent);
108
+ const model = agent.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
109
+ const maxTokens = agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
110
+ const anthropicTools = delegationTools.map((t) => ({
111
+ name: t.name,
112
+ description: t.description,
113
+ input_schema: t.inputSchema,
114
+ }));
115
+ let currentMessages = [...messages];
116
+ let totalUsage = {
117
+ inputTokens: 0,
118
+ outputTokens: 0,
119
+ totalTokens: 0,
120
+ };
121
+ const messageId = (0, crypto_1.randomUUID)();
122
+ // Hard cap on orchestrator loops. Without one, a misbehaving model
123
+ // could ping-pong delegate → think → delegate forever. Three hops
124
+ // is enough for any well-formed team flow; the system prompt also
125
+ // discourages chaining so this is mostly defense in depth.
126
+ const MAX_LOOPS = 3;
127
+ let loopCount = 0;
128
+ while (true) {
129
+ if (loopCount++ > MAX_LOOPS) {
130
+ yield {
131
+ type: 'text_delta',
132
+ delta: '\n\n(Orchestrator stopped: too many delegations in one turn.)',
133
+ };
134
+ break;
135
+ }
136
+ // Orchestrator's planning step — non-streamed. The model picks a
137
+ // member, we forward its prose as a single text_delta so the
138
+ // chat shows the "Routing to …" reasoning, then we drain the
139
+ // tool_use blocks one by one.
140
+ const response = await this.client.messages.create({
141
+ model,
142
+ max_tokens: maxTokens,
143
+ system: agent.systemPrompt,
144
+ messages: currentMessages,
145
+ tools: anthropicTools,
146
+ });
147
+ totalUsage = {
148
+ inputTokens: totalUsage.inputTokens + response.usage.input_tokens,
149
+ outputTokens: totalUsage.outputTokens + response.usage.output_tokens,
150
+ totalTokens: totalUsage.totalTokens +
151
+ response.usage.input_tokens +
152
+ response.usage.output_tokens,
153
+ };
154
+ // Emit orchestrator's own prose (the "Routing to Ana…" line).
155
+ // No actingAgentId — that means "this is the team speaking as
156
+ // itself", which the client renders with the team's branding.
157
+ for (const block of response.content) {
158
+ if (block.type === 'text' && block.text.trim()) {
159
+ yield { type: 'text_delta', delta: block.text };
160
+ }
161
+ }
162
+ if (response.stop_reason !== 'tool_use') {
163
+ // No delegation this turn — we're done.
164
+ break;
165
+ }
166
+ // Append the orchestrator's tool-use turn so the next loop sees
167
+ // it as part of the planning history.
168
+ const assistantMsg = {
169
+ role: 'assistant',
170
+ content: response.content,
171
+ };
172
+ currentMessages = [...currentMessages, assistantMsg];
173
+ const toolResults = [];
174
+ for (const block of response.content) {
175
+ if (block.type !== 'tool_use')
176
+ continue;
177
+ const delegateTool = delegationTools.find((t) => t.name === block.name);
178
+ if (!delegateTool)
179
+ continue;
180
+ const { task } = block.input;
181
+ const { subAgentId } = delegateTool;
182
+ const subAgent = await this.resolveAgentDynamic(subAgentId);
183
+ // delegation_start carries the member's identity so the client
184
+ // can render the routing hint AND swap the active bubble's
185
+ // avatar before the first text_delta arrives.
186
+ yield {
187
+ type: 'delegation_start',
188
+ subAgentId,
189
+ subAgentName: subAgent?.name,
190
+ subAgentAvatarUrl: undefined,
191
+ task,
192
+ };
193
+ if (!subAgent) {
194
+ // Member was deleted between team config + the orchestrator
195
+ // call. Synthesize a tool_result so the orchestrator can
196
+ // apologize on its next loop.
197
+ const errMsg = `Sub-agent "${subAgentId}" is no longer available.`;
198
+ yield {
199
+ type: 'delegation_result',
200
+ subAgentId,
201
+ result: errMsg,
202
+ };
203
+ toolResults.push({
204
+ type: 'tool_result',
205
+ tool_use_id: block.id,
206
+ content: errMsg,
207
+ });
208
+ continue;
209
+ }
210
+ // Stream the sub-agent's reply through the runner, tagging every
211
+ // chunk with the member's id so the client renders it with the
212
+ // member's identity. We accumulate the text body so the
213
+ // orchestrator's next loop can see what the member said.
214
+ const subMessages = [{ role: 'user', content: task }];
215
+ let assembled = '';
216
+ let subUsage = {
217
+ inputTokens: 0,
218
+ outputTokens: 0,
219
+ totalTokens: 0,
220
+ };
221
+ for await (const chunk of this.runner.stream(subAgent, subMessages, {
222
+ ...context,
223
+ agentId: subAgentId,
224
+ })) {
225
+ // text_delta is the only chunk type whose body we accumulate
226
+ // for the orchestrator's tool_result. Other chunks (tool
227
+ // calls inside the member, usage updates) we forward to the
228
+ // visitor but don't feed back to the orchestrator.
229
+ if (chunk.type === 'text_delta') {
230
+ assembled += chunk.delta;
231
+ yield { ...chunk, actingAgentId: subAgentId };
232
+ continue;
233
+ }
234
+ if (chunk.type === 'usage') {
235
+ subUsage = chunk.usage;
236
+ yield { ...chunk, actingAgentId: subAgentId };
237
+ continue;
238
+ }
239
+ if (chunk.type === 'done') {
240
+ // Swallow the inner `done` — the OUTER loop emits its own
241
+ // when the whole orchestrator turn ends.
242
+ continue;
243
+ }
244
+ // Pass-through with identity tag (tool_use_start, tool_result,
245
+ // awaiting_approval, tool_blocked, etc.).
246
+ yield { ...chunk, actingAgentId: subAgentId };
247
+ }
248
+ totalUsage.inputTokens += subUsage.inputTokens;
249
+ totalUsage.outputTokens += subUsage.outputTokens;
250
+ totalUsage.totalTokens += subUsage.totalTokens;
251
+ yield {
252
+ type: 'delegation_result',
253
+ subAgentId,
254
+ result: assembled,
255
+ };
256
+ toolResults.push({
257
+ type: 'tool_result',
258
+ tool_use_id: block.id,
259
+ // Anthropic rejects empty user-message content with a 400. A
260
+ // sub-agent can produce zero text_deltas legitimately (it ran
261
+ // only tools and never spoke) — when that happens we feed a
262
+ // sentinel string back into the orchestrator's next loop so
263
+ // the conversation stays well-formed. The sentinel doubles as
264
+ // a signal the orchestrator can interpret ("the member acted
265
+ // silently — decide whether to ask the user for confirmation").
266
+ content: assembled || '(member completed silently — no textual response)',
267
+ });
268
+ }
269
+ // Feed the tool results back into the orchestrator's next loop.
270
+ currentMessages = [
271
+ ...currentMessages,
272
+ { role: 'user', content: toolResults },
273
+ ];
274
+ }
275
+ yield { type: 'usage', usage: totalUsage };
276
+ yield { type: 'done', messageId };
277
+ }
58
278
  async runOrchestratorLoop(orchestrator, messages, context) {
59
279
  const delegations = [];
60
280
  const delegationTools = this.buildDelegationTools(orchestrator);
@@ -165,29 +165,54 @@ export interface ApprovalCopyBundle {
165
165
  blockedBody: string;
166
166
  expiresPrefix: string;
167
167
  }
168
- export type StreamChunk = {
168
+ /**
169
+ * Per-chunk identity. When a Team orchestrator delegates a turn to a
170
+ * member, the streaming chunks coming out of the member carry the
171
+ * member's `agentId` here so the chat client can render a bubble with
172
+ * the member's avatar and name. Optional everywhere — when absent the
173
+ * client falls back to the session's primary agent identity (the
174
+ * default, non-team behaviour).
175
+ *
176
+ * The orchestrator's OWN chunks (the routing wrappers it speaks as
177
+ * itself) carry no `actingAgentId` so the client renders them as the
178
+ * team / orchestrator identity. The boundary between members is marked
179
+ * by `delegation_start` and `delegation_result`.
180
+ */
181
+ export interface ActingAgentTag {
182
+ /** Id of the agent producing this chunk. When absent, treat the
183
+ * chunk as coming from the session's primary agent (legacy
184
+ * non-team behaviour). */
185
+ actingAgentId?: string;
186
+ }
187
+ export type StreamChunk = ({
169
188
  type: 'text_delta';
170
189
  delta: string;
171
- } | {
190
+ } & ActingAgentTag) | ({
172
191
  type: 'tool_use_start';
173
192
  toolName: string;
174
193
  toolUseId: string;
175
- } | {
194
+ } & ActingAgentTag) | ({
176
195
  type: 'tool_result';
177
196
  toolName: string;
178
197
  result: string;
179
- } | {
198
+ } & ActingAgentTag) | {
180
199
  type: 'delegation_start';
181
200
  subAgentId: string;
182
201
  task: string;
202
+ /** Display name of the sub-agent. Lets the client render the
203
+ * "Routing to <name>…" hint without a separate roster fetch. */
204
+ subAgentName?: string;
205
+ /** Optional avatar URL of the sub-agent — used by the chat client
206
+ * to swap the avatar column for the member's bubble. */
207
+ subAgentAvatarUrl?: string;
183
208
  } | {
184
209
  type: 'delegation_result';
185
210
  subAgentId: string;
186
211
  result: string;
187
- } | {
212
+ } | ({
188
213
  type: 'usage';
189
214
  usage: TokenUsage;
190
- } | {
215
+ } & ActingAgentTag) | {
191
216
  /**
192
217
  * Emitted by the runtime when a tool dispatch hit
193
218
  * `ToolApprovalGate.check() → { kind: 'approval' }`. The stream
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/core",
3
- "version": "2.0.24",
3
+ "version": "2.1.0",
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",