@agentforge-io/core 2.0.23 → 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 (95) hide show
  1. package/dist/ai/index.d.ts +2 -0
  2. package/dist/ai/index.js +5 -1
  3. package/dist/factory.js +56 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +7 -1
  6. package/dist/services/agent-runner.service.js +117 -7
  7. package/dist/services/agent.service.d.ts +21 -1
  8. package/dist/services/agent.service.js +77 -10
  9. package/dist/services/orchestrator.service.d.ts +40 -1
  10. package/dist/services/orchestrator.service.js +220 -0
  11. package/dist/types/agent.types.d.ts +31 -6
  12. package/dist/types/config.types.d.ts +8 -1
  13. package/dist/types/index.d.ts +1 -0
  14. package/dist/types/index.js +1 -0
  15. package/dist/types/model-strategy.d.ts +97 -0
  16. package/dist/types/model-strategy.js +83 -0
  17. package/package.json +1 -1
  18. package/dist/adapters/billing/billing-adapter.interface.d.ts +0 -41
  19. package/dist/adapters/billing/billing-adapter.interface.js +0 -5
  20. package/dist/adapters/billing/stripe/stripe.adapter.d.ts +0 -30
  21. package/dist/adapters/billing/stripe/stripe.adapter.js +0 -122
  22. package/dist/adapters/email/email-adapter.interface.d.ts +0 -25
  23. package/dist/adapters/email/email-adapter.interface.js +0 -6
  24. package/dist/adapters/email/noop.adapter.d.ts +0 -10
  25. package/dist/adapters/email/noop.adapter.js +0 -15
  26. package/dist/adapters/email/resend.adapter.d.ts +0 -8
  27. package/dist/adapters/email/resend.adapter.js +0 -39
  28. package/dist/adapters/upload/noop.adapter.d.ts +0 -9
  29. package/dist/adapters/upload/noop.adapter.js +0 -14
  30. package/dist/adapters/upload/s3.adapter.d.ts +0 -38
  31. package/dist/adapters/upload/s3.adapter.js +0 -69
  32. package/dist/adapters/upload/upload-adapter.interface.d.ts +0 -37
  33. package/dist/adapters/upload/upload-adapter.interface.js +0 -15
  34. package/dist/billing/index.d.ts +0 -12
  35. package/dist/billing/index.js +0 -28
  36. package/dist/domain/agent.d.ts +0 -59
  37. package/dist/domain/agent.js +0 -2
  38. package/dist/domain/api-key.d.ts +0 -28
  39. package/dist/domain/api-key.js +0 -2
  40. package/dist/domain/auth-identity.d.ts +0 -10
  41. package/dist/domain/auth-identity.js +0 -2
  42. package/dist/domain/email-token.d.ts +0 -11
  43. package/dist/domain/email-token.js +0 -2
  44. package/dist/domain/external-user.d.ts +0 -23
  45. package/dist/domain/external-user.js +0 -2
  46. package/dist/domain/plan.d.ts +0 -20
  47. package/dist/domain/plan.js +0 -2
  48. package/dist/domain/platform-secret.d.ts +0 -24
  49. package/dist/domain/platform-secret.js +0 -8
  50. package/dist/domain/refresh-token.d.ts +0 -15
  51. package/dist/domain/refresh-token.js +0 -2
  52. package/dist/domain/subscription.d.ts +0 -21
  53. package/dist/domain/subscription.js +0 -2
  54. package/dist/domain/tenant.d.ts +0 -21
  55. package/dist/domain/tenant.js +0 -2
  56. package/dist/domain/usage-record.d.ts +0 -15
  57. package/dist/domain/usage-record.js +0 -2
  58. package/dist/domain/user.d.ts +0 -43
  59. package/dist/domain/user.js +0 -2
  60. package/dist/services/agent-config.service.d.ts +0 -45
  61. package/dist/services/agent-config.service.js +0 -114
  62. package/dist/services/api-key.service.d.ts +0 -41
  63. package/dist/services/api-key.service.js +0 -80
  64. package/dist/services/auth.service.d.ts +0 -133
  65. package/dist/services/auth.service.js +0 -411
  66. package/dist/services/billing.service.d.ts +0 -67
  67. package/dist/services/billing.service.js +0 -254
  68. package/dist/services/email-templates.d.ts +0 -18
  69. package/dist/services/email-templates.js +0 -39
  70. package/dist/services/email.service.d.ts +0 -26
  71. package/dist/services/email.service.js +0 -42
  72. package/dist/services/errors.d.ts +0 -7
  73. package/dist/services/errors.js +0 -27
  74. package/dist/services/oauth.service.d.ts +0 -73
  75. package/dist/services/oauth.service.js +0 -174
  76. package/dist/services/plan.service.d.ts +0 -54
  77. package/dist/services/plan.service.js +0 -120
  78. package/dist/services/refresh-token.service.d.ts +0 -38
  79. package/dist/services/refresh-token.service.js +0 -73
  80. package/dist/services/secrets/crypto.d.ts +0 -37
  81. package/dist/services/secrets/crypto.js +0 -110
  82. package/dist/services/secrets/known-keys.d.ts +0 -38
  83. package/dist/services/secrets/known-keys.js +0 -50
  84. package/dist/services/secrets.service.d.ts +0 -91
  85. package/dist/services/secrets.service.js +0 -193
  86. package/dist/services/tenant-billing.service.d.ts +0 -121
  87. package/dist/services/tenant-billing.service.js +0 -290
  88. package/dist/services/tenant.service.d.ts +0 -54
  89. package/dist/services/tenant.service.js +0 -96
  90. package/dist/services/upload.service.d.ts +0 -37
  91. package/dist/services/upload.service.js +0 -84
  92. package/dist/services/usage.service.d.ts +0 -34
  93. package/dist/services/usage.service.js +0 -108
  94. package/dist/types/billing.types.d.ts +0 -82
  95. package/dist/types/billing.types.js +0 -3
