@agentforge-io/core 0.2.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 (151) hide show
  1. package/dist/adapters/billing/billing-adapter.interface.d.ts +41 -0
  2. package/dist/adapters/billing/billing-adapter.interface.js +5 -0
  3. package/dist/adapters/billing/stripe/stripe.adapter.d.ts +30 -0
  4. package/dist/adapters/billing/stripe/stripe.adapter.js +122 -0
  5. package/dist/adapters/email/email-adapter.interface.d.ts +25 -0
  6. package/dist/adapters/email/email-adapter.interface.js +6 -0
  7. package/dist/adapters/email/noop.adapter.d.ts +10 -0
  8. package/dist/adapters/email/noop.adapter.js +15 -0
  9. package/dist/adapters/email/resend.adapter.d.ts +8 -0
  10. package/dist/adapters/email/resend.adapter.js +39 -0
  11. package/dist/adapters/job-queue/in-memory.d.ts +43 -0
  12. package/dist/adapters/job-queue/in-memory.js +154 -0
  13. package/dist/adapters/job-queue/job-queue.types.d.ts +76 -0
  14. package/dist/adapters/job-queue/job-queue.types.js +5 -0
  15. package/dist/adapters/prepared-stream/prepared-stream.types.d.ts +23 -0
  16. package/dist/adapters/prepared-stream/prepared-stream.types.js +5 -0
  17. package/dist/adapters/rate-limiter/in-memory.d.ts +19 -0
  18. package/dist/adapters/rate-limiter/in-memory.js +63 -0
  19. package/dist/adapters/rate-limiter/rate-limiter.types.d.ts +42 -0
  20. package/dist/adapters/rate-limiter/rate-limiter.types.js +5 -0
  21. package/dist/adapters/rate-limiter/redis.d.ts +31 -0
  22. package/dist/adapters/rate-limiter/redis.js +47 -0
  23. package/dist/adapters/upload/noop.adapter.d.ts +9 -0
  24. package/dist/adapters/upload/noop.adapter.js +14 -0
  25. package/dist/adapters/upload/s3.adapter.d.ts +38 -0
  26. package/dist/adapters/upload/s3.adapter.js +69 -0
  27. package/dist/adapters/upload/upload-adapter.interface.d.ts +37 -0
  28. package/dist/adapters/upload/upload-adapter.interface.js +15 -0
  29. package/dist/ai/index.d.ts +15 -0
  30. package/dist/ai/index.js +43 -0
  31. package/dist/billing/index.d.ts +12 -0
  32. package/dist/billing/index.js +28 -0
  33. package/dist/constants.d.ts +3 -0
  34. package/dist/constants.js +8 -0
  35. package/dist/domain/agent.d.ts +59 -0
  36. package/dist/domain/agent.js +2 -0
  37. package/dist/domain/api-key.d.ts +28 -0
  38. package/dist/domain/api-key.js +2 -0
  39. package/dist/domain/auth-identity.d.ts +10 -0
  40. package/dist/domain/auth-identity.js +2 -0
  41. package/dist/domain/chat-token.d.ts +39 -0
  42. package/dist/domain/chat-token.js +2 -0
  43. package/dist/domain/connector-auth.d.ts +42 -0
  44. package/dist/domain/connector-auth.js +2 -0
  45. package/dist/domain/connector.d.ts +52 -0
  46. package/dist/domain/connector.js +2 -0
  47. package/dist/domain/conversation.d.ts +26 -0
  48. package/dist/domain/conversation.js +2 -0
  49. package/dist/domain/email-token.d.ts +11 -0
  50. package/dist/domain/email-token.js +2 -0
  51. package/dist/domain/external-user.d.ts +23 -0
  52. package/dist/domain/external-user.js +2 -0
  53. package/dist/domain/index.d.ts +5 -0
  54. package/dist/domain/index.js +24 -0
  55. package/dist/domain/mcp-server.d.ts +33 -0
  56. package/dist/domain/mcp-server.js +2 -0
  57. package/dist/domain/plan.d.ts +20 -0
  58. package/dist/domain/plan.js +2 -0
  59. package/dist/domain/platform-secret.d.ts +24 -0
  60. package/dist/domain/platform-secret.js +8 -0
  61. package/dist/domain/refresh-token.d.ts +15 -0
  62. package/dist/domain/refresh-token.js +2 -0
  63. package/dist/domain/subscription.d.ts +21 -0
  64. package/dist/domain/subscription.js +2 -0
  65. package/dist/domain/tenant.d.ts +21 -0
  66. package/dist/domain/tenant.js +2 -0
  67. package/dist/domain/usage-record.d.ts +15 -0
  68. package/dist/domain/usage-record.js +2 -0
  69. package/dist/domain/user.d.ts +43 -0
  70. package/dist/domain/user.js +2 -0
  71. package/dist/factory.d.ts +68 -0
  72. package/dist/factory.js +56 -0
  73. package/dist/index.d.ts +14 -0
  74. package/dist/index.js +59 -0
  75. package/dist/repositories/in-memory.d.ts +30 -0
  76. package/dist/repositories/in-memory.js +82 -0
  77. package/dist/repositories/index.d.ts +67 -0
  78. package/dist/repositories/index.js +16 -0
  79. package/dist/services/agent-config.service.d.ts +45 -0
  80. package/dist/services/agent-config.service.js +114 -0
  81. package/dist/services/agent-job.worker.d.ts +32 -0
  82. package/dist/services/agent-job.worker.js +97 -0
  83. package/dist/services/agent-runner.service.d.ts +35 -0
  84. package/dist/services/agent-runner.service.js +224 -0
  85. package/dist/services/agent.service.d.ts +171 -0
  86. package/dist/services/agent.service.js +329 -0
  87. package/dist/services/api-key.service.d.ts +41 -0
  88. package/dist/services/api-key.service.js +80 -0
  89. package/dist/services/auth.service.d.ts +133 -0
  90. package/dist/services/auth.service.js +411 -0
  91. package/dist/services/billing.service.d.ts +67 -0
  92. package/dist/services/billing.service.js +254 -0
  93. package/dist/services/chat-token.service.d.ts +29 -0
  94. package/dist/services/chat-token.service.js +113 -0
  95. package/dist/services/connector-registry.service.d.ts +156 -0
  96. package/dist/services/connector-registry.service.js +278 -0
  97. package/dist/services/conversation.service.d.ts +47 -0
  98. package/dist/services/conversation.service.js +101 -0
  99. package/dist/services/email-templates.d.ts +18 -0
  100. package/dist/services/email-templates.js +39 -0
  101. package/dist/services/email.service.d.ts +26 -0
  102. package/dist/services/email.service.js +42 -0
  103. package/dist/services/errors.d.ts +7 -0
  104. package/dist/services/errors.js +27 -0
  105. package/dist/services/in-memory-prepared-stream.store.d.ts +13 -0
  106. package/dist/services/in-memory-prepared-stream.store.js +35 -0
  107. package/dist/services/index.d.ts +13 -0
  108. package/dist/services/index.js +40 -0
  109. package/dist/services/mcp-client.service.d.ts +64 -0
  110. package/dist/services/mcp-client.service.js +157 -0
  111. package/dist/services/mcp-server.service.d.ts +44 -0
  112. package/dist/services/mcp-server.service.js +147 -0
  113. package/dist/services/oauth.service.d.ts +73 -0
  114. package/dist/services/oauth.service.js +174 -0
  115. package/dist/services/oauth2.service.d.ts +57 -0
  116. package/dist/services/oauth2.service.js +82 -0
  117. package/dist/services/orchestrator.service.d.ts +45 -0
  118. package/dist/services/orchestrator.service.js +180 -0
  119. package/dist/services/plan.service.d.ts +54 -0
  120. package/dist/services/plan.service.js +120 -0
  121. package/dist/services/prepared-stream.service.d.ts +23 -0
  122. package/dist/services/prepared-stream.service.js +43 -0
  123. package/dist/services/refresh-token.service.d.ts +38 -0
  124. package/dist/services/refresh-token.service.js +73 -0
  125. package/dist/services/secrets/crypto.d.ts +37 -0
  126. package/dist/services/secrets/crypto.js +110 -0
  127. package/dist/services/secrets/known-keys.d.ts +38 -0
  128. package/dist/services/secrets/known-keys.js +50 -0
  129. package/dist/services/secrets.service.d.ts +91 -0
  130. package/dist/services/secrets.service.js +193 -0
  131. package/dist/services/tenant-billing.service.d.ts +121 -0
  132. package/dist/services/tenant-billing.service.js +290 -0
  133. package/dist/services/tenant.service.d.ts +54 -0
  134. package/dist/services/tenant.service.js +96 -0
  135. package/dist/services/tool-registry.service.d.ts +42 -0
  136. package/dist/services/tool-registry.service.js +101 -0
  137. package/dist/services/upload.service.d.ts +37 -0
  138. package/dist/services/upload.service.js +84 -0
  139. package/dist/services/usage.service.d.ts +34 -0
  140. package/dist/services/usage.service.js +108 -0
  141. package/dist/types/agent.types.d.ts +160 -0
  142. package/dist/types/agent.types.js +2 -0
  143. package/dist/types/billing.types.d.ts +82 -0
  144. package/dist/types/billing.types.js +3 -0
  145. package/dist/types/config.types.d.ts +127 -0
  146. package/dist/types/config.types.js +9 -0
  147. package/dist/types/hooks.d.ts +85 -0
  148. package/dist/types/hooks.js +2 -0
  149. package/dist/types/index.d.ts +3 -0
  150. package/dist/types/index.js +19 -0
  151. package/package.json +36 -0
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GITHUB_OAUTH_SPEC = exports.GOOGLE_OAUTH_SPEC = exports.OAuthCallbackError = exports.OAUTH_STATE_TTL_MS = exports.OAUTH_STATE_COOKIE_PREFIX = void 0;
4
+ exports.buildAuthorizeUrl = buildAuthorizeUrl;
5
+ exports.completeAuthorization = completeAuthorization;
6
+ exports.getOAuthProviderSpec = getOAuthProviderSpec;
7
+ const crypto_1 = require("crypto");
8
+ /**
9
+ * Public name of the state cookie/header per provider. Adapters share the
10
+ * convention so a session started under one transport could (in theory) be
11
+ * resumed under another — and so the operations docs are consistent.
12
+ */
13
+ exports.OAUTH_STATE_COOKIE_PREFIX = 'af_oauth_state_';
14
+ exports.OAUTH_STATE_TTL_MS = 10 * 60 * 1000; // 10 min
15
+ /** Build the authorize URL + a fresh random state for the cookie/store. */
16
+ function buildAuthorizeUrl(spec, cfg) {
17
+ const state = (0, crypto_1.randomBytes)(16).toString('hex');
18
+ const params = new URLSearchParams({
19
+ client_id: cfg.clientId,
20
+ redirect_uri: cfg.callbackURL,
21
+ response_type: 'code',
22
+ state,
23
+ ...spec.authorizeExtras(),
24
+ });
25
+ return {
26
+ authorizeUrl: `${spec.authorizeUrl}?${params.toString()}`,
27
+ state,
28
+ };
29
+ }
30
+ /**
31
+ * Validate the callback against the expected state and exchange the
32
+ * authorization code for an access token + normalized profile.
33
+ *
34
+ * Returns the profile on success. Throws OAuthCallbackError with a stable
35
+ * `.code` on failure — adapters surface the code in the failure redirect.
36
+ */
37
+ async function completeAuthorization(input) {
38
+ if (input.providerError) {
39
+ throw new OAuthCallbackError(input.providerError, 'Provider returned an error');
40
+ }
41
+ if (!input.code) {
42
+ throw new OAuthCallbackError('missing_code', 'authorization code missing');
43
+ }
44
+ if (!input.expectedState || input.expectedState !== input.returnedState) {
45
+ throw new OAuthCallbackError('invalid_state', 'state mismatch');
46
+ }
47
+ let accessToken;
48
+ try {
49
+ accessToken = await exchangeCodeForToken({
50
+ tokenUrl: input.spec.tokenUrl,
51
+ clientId: input.cfg.clientId,
52
+ clientSecret: input.cfg.clientSecret,
53
+ code: input.code,
54
+ redirectUri: input.cfg.callbackURL,
55
+ });
56
+ }
57
+ catch (err) {
58
+ throw new OAuthCallbackError('token_exchange_failed', err.message);
59
+ }
60
+ try {
61
+ return await input.spec.fetchProfile(accessToken);
62
+ }
63
+ catch (err) {
64
+ throw new OAuthCallbackError('profile_fetch_failed', err.message);
65
+ }
66
+ }
67
+ /** Stable error shape adapters can pattern-match on for the failure redirect. */
68
+ class OAuthCallbackError extends Error {
69
+ constructor(code, message) {
70
+ super(message);
71
+ this.name = 'OAuthCallbackError';
72
+ this.code = code;
73
+ }
74
+ }
75
+ exports.OAuthCallbackError = OAuthCallbackError;
76
+ // ─── Token exchange ────────────────────────────────────────────────────────
77
+ async function exchangeCodeForToken(input) {
78
+ const body = new URLSearchParams({
79
+ grant_type: 'authorization_code',
80
+ client_id: input.clientId,
81
+ client_secret: input.clientSecret,
82
+ code: input.code,
83
+ redirect_uri: input.redirectUri,
84
+ });
85
+ const res = await fetch(input.tokenUrl, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/x-www-form-urlencoded',
89
+ Accept: 'application/json',
90
+ },
91
+ body: body.toString(),
92
+ });
93
+ if (!res.ok)
94
+ throw new Error(`token endpoint returned ${res.status}`);
95
+ const json = (await res.json());
96
+ if (!json.access_token)
97
+ throw new Error('no access_token in response');
98
+ return json.access_token;
99
+ }
100
+ // ─── Provider specs ────────────────────────────────────────────────────────
101
+ exports.GOOGLE_OAUTH_SPEC = {
102
+ name: 'google',
103
+ authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
104
+ tokenUrl: 'https://oauth2.googleapis.com/token',
105
+ authorizeExtras: () => ({
106
+ scope: 'openid email profile',
107
+ access_type: 'offline',
108
+ prompt: 'select_account',
109
+ }),
110
+ async fetchProfile(accessToken) {
111
+ const r = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
112
+ headers: { Authorization: `Bearer ${accessToken}` },
113
+ });
114
+ if (!r.ok)
115
+ throw new Error(`userinfo ${r.status}`);
116
+ const p = (await r.json());
117
+ return {
118
+ provider: 'google',
119
+ providerId: p.sub,
120
+ email: p.email,
121
+ emailVerified: p.email_verified === true,
122
+ name: p.name,
123
+ metadata: { avatar: p.picture, locale: p.locale },
124
+ };
125
+ },
126
+ };
127
+ exports.GITHUB_OAUTH_SPEC = {
128
+ name: 'github',
129
+ authorizeUrl: 'https://github.com/login/oauth/authorize',
130
+ tokenUrl: 'https://github.com/login/oauth/access_token',
131
+ authorizeExtras: () => ({ scope: 'user:email' }),
132
+ async fetchProfile(accessToken) {
133
+ const headers = {
134
+ Authorization: `Bearer ${accessToken}`,
135
+ Accept: 'application/vnd.github+json',
136
+ 'User-Agent': 'agentforge',
137
+ };
138
+ const userRes = await fetch('https://api.github.com/user', { headers });
139
+ if (!userRes.ok)
140
+ throw new Error(`/user ${userRes.status}`);
141
+ const u = (await userRes.json());
142
+ // /user.email is null when private. Fetch /user/emails to get the verified primary.
143
+ let email = u.email ?? undefined;
144
+ if (!email) {
145
+ const emailsRes = await fetch('https://api.github.com/user/emails', { headers });
146
+ if (emailsRes.ok) {
147
+ const emails = (await emailsRes.json());
148
+ email =
149
+ emails.find((e) => e.primary && e.verified)?.email ??
150
+ emails.find((e) => e.verified)?.email;
151
+ }
152
+ }
153
+ return {
154
+ provider: 'github',
155
+ providerId: String(u.id),
156
+ email,
157
+ emailVerified: !!email, // GitHub only returns verified emails through /user/emails
158
+ name: u.name || u.login,
159
+ metadata: {
160
+ username: u.login,
161
+ avatar: u.avatar_url,
162
+ profileUrl: u.html_url,
163
+ },
164
+ };
165
+ },
166
+ };
167
+ /** Look up a built-in spec by name. Returns undefined for unknown names. */
168
+ function getOAuthProviderSpec(name) {
169
+ if (name === 'google')
170
+ return exports.GOOGLE_OAUTH_SPEC;
171
+ if (name === 'github')
172
+ return exports.GITHUB_OAUTH_SPEC;
173
+ return undefined;
174
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Endpoint + scope configuration for a single OAuth2 provider. The
3
+ * ConnectorRegistry holds one of these per registered connector.
4
+ *
5
+ * Why per-connector and not global: each provider has different scopes,
6
+ * different authorize/token URLs, and may require provider-specific extras
7
+ * like Google's `access_type=offline` for refresh tokens.
8
+ */
9
+ export interface OAuth2ProviderConfig {
10
+ authorizeUrl: string;
11
+ tokenUrl: string;
12
+ clientId: string;
13
+ clientSecret: string;
14
+ /** Space-delimited scopes when building the authorize URL. */
15
+ scopes: string[];
16
+ /** Extra query params merged into the authorize URL (e.g. Google's
17
+ * `access_type=offline`, `prompt=consent`). */
18
+ authorizeExtras?: Record<string, string>;
19
+ /** PKCE: defaults to true for security, but providers that don't support
20
+ * it (rare in 2026) can opt out. */
21
+ usePkce?: boolean;
22
+ }
23
+ export interface AuthorizeUrlResult {
24
+ url: string;
25
+ state: string;
26
+ /** Opaque payload the caller must persist (e.g. in a cookie/session) and
27
+ * hand back to `exchangeCode` so PKCE verification can complete. */
28
+ pkceVerifier?: string;
29
+ }
30
+ export interface TokenSet {
31
+ accessToken: string;
32
+ refreshToken?: string;
33
+ /** Seconds remaining when issued; absolute expiry is computed by the
34
+ * caller against `Date.now()` to avoid clock drift inside this service. */
35
+ expiresIn?: number;
36
+ scope?: string;
37
+ tokenType?: string;
38
+ }
39
+ interface OAuth2ServiceDeps {
40
+ /** Override for tests. */
41
+ fetchImpl?: typeof fetch;
42
+ }
43
+ /**
44
+ * Provider-agnostic OAuth2 dance. Holds zero per-user state — callers are
45
+ * responsible for storing the issued tokens. Designed so that the same
46
+ * instance can serve every connector (Google, Slack, Notion, …); the
47
+ * provider config is passed in on each call.
48
+ */
49
+ export declare class OAuth2Service {
50
+ private readonly fetchImpl;
51
+ constructor(deps?: OAuth2ServiceDeps);
52
+ buildAuthorizeUrl(cfg: OAuth2ProviderConfig, redirectUri: string): AuthorizeUrlResult;
53
+ exchangeCode(cfg: OAuth2ProviderConfig, code: string, redirectUri: string, pkceVerifier?: string): Promise<TokenSet>;
54
+ refresh(cfg: OAuth2ProviderConfig, refreshToken: string): Promise<TokenSet>;
55
+ private postToken;
56
+ }
57
+ export {};
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OAuth2Service = void 0;
4
+ const crypto_1 = require("crypto");
5
+ /**
6
+ * Provider-agnostic OAuth2 dance. Holds zero per-user state — callers are
7
+ * responsible for storing the issued tokens. Designed so that the same
8
+ * instance can serve every connector (Google, Slack, Notion, …); the
9
+ * provider config is passed in on each call.
10
+ */
11
+ class OAuth2Service {
12
+ constructor(deps = {}) {
13
+ this.fetchImpl = deps.fetchImpl ?? fetch;
14
+ }
15
+ buildAuthorizeUrl(cfg, redirectUri) {
16
+ const state = (0, crypto_1.randomBytes)(16).toString('hex');
17
+ const usePkce = cfg.usePkce !== false;
18
+ const params = {
19
+ response_type: 'code',
20
+ client_id: cfg.clientId,
21
+ redirect_uri: redirectUri,
22
+ scope: cfg.scopes.join(' '),
23
+ state,
24
+ ...(cfg.authorizeExtras ?? {}),
25
+ };
26
+ let pkceVerifier;
27
+ if (usePkce) {
28
+ pkceVerifier = (0, crypto_1.randomBytes)(32).toString('base64url');
29
+ const challenge = (0, crypto_1.createHash)('sha256').update(pkceVerifier).digest('base64url');
30
+ params.code_challenge = challenge;
31
+ params.code_challenge_method = 'S256';
32
+ }
33
+ const url = new URL(cfg.authorizeUrl);
34
+ for (const [k, v] of Object.entries(params))
35
+ url.searchParams.set(k, v);
36
+ return { url: url.toString(), state, pkceVerifier };
37
+ }
38
+ async exchangeCode(cfg, code, redirectUri, pkceVerifier) {
39
+ const body = new URLSearchParams({
40
+ grant_type: 'authorization_code',
41
+ code,
42
+ redirect_uri: redirectUri,
43
+ client_id: cfg.clientId,
44
+ client_secret: cfg.clientSecret,
45
+ });
46
+ if (pkceVerifier)
47
+ body.set('code_verifier', pkceVerifier);
48
+ return this.postToken(cfg.tokenUrl, body);
49
+ }
50
+ async refresh(cfg, refreshToken) {
51
+ const body = new URLSearchParams({
52
+ grant_type: 'refresh_token',
53
+ refresh_token: refreshToken,
54
+ client_id: cfg.clientId,
55
+ client_secret: cfg.clientSecret,
56
+ });
57
+ return this.postToken(cfg.tokenUrl, body);
58
+ }
59
+ async postToken(tokenUrl, body) {
60
+ const res = await this.fetchImpl(tokenUrl, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'content-type': 'application/x-www-form-urlencoded',
64
+ accept: 'application/json',
65
+ },
66
+ body: body.toString(),
67
+ });
68
+ if (!res.ok) {
69
+ const text = await res.text().catch(() => '');
70
+ throw new Error(`OAuth2 token endpoint ${tokenUrl} returned ${res.status}: ${text}`);
71
+ }
72
+ const json = (await res.json());
73
+ return {
74
+ accessToken: json.access_token,
75
+ refreshToken: json.refresh_token,
76
+ expiresIn: json.expires_in,
77
+ scope: json.scope,
78
+ tokenType: json.token_type,
79
+ };
80
+ }
81
+ }
82
+ exports.OAuth2Service = OAuth2Service;
@@ -0,0 +1,45 @@
1
+ import type { AgentResponse, AnthropicMessage, SubAgentDelegation, ToolExecutionContext } from '../types/agent.types';
2
+ import type { AgentDefinition, AnthropicConfig } from '../types/config.types';
3
+ import type { AgentRunnerService } from './agent-runner.service';
4
+ import type { Logger } from './tool-registry.service';
5
+ /**
6
+ * Operational error. Adapters map `.status` to an HTTP code.
7
+ */
8
+ export declare class OrchestratorError extends Error {
9
+ status: number;
10
+ code: 'agent_not_found';
11
+ constructor(code: 'agent_not_found', message: string);
12
+ }
13
+ export interface OrchestratorServiceOptions {
14
+ agents: AgentDefinition[];
15
+ logger?: Logger;
16
+ }
17
+ /**
18
+ * Multi-agent workflows. Orchestrator agents can delegate tasks to specialized
19
+ * subagents via auto-generated delegation tools that Claude sees as ordinary
20
+ * tools. The orchestrator runs the loop, invokes the subagent through the
21
+ * AgentRunnerService, and accumulates token usage across both layers.
22
+ *
23
+ * Framework-free; the Nest binding wraps this in a factory provider, the
24
+ * Express binding constructs it directly via createAgentForge().
25
+ */
26
+ export declare class OrchestratorService {
27
+ private readonly anthropicConfig;
28
+ private readonly runner;
29
+ private readonly agentsMap;
30
+ private readonly client;
31
+ private readonly logger;
32
+ constructor(anthropicConfig: AnthropicConfig, runner: AgentRunnerService, opts: OrchestratorServiceOptions);
33
+ /**
34
+ * Run an agent. Orchestrators automatically get delegation tools injected.
35
+ * Non-orchestrator agents fall straight through to the runner.
36
+ */
37
+ run(agentId: string, messages: AnthropicMessage[], context: ToolExecutionContext): Promise<AgentResponse & {
38
+ delegations?: SubAgentDelegation[];
39
+ }>;
40
+ private runOrchestratorLoop;
41
+ private buildDelegationTools;
42
+ private getAgent;
43
+ listAgents(): AgentDefinition[];
44
+ getAgentById(id: string): AgentDefinition | undefined;
45
+ }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OrchestratorService = exports.OrchestratorError = void 0;
7
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
+ const crypto_1 = require("crypto");
9
+ const noopLogger = {
10
+ log: () => { }, warn: () => { }, debug: () => { }, error: () => { },
11
+ };
12
+ /**
13
+ * Operational error. Adapters map `.status` to an HTTP code.
14
+ */
15
+ class OrchestratorError extends Error {
16
+ constructor(code, message) {
17
+ super(message);
18
+ this.code = code;
19
+ this.status = 404;
20
+ }
21
+ }
22
+ exports.OrchestratorError = OrchestratorError;
23
+ /**
24
+ * Multi-agent workflows. Orchestrator agents can delegate tasks to specialized
25
+ * subagents via auto-generated delegation tools that Claude sees as ordinary
26
+ * tools. The orchestrator runs the loop, invokes the subagent through the
27
+ * AgentRunnerService, and accumulates token usage across both layers.
28
+ *
29
+ * Framework-free; the Nest binding wraps this in a factory provider, the
30
+ * Express binding constructs it directly via createAgentForge().
31
+ */
32
+ class OrchestratorService {
33
+ constructor(anthropicConfig, runner, opts) {
34
+ this.anthropicConfig = anthropicConfig;
35
+ this.runner = runner;
36
+ this.agentsMap = new Map();
37
+ this.client = new sdk_1.default({
38
+ apiKey: anthropicConfig.apiKey,
39
+ baseURL: anthropicConfig.baseURL,
40
+ });
41
+ this.logger = opts.logger ?? noopLogger;
42
+ for (const agent of opts.agents) {
43
+ this.agentsMap.set(agent.id, agent);
44
+ }
45
+ }
46
+ /**
47
+ * Run an agent. Orchestrators automatically get delegation tools injected.
48
+ * Non-orchestrator agents fall straight through to the runner.
49
+ */
50
+ async run(agentId, messages, context) {
51
+ const agent = this.getAgent(agentId);
52
+ if (!agent.canOrchestrate || !agent.subAgents?.length) {
53
+ return this.runner.run(agent, messages, context);
54
+ }
55
+ this.logger.debug(`Running orchestrator "${agentId}" with subagents: ${agent.subAgents.join(', ')}`);
56
+ return this.runOrchestratorLoop(agent, messages, context);
57
+ }
58
+ async runOrchestratorLoop(orchestrator, messages, context) {
59
+ const delegations = [];
60
+ const delegationTools = this.buildDelegationTools(orchestrator);
61
+ const model = orchestrator.model ?? this.anthropicConfig.defaultModel ?? 'claude-opus-4-6';
62
+ const maxTokens = orchestrator.maxTokens ?? this.anthropicConfig.defaultMaxTokens ?? 4096;
63
+ const anthropicTools = delegationTools.map((t) => ({
64
+ name: t.name,
65
+ description: t.description,
66
+ input_schema: t.inputSchema,
67
+ }));
68
+ let currentMessages = [...messages];
69
+ let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
70
+ let finalContent = '';
71
+ // ─── Agentic loop ──────────────────────────────────────────────────────
72
+ while (true) {
73
+ const response = await this.client.messages.create({
74
+ model,
75
+ max_tokens: maxTokens,
76
+ system: orchestrator.systemPrompt,
77
+ messages: currentMessages,
78
+ tools: anthropicTools,
79
+ });
80
+ totalUsage = {
81
+ inputTokens: totalUsage.inputTokens + response.usage.input_tokens,
82
+ outputTokens: totalUsage.outputTokens + response.usage.output_tokens,
83
+ totalTokens: totalUsage.totalTokens +
84
+ response.usage.input_tokens +
85
+ response.usage.output_tokens,
86
+ };
87
+ if (response.stop_reason === 'tool_use') {
88
+ const assistantMsg = { role: 'assistant', content: response.content };
89
+ currentMessages = [...currentMessages, assistantMsg];
90
+ const toolResults = [];
91
+ for (const block of response.content) {
92
+ if (block.type !== 'tool_use')
93
+ continue;
94
+ const delegateTool = delegationTools.find((t) => t.name === block.name);
95
+ if (!delegateTool)
96
+ continue;
97
+ const { task } = block.input;
98
+ const { subAgentId } = delegateTool;
99
+ this.logger.log(`Orchestrator "${orchestrator.id}" → "${subAgentId}": ${task.slice(0, 80)}...`);
100
+ const subAgent = this.getAgent(subAgentId);
101
+ const subMessages = [{ role: 'user', content: task }];
102
+ const subResult = await this.runner.run(subAgent, subMessages, {
103
+ ...context,
104
+ agentId: subAgentId,
105
+ });
106
+ totalUsage.inputTokens += subResult.usage.inputTokens;
107
+ totalUsage.outputTokens += subResult.usage.outputTokens;
108
+ totalUsage.totalTokens += subResult.usage.totalTokens;
109
+ delegations.push({
110
+ subAgentId,
111
+ task,
112
+ result: subResult.content,
113
+ usage: subResult.usage,
114
+ });
115
+ toolResults.push({
116
+ type: 'tool_result',
117
+ tool_use_id: block.id,
118
+ content: subResult.content,
119
+ });
120
+ }
121
+ currentMessages = [...currentMessages, { role: 'user', content: toolResults }];
122
+ }
123
+ else {
124
+ finalContent = response.content
125
+ .filter((b) => b.type === 'text')
126
+ .map((b) => b.text)
127
+ .join('');
128
+ break;
129
+ }
130
+ }
131
+ return {
132
+ messageId: (0, crypto_1.randomUUID)(),
133
+ conversationId: context.conversationId,
134
+ content: finalContent,
135
+ role: 'assistant',
136
+ delegations: delegations.length > 0 ? delegations : undefined,
137
+ usage: totalUsage,
138
+ model,
139
+ stopReason: 'end_turn',
140
+ createdAt: new Date(),
141
+ };
142
+ }
143
+ // ─── Helpers ───────────────────────────────────────────────────────────────
144
+ buildDelegationTools(orchestrator) {
145
+ return (orchestrator.subAgents ?? []).map((subAgentId) => {
146
+ const sub = this.agentsMap.get(subAgentId);
147
+ const description = sub
148
+ ? `Delegate a task to the "${sub.name}" specialist agent. ${sub.description ?? ''}`
149
+ : `Delegate a task to subagent "${subAgentId}"`;
150
+ return {
151
+ name: `delegate_to_${subAgentId.replace(/[^a-zA-Z0-9]/g, '_')}`,
152
+ description,
153
+ subAgentId,
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {
157
+ task: {
158
+ type: 'string',
159
+ description: 'The specific task or question to send to this specialist',
160
+ },
161
+ },
162
+ required: ['task'],
163
+ },
164
+ };
165
+ });
166
+ }
167
+ getAgent(agentId) {
168
+ const agent = this.agentsMap.get(agentId);
169
+ if (!agent)
170
+ throw new OrchestratorError('agent_not_found', `Agent "${agentId}" not found`);
171
+ return agent;
172
+ }
173
+ listAgents() {
174
+ return Array.from(this.agentsMap.values());
175
+ }
176
+ getAgentById(id) {
177
+ return this.agentsMap.get(id);
178
+ }
179
+ }
180
+ exports.OrchestratorService = OrchestratorService;
@@ -0,0 +1,54 @@
1
+ import type { PlanRepository } from '../repositories';
2
+ import type { Plan, NewPlan, PlanPatch } from '../domain/plan';
3
+ import type { PlanDefinition } from '../types/config.types';
4
+ export interface PlanServiceOptions {
5
+ /**
6
+ * Plans to seed the DB with on first boot (i.e. when `af_plans` is empty).
7
+ * After the table has at least one row, this seed is ignored — the DB is
8
+ * the source of truth. Pass your `config.billing.plans` here so existing
9
+ * deployments migrate transparently.
10
+ */
11
+ seedPlans?: PlanDefinition[];
12
+ }
13
+ /**
14
+ * Reads and writes plan rows from the DB, with an in-process cache invalidated
15
+ * on every mutation. Hot-path reads (`getPlan(id)`) are O(n) over a tiny set,
16
+ * which is fine for the few plans most apps need.
17
+ *
18
+ * Single-instance assumption: when running multiple API instances, mutations
19
+ * via admin endpoints invalidate only the local cache. The next read on other
20
+ * instances still serves stale data until their own TTL expires. For most B2B
21
+ * scenarios (plan edits are rare, eventually-consistent is OK) this is fine;
22
+ * if you need strict consistency, set `cacheTtlMs: 0`.
23
+ */
24
+ export declare class PlanService {
25
+ private readonly repo;
26
+ private readonly opts;
27
+ private cache;
28
+ private cacheLoadedAt;
29
+ /** Default TTL: 60 seconds. */
30
+ private readonly cacheTtlMs;
31
+ constructor(repo: PlanRepository, opts?: PlanServiceOptions);
32
+ /**
33
+ * Call once at boot. Seeds the table from `opts.seedPlans` if empty, then
34
+ * warms the cache. Safe to call multiple times.
35
+ */
36
+ initialize(): Promise<void>;
37
+ list(): Promise<Plan[]>;
38
+ listAll(): Promise<Plan[]>;
39
+ getPlan(id: string): Promise<Plan | undefined>;
40
+ getDefault(): Promise<Plan | undefined>;
41
+ getDefaultId(): Promise<string>;
42
+ create(plan: NewPlan): Promise<Plan>;
43
+ update(id: string, patch: PlanPatch): Promise<Plan>;
44
+ deactivate(id: string): Promise<void>;
45
+ /** Force the next read to bypass the cache. Useful in tests. */
46
+ invalidate(): void;
47
+ private refresh;
48
+ private isCacheFresh;
49
+ }
50
+ export declare class PlanServiceError extends Error {
51
+ status: number;
52
+ code: 'plan_exists' | 'plan_not_found' | 'plan_is_default';
53
+ constructor(code: 'plan_exists' | 'plan_not_found' | 'plan_is_default', message: string);
54
+ }