@agentforge-io/core 2.0.24 → 2.1.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.
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 +57 -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; } });
@@ -11,6 +11,38 @@ const model_strategy_1 = require("../types/model-strategy");
11
11
  const noopLogger = {
12
12
  log: () => { }, warn: () => { }, debug: () => { }, error: () => { },
13
13
  };
14
+ /**
15
+ * Anthropic's newer model families deprecated the `temperature` parameter
16
+ * entirely — they auto-tune sampling internally and return 400
17
+ * `invalid_request_error: \`temperature\` is deprecated for this model` if
18
+ * the caller still sends one. Older families (3.x, the original 4.0
19
+ * releases) accept it fine.
20
+ *
21
+ * Detection by string match on the model id rather than a hard-coded
22
+ * allowlist: new model ids land between SDK releases, and we don't want
23
+ * to break temperature on legacy agents the day a new family ships.
24
+ * Pattern: anything that contains `-4-5`, `-4-6`, `-4-7`, …, `-5-*`,
25
+ * `-6-*`, etc. counts as "newer." Old 4-0 / 4-1 / 3-x ids are unaffected.
26
+ *
27
+ * Heuristic, not exhaustive — if a future family lands with a different
28
+ * naming convention we'll have to extend this. The cost of being wrong
29
+ * is a single 400 the operator can fix by clearing the temperature in
30
+ * the editor; the cost of NOT filtering is the same 400 today.
31
+ */
32
+ function modelRejectsTemperature(model) {
33
+ if (!model)
34
+ return false;
35
+ // Normalize: ignore vendor prefixes like "anthropic/claude-..." and
36
+ // bracket suffixes like "claude-opus-4-7[1m]" (long-context variant).
37
+ const m = model.toLowerCase().replace(/\[[^\]]*\]/g, '');
38
+ // claude-*-4-5, 4-6, 4-7, 4-8 …
39
+ if (/claude-[a-z]+-4-([5-9])\b/.test(m))
40
+ return true;
41
+ // claude-*-5-x, claude-*-6-x, … (future major bumps)
42
+ if (/claude-[a-z]+-([5-9])-/.test(m))
43
+ return true;
44
+ return false;
45
+ }
14
46
  /**
15
47
  * Framework-free runner for Claude. Handles the agentic loop (tool calls) for
16
48
  * sync runs and exposes streaming as an `AsyncGenerator<StreamChunk>` so any
@@ -33,7 +65,13 @@ class AgentRunnerService {
33
65
  const runnerDefault = this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
34
66
  const baseModel = overrides?.model ?? agent.model ?? runnerDefault;
35
67
  const maxTokens = overrides?.maxTokens ?? agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
36
- const temperature = overrides?.temperature ?? agent.temperature ?? 1;
68
+ // Anthropic's newer models (Sonnet 4.6+, Haiku 4.5+) reject
69
+ // `temperature` when tools are present — they auto-tune sampling for
70
+ // tool use. Only forward it when the operator/caller declared one
71
+ // explicitly; never inject a default. Old models that required it
72
+ // accept its absence too (they fall back to their own internal
73
+ // default of 1.0).
74
+ const temperature = overrides?.temperature ?? agent.temperature;
37
75
  const { tools, extras } = this.buildToolList(agent, overrides);
38
76
  const systemPrompt = this.buildSystemPrompt(agent, tools, overrides);
39
77
  const toolCalls = [];
@@ -68,10 +106,17 @@ class AgentRunnerService {
68
106
  if (this.logger && selection.reason !== 'default' && selection.reason !== 'forced') {
69
107
  this.logger.debug(`[modelRouter] agent=${agent.id} ${selection.reason}=${selection.trigger} → ${model}`);
70
108
  }
109
+ // Per-turn temperature gating. The PER-MODEL filter runs INSIDE
110
+ // the loop because `model` can change between turns (model
111
+ // strategy can route a long-context turn to a different family
112
+ // than the short turns above it). Computing once outside would
113
+ // either over-strip (drop temperature for a legacy follow-up
114
+ // model) or under-strip (forward it to a new-family upgrade).
115
+ const includeTemperature = typeof temperature === 'number' && !modelRejectsTemperature(model);
71
116
  const response = await this.client.messages.create({
72
117
  model,
73
118
  max_tokens: maxTokens,
74
- temperature,
119
+ ...(includeTemperature ? { temperature } : {}),
75
120
  system: systemPrompt,
76
121
  messages: currentMessages,
77
122
  tools: tools,
@@ -160,7 +205,13 @@ class AgentRunnerService {
160
205
  const runnerDefault = this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
161
206
  const baseModel = overrides?.model ?? agent.model ?? runnerDefault;
162
207
  const maxTokens = overrides?.maxTokens ?? agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
163
- const temperature = overrides?.temperature ?? agent.temperature ?? 1;
208
+ // Anthropic's newer models (Sonnet 4.6+, Haiku 4.5+) reject
209
+ // `temperature` when tools are present — they auto-tune sampling for
210
+ // tool use. Only forward it when the operator/caller declared one
211
+ // explicitly; never inject a default. Old models that required it
212
+ // accept its absence too (they fall back to their own internal
213
+ // default of 1.0).
214
+ const temperature = overrides?.temperature ?? agent.temperature;
164
215
  const { tools, extras } = this.buildToolList(agent, overrides);
165
216
  const systemPrompt = this.buildSystemPrompt(agent, tools, overrides);
166
217
  let currentMessages = [...messages];
@@ -179,10 +230,12 @@ class AgentRunnerService {
179
230
  if (this.logger && selection.reason !== 'default' && selection.reason !== 'forced') {
180
231
  this.logger.debug(`[modelRouter] agent=${agent.id} ${selection.reason}=${selection.trigger} → ${model}`);
181
232
  }
233
+ // Per-turn temperature gating — see `run()` above for rationale.
234
+ const includeTemperature = typeof temperature === 'number' && !modelRejectsTemperature(model);
182
235
  const stream = this.client.messages.stream({
183
236
  model,
184
237
  max_tokens: maxTokens,
185
- temperature,
238
+ ...(includeTemperature ? { temperature } : {}),
186
239
  system: systemPrompt,
187
240
  messages: currentMessages,
188
241
  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);