@@ -2,6 +2,8 @@ export { AGENT_FORGE_CONFIG, AGENT_QUEUE_NAME, CURRENT_USER, } from '../constant
2
2
  export type { AgentDefinition, AnthropicConfig, McpServerConfig, AgentForgeConfig, DatabaseConfig, RedisConfig, QueueConfig, } from '../types/config.types';
3
3
  export type { AgentResponse, AgentOverrides, StreamChunk, TokenUsage, ToolCallRecord, AgentToolDefinition, AgentJobPayload, AgentJobResult, AnthropicMessage, } from '../types/agent.types';
4
4
  export type { SdkHooks, UsageEvent, TurnCompleteEvent, ToolCallEvent, } from '../types/hooks';
5
+ export type { ModelStrategy, ModelTier, EscalateRule, FallbackRule, TurnSignals, ModelSelection, } from '../types/model-strategy';
6
+ export { selectModel, DEFAULT_LONG_CONTEXT_TOKENS, DEFAULT_SHORT_INPUT_TOKENS, } from '../types/model-strategy';
5
7
  export { ToolRegistryService, type Logger } from '../services/tool-registry.service';
6
8
  export { AgentRunnerService } from '../services/agent-runner.service';
7
9
  export { OrchestratorService } from '../services/orchestrator.service';
package/dist/ai/index.js CHANGED
@@ -8,12 +8,16 @@
8
8
  //
9
9
  // Files still co-located physically; this is a logical seam.
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.InMemoryJobQueue = exports.JOB_QUEUE = exports.InMemoryPreparedStreamStore = exports.PREPARED_STREAM_STORE = exports.AgentJobWorker = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.PreparedStreamError = exports.PreparedStreamService = exports.OrchestratorService = exports.AgentRunnerService = exports.ToolRegistryService = exports.CURRENT_USER = exports.AGENT_QUEUE_NAME = exports.AGENT_FORGE_CONFIG = void 0;
11
+ exports.InMemoryJobQueue = exports.JOB_QUEUE = exports.InMemoryPreparedStreamStore = exports.PREPARED_STREAM_STORE = exports.AgentJobWorker = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.PreparedStreamError = exports.PreparedStreamService = exports.OrchestratorService = exports.AgentRunnerService = exports.ToolRegistryService = exports.DEFAULT_SHORT_INPUT_TOKENS = exports.DEFAULT_LONG_CONTEXT_TOKENS = exports.selectModel = exports.CURRENT_USER = exports.AGENT_QUEUE_NAME = exports.AGENT_FORGE_CONFIG = void 0;
12
12
  // ─── Constants ─────────────────────────────────────────────────────────────
13
13
  var constants_1 = require("../constants");
14
14
  Object.defineProperty(exports, "AGENT_FORGE_CONFIG", { enumerable: true, get: function () { return constants_1.AGENT_FORGE_CONFIG; } });
15
15
  Object.defineProperty(exports, "AGENT_QUEUE_NAME", { enumerable: true, get: function () { return constants_1.AGENT_QUEUE_NAME; } });
