@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.
- package/dist/factory.js +56 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/services/agent-runner.service.js +18 -4
- package/dist/services/agent.service.d.ts +21 -1
- package/dist/services/agent.service.js +42 -8
- package/dist/services/orchestrator.service.d.ts +40 -1
- package/dist/services/orchestrator.service.js +220 -0
- package/dist/types/agent.types.d.ts +31 -6
- package/package.json +1 -1
- package/dist/adapters/billing/billing-adapter.interface.d.ts +0 -41
- package/dist/adapters/billing/billing-adapter.interface.js +0 -5
- package/dist/adapters/billing/stripe/stripe.adapter.d.ts +0 -30
- package/dist/adapters/billing/stripe/stripe.adapter.js +0 -122
- package/dist/adapters/email/email-adapter.interface.d.ts +0 -25
- package/dist/adapters/email/email-adapter.interface.js +0 -6
- package/dist/adapters/email/noop.adapter.d.ts +0 -10
- package/dist/adapters/email/noop.adapter.js +0 -15
- package/dist/adapters/email/resend.adapter.d.ts +0 -8
- package/dist/adapters/email/resend.adapter.js +0 -39
- package/dist/adapters/upload/noop.adapter.d.ts +0 -9
- package/dist/adapters/upload/noop.adapter.js +0 -14
- package/dist/adapters/upload/s3.adapter.d.ts +0 -38
- package/dist/adapters/upload/s3.adapter.js +0 -69
- package/dist/adapters/upload/upload-adapter.interface.d.ts +0 -37
- package/dist/adapters/upload/upload-adapter.interface.js +0 -15
- package/dist/billing/index.d.ts +0 -12
- package/dist/billing/index.js +0 -28
- package/dist/domain/agent.d.ts +0 -59
- package/dist/domain/agent.js +0 -2
- package/dist/domain/api-key.d.ts +0 -28
- package/dist/domain/api-key.js +0 -2
- package/dist/domain/auth-identity.d.ts +0 -10
- package/dist/domain/auth-identity.js +0 -2
- package/dist/domain/email-token.d.ts +0 -11
- package/dist/domain/email-token.js +0 -2
- package/dist/domain/external-user.d.ts +0 -23
- package/dist/domain/external-user.js +0 -2
- package/dist/domain/plan.d.ts +0 -20
- package/dist/domain/plan.js +0 -2
- package/dist/domain/platform-secret.d.ts +0 -24
- package/dist/domain/platform-secret.js +0 -8
- package/dist/domain/refresh-token.d.ts +0 -15
- package/dist/domain/refresh-token.js +0 -2
- package/dist/domain/subscription.d.ts +0 -21
- package/dist/domain/subscription.js +0 -2
- package/dist/domain/tenant.d.ts +0 -21
- package/dist/domain/tenant.js +0 -2
- package/dist/domain/usage-record.d.ts +0 -15
- package/dist/domain/usage-record.js +0 -2
- package/dist/domain/user.d.ts +0 -43
- package/dist/domain/user.js +0 -2
- package/dist/services/agent-config.service.d.ts +0 -45
- package/dist/services/agent-config.service.js +0 -114
- package/dist/services/api-key.service.d.ts +0 -41
- package/dist/services/api-key.service.js +0 -80
- package/dist/services/auth.service.d.ts +0 -133
- package/dist/services/auth.service.js +0 -411
- package/dist/services/billing.service.d.ts +0 -67
- package/dist/services/billing.service.js +0 -254
- package/dist/services/email-templates.d.ts +0 -18
- package/dist/services/email-templates.js +0 -39
- package/dist/services/email.service.d.ts +0 -26
- package/dist/services/email.service.js +0 -42
- package/dist/services/errors.d.ts +0 -7
- package/dist/services/errors.js +0 -27
- package/dist/services/oauth.service.d.ts +0 -73
- package/dist/services/oauth.service.js +0 -174
- package/dist/services/plan.service.d.ts +0 -54
- package/dist/services/plan.service.js +0 -120
- package/dist/services/refresh-token.service.d.ts +0 -38
- package/dist/services/refresh-token.service.js +0 -73
- package/dist/services/secrets/crypto.d.ts +0 -37
- package/dist/services/secrets/crypto.js +0 -110
- package/dist/services/secrets/known-keys.d.ts +0 -38
- package/dist/services/secrets/known-keys.js +0 -50
- package/dist/services/secrets.service.d.ts +0 -91
- package/dist/services/secrets.service.js +0 -193
- package/dist/services/tenant-billing.service.d.ts +0 -121
- package/dist/services/tenant-billing.service.js +0 -290
- package/dist/services/tenant.service.d.ts +0 -54
- package/dist/services/tenant.service.js +0 -96
- package/dist/services/upload.service.d.ts +0 -37
- package/dist/services/upload.service.js +0 -84
- package/dist/services/usage.service.d.ts +0 -34
- package/dist/services/usage.service.js +0 -108
- package/dist/types/billing.types.d.ts +0 -82
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
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",
|