16
16
  Object.defineProperty(exports, "CURRENT_USER", { enumerable: true, get: function () { return constants_1.CURRENT_USER; } });
17
+ var model_strategy_1 = require("../types/model-strategy");
18
+ Object.defineProperty(exports, "selectModel", { enumerable: true, get: function () { return model_strategy_1.selectModel; } });
19
+ Object.defineProperty(exports, "DEFAULT_LONG_CONTEXT_TOKENS", { enumerable: true, get: function () { return model_strategy_1.DEFAULT_LONG_CONTEXT_TOKENS; } });
20
+ Object.defineProperty(exports, "DEFAULT_SHORT_INPUT_TOKENS", { enumerable: true, get: function () { return model_strategy_1.DEFAULT_SHORT_INPUT_TOKENS; } });
17
21
  // ─── Services ──────────────────────────────────────────────────────────────
18
22
  var tool_registry_service_1 = require("../services/tool-registry.service");
19
23
  Object.defineProperty(exports, "ToolRegistryService", { enumerable: true, get: function () { return tool_registry_service_1.ToolRegistryService; } });
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; } });
@@ -7,6 +7,7 @@ exports.AgentRunnerService = void 0;
7
7
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
8
  const crypto_1 = require("crypto");
9
9
  const tool_approval_gate_1 = require("./tool-approval-gate");
10
+ const model_strategy_1 = require("../types/model-strategy");
10
11
  const noopLogger = {
11
12
  log: () => { }, warn: () => { }, debug: () => { }, error: () => { },
12
13
  };
@@ -29,25 +30,64 @@ class AgentRunnerService {
29
30
  // ─── Run (non-streaming) ──────────────────────────────────────────────────
30
31
  async run(agent, messages, context, overrides) {
31
32
  const messageId = (0, crypto_1.randomUUID)();
32
- const model = overrides?.model ?? agent.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
33
+ const runnerDefault = this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
34
+ const baseModel = overrides?.model ?? agent.model ?? runnerDefault;
33
35
  const maxTokens = overrides?.maxTokens ?? agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
34
- 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;
35
43
  const { tools, extras } = this.buildToolList(agent, overrides);
36
44
  const systemPrompt = this.buildSystemPrompt(agent, tools, overrides);
37
45
  const toolCalls = [];
38
46
  let currentMessages = [...messages];
47
+ // Pre-compute the signals the model router reads. `hasTools` and
48
+ // `hasApprovalTool` are constant across the agentic loop (we
49
+ // don't add tools mid-conversation); `estimatedInputTokens`
50
+ // starts from a word-count heuristic and gets replaced by the
51
+ // real `usage.input_tokens` once we have a response.
52
+ const turnSignals = {
53
+ hasTools: !!tools && tools.length > 0,
54
+ hasApprovalTool: hasApprovalGatedTool(agent),
55
+ estimatedInputTokens: estimateInputTokens(systemPrompt, currentMessages),
56
+ };
39
57
  let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
40
58
  let finalContent = '';
41
59
  let stopReason = 'end_turn';
60
+ // Last model id the router chose. Surfaced on the response so
61
+ // callers (and the conversation/usage logs) record what
62
+ // actually ran, not what the agent's `model` field says.
63
+ let lastModel = baseModel;
42
64
  while (true) {
65
+ // Per-turn model selection. When overrides force a model we
66
+ // honour it (manual `agent.runMessage({ overrides: { model }})`
67
+ // beats the strategy). Otherwise the strategy decides; absent
68
+ // strategy → behave exactly like before this feature landed.
69
+ const selection = overrides?.model
70
+ ? { model: overrides.model, reason: 'forced' }
71
+ : (0, model_strategy_1.selectModel)(agent.modelStrategy, turnSignals, baseModel);
72
+ const model = selection.model;
73
+ lastModel = model;
74
+ if (this.logger && selection.reason !== 'default' && selection.reason !== 'forced') {
75
+ this.logger.debug(`[modelRouter] agent=${agent.id} ${selection.reason}=${selection.trigger} → ${model}`);
76
+ }
43
77
  const response = await this.client.messages.create({
44
78
  model,
45
79
  max_tokens: maxTokens,
46
- temperature,
80
+ // Only include temperature when explicitly declared — newer
81
+ // models 400 on `temperature` when tools are present.
82
+ ...(typeof temperature === 'number' ? { temperature } : {}),
47
83
  system: systemPrompt,
48
84
  messages: currentMessages,
49
85
  tools: tools,
50
86
  });
87
+ // Update the signal for the NEXT iteration of the loop — the
88
+ // tool-result feedback we're about to add can balloon the
89
+ // context past the long-context threshold.
90
+ turnSignals.estimatedInputTokens = response.usage.input_tokens;
51
91
  totalUsage = {
52
92
  inputTokens: totalUsage.inputTokens + response.usage.input_tokens,
53
93
  outputTokens: totalUsage.outputTokens + response.usage.output_tokens,
@@ -117,7 +157,7 @@ class AgentRunnerService {
117
157
  role: 'assistant',
118
158
  toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
119
159
  usage: totalUsage,
120
- model,
160
+ model: lastModel,
121
161
  stopReason,
122
162
  createdAt: new Date(),
123
163
  };
@@ -125,18 +165,38 @@ class AgentRunnerService {
125
165
  // ─── Run (streaming) ──────────────────────────────────────────────────────
126
166
  async *stream(agent, messages, context, overrides) {
127
167
  const messageId = (0, crypto_1.randomUUID)();
128
- const model = overrides?.model ?? agent.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
168
+ const runnerDefault = this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
169
+ const baseModel = overrides?.model ?? agent.model ?? runnerDefault;
129
170
  const maxTokens = overrides?.maxTokens ?? agent.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
130
- 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;
131
178
  const { tools, extras } = this.buildToolList(agent, overrides);
132
179
  const systemPrompt = this.buildSystemPrompt(agent, tools, overrides);
133
180
  let currentMessages = [...messages];
134
181
  let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
182
+ // See `run()` above for the rationale on these signals.
183
+ const turnSignals = {
184
+ hasTools: !!tools && tools.length > 0,
185
+ hasApprovalTool: hasApprovalGatedTool(agent),
186
+ estimatedInputTokens: estimateInputTokens(systemPrompt, currentMessages),
187
+ };
135
188
  while (true) {
189
+ const selection = overrides?.model
190
+ ? { model: overrides.model, reason: 'forced' }
191
+ : (0, model_strategy_1.selectModel)(agent.modelStrategy, turnSignals, baseModel);
192
+ const model = selection.model;
193
+ if (this.logger && selection.reason !== 'default' && selection.reason !== 'forced') {
194
+ this.logger.debug(`[modelRouter] agent=${agent.id} ${selection.reason}=${selection.trigger} → ${model}`);
195
+ }
136
196
  const stream = this.client.messages.stream({
137
197
  model,
138
198
  max_tokens: maxTokens,
139
- temperature,
199
+ ...(typeof temperature === 'number' ? { temperature } : {}),
140
200
  system: systemPrompt,
141
201
  messages: currentMessages,
142
202
  tools: tools,
@@ -168,6 +228,12 @@ class AgentRunnerService {
168
228
  }
169
229
  }
170
230
  const finalMessage = await stream.finalMessage();
231
+ // Refresh the input-token signal so the next iteration of the
232
+ // tool loop has the post-tool-result context length, not the
233
+ // initial estimate.
234
+ if (typeof finalMessage.usage?.input_tokens === 'number') {
235
+ turnSignals.estimatedInputTokens = finalMessage.usage.input_tokens;
236
+ }
171
237
  if (finalMessage.stop_reason === 'tool_use') {
172
238
  currentMessages = [...currentMessages, { role: 'assistant', content: finalMessage.content }];
173
239
  const toolResults = [];
@@ -311,3 +377,47 @@ class AgentRunnerService {
311
377
  }
312
378
  }
313
379
  exports.AgentRunnerService = AgentRunnerService;
380
+ /** Cheap word-based token estimate for the first turn — Anthropic
381
+ * reports real `usage.input_tokens` from the response onwards, so
382
+ * this only needs to be accurate enough to fire `longContext` /
383
+ * `shortInput` rules on the FIRST request before any usage is back.
384
+ * Rough rule of thumb (verified against Anthropic's tokenizer on
385
+ * English prose): ~0.75 tokens per whitespace-split word. */
386
+ function estimateInputTokens(systemPrompt, messages) {
387
+ let words = systemPrompt.trim().split(/\s+/).filter(Boolean).length;
388
+ for (const m of messages) {
389
+ if (typeof m.content === 'string') {
390
+ words += m.content.trim().split(/\s+/).filter(Boolean).length;
391
+ continue;
392
+ }
393
+ if (Array.isArray(m.content)) {
394
+ for (const block of m.content) {
395
+ if (block &&
396
+ typeof block === 'object' &&
397
+ 'type' in block &&
398
+ block.type === 'text' &&
399
+ typeof block.text === 'string') {
400
+ words += block.text
401
+ .trim()
402
+ .split(/\s+/)
403
+ .filter(Boolean).length;
404
+ }
405
+ }
406
+ }
407
+ }
408
+ return Math.ceil(words / 0.75);
409
+ }
410
+ /** True when at least one declared tool requires human approval at
411
+ * runtime. Read off `agent.tools` only — `extraTools` (per-call
412
+ * connector tools attached at request time) follow the host's own
413
+ * per-tenant policy and aren't visible to this scope. */
414
+ function hasApprovalGatedTool(agent) {
415
+ const tools = agent.tools;
416
+ if (!tools)
417
+ return false;
418
+ for (const t of tools) {
419
+ if (typeof t === 'object' && t && t.mode === 'approval')
420
+ return true;
421
+ }
422
+ return false;
423
+ }
@@ -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
@@ -212,7 +219,12 @@ class AgentService {
212
219
  // caller's userId, which is the historical personal-agent path.
213
220
  const resolvedExtras = await this.resolveExtraTools(agent.connectorOwnerUserId ?? params.userId);
214
221
  const filter = params.overrides?.extraToolsFilter;
215
- const extraTools = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
222
+ const fromConnectors = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
223
+ // Merge connector tools with whatever the caller passed in
224
+ // `overrides.extraTools` (e.g. the remote-tool wrappers from
225
+ // ChatStreamController). Caller wins on name collisions so an
226
+ // explicit override always trumps an inherited connector tool.
227
+ const extraTools = mergeExtraTools(params.overrides?.extraTools, fromConnectors);
216
228
  const response = await this.runner.run(agent, messages, {
217
229
  userId: params.userId,
218
230
  conversationId: params.conversationId,
@@ -278,15 +290,33 @@ class AgentService {
278
290
  // toolbelt regardless of which visitor session is streaming.
279
291
  const resolvedExtras = await this.resolveExtraTools(agent.connectorOwnerUserId ?? params.userId);
280
292
  const filter = params.overrides?.extraToolsFilter;
281
- const extraTools = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
293
+ const fromConnectors = filter && resolvedExtras ? filter(resolvedExtras) : resolvedExtras;
294
+ const extraTools = mergeExtraTools(params.overrides?.extraTools, fromConnectors);
282
295
  try {
283
- for await (const chunk of this.runner.stream(agent, messages, {
284
- userId: params.userId,
285
- conversationId: params.conversationId,
286
- agentId: conv.agentId,
287
- messageId: 'streaming',
288
- agent: { timezone: agent.timezone },
289
- }, { ...(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) {
290
320
  if (chunk.type === 'text_delta')
291
321
  fullContent += chunk.delta;
292
322
  if (chunk.type === 'usage')
@@ -505,7 +535,44 @@ function toAgentDefinition(record) {
505
535
  // from the host's resolver to the SDK and every agent reads as
506
536
  // `undefined` (i.e. public).
507
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
+ : {}),
508
548
  ...(record.slug !== undefined ? { slug: record.slug } : {}),
509
549
  ...(extra.appearance !== undefined ? { appearance: extra.appearance } : {}),
510
550
  };
511
551
  }
552
+ /**
553
+ * Merge two `extraTools` arrays so an explicit caller-provided list
554
+ * (e.g. remote-tool wrappers from a chat-stream controller) doesn't get
555
+ * shadowed by an `undefined` result from the connector resolver.
556
+ *
557
+ * Caller wins on name collisions — the caller passed the tool
558
+ * deliberately and knows the host context; an inherited connector tool
559
+ * with the same name is almost certainly stale or coincidental.
560
+ *
561
+ * Returns `undefined` (not `[]`) when both inputs are empty so the
562
+ * runner's "if (!extras?.length) skip overrides" path keeps working.
563
+ */
564
+ function mergeExtraTools(caller, connectors) {
565
+ if (!caller?.length && !connectors?.length)
566
+ return undefined;
567
+ if (!caller?.length)
568
+ return connectors;
569
+ if (!connectors?.length)
570
+ return caller;
571
+ const callerNames = new Set(caller.map((t) => t.name));
572
+ const merged = [...caller];
573
+ for (const t of connectors) {
574
+ if (!callerNames.has(t.name))
575
+ merged.push(t);
576
+ }
577
+ return merged;
578
+ }
@@ -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;