@framers/agentos 0.1.98 → 0.1.100

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 (53) hide show
  1. package/README.md +60 -13
  2. package/dist/api/agency.d.ts +51 -0
  3. package/dist/api/agency.d.ts.map +1 -0
  4. package/dist/api/agency.js +814 -0
  5. package/dist/api/agency.js.map +1 -0
  6. package/dist/api/agent.d.ts +15 -54
  7. package/dist/api/agent.d.ts.map +1 -1
  8. package/dist/api/agent.js +14 -7
  9. package/dist/api/agent.js.map +1 -1
  10. package/dist/api/generateText.d.ts +16 -0
  11. package/dist/api/generateText.d.ts.map +1 -1
  12. package/dist/api/generateText.js.map +1 -1
  13. package/dist/api/hitl.d.ts +139 -0
  14. package/dist/api/hitl.d.ts.map +1 -0
  15. package/dist/api/hitl.js +211 -0
  16. package/dist/api/hitl.js.map +1 -0
  17. package/dist/api/strategies/debate.d.ts +16 -0
  18. package/dist/api/strategies/debate.d.ts.map +1 -0
  19. package/dist/api/strategies/debate.js +118 -0
  20. package/dist/api/strategies/debate.js.map +1 -0
  21. package/dist/api/strategies/hierarchical.d.ts +20 -0
  22. package/dist/api/strategies/hierarchical.d.ts.map +1 -0
  23. package/dist/api/strategies/hierarchical.js +140 -0
  24. package/dist/api/strategies/hierarchical.js.map +1 -0
  25. package/dist/api/strategies/index.d.ts +41 -0
  26. package/dist/api/strategies/index.d.ts.map +1 -0
  27. package/dist/api/strategies/index.js +95 -0
  28. package/dist/api/strategies/index.js.map +1 -0
  29. package/dist/api/strategies/parallel.d.ts +17 -0
  30. package/dist/api/strategies/parallel.d.ts.map +1 -0
  31. package/dist/api/strategies/parallel.js +122 -0
  32. package/dist/api/strategies/parallel.js.map +1 -0
  33. package/dist/api/strategies/review-loop.d.ts +18 -0
  34. package/dist/api/strategies/review-loop.d.ts.map +1 -0
  35. package/dist/api/strategies/review-loop.js +153 -0
  36. package/dist/api/strategies/review-loop.js.map +1 -0
  37. package/dist/api/strategies/sequential.d.ts +15 -0
  38. package/dist/api/strategies/sequential.d.ts.map +1 -0
  39. package/dist/api/strategies/sequential.js +205 -0
  40. package/dist/api/strategies/sequential.js.map +1 -0
  41. package/dist/api/strategies/shared.d.ts +36 -0
  42. package/dist/api/strategies/shared.d.ts.map +1 -0
  43. package/dist/api/strategies/shared.js +82 -0
  44. package/dist/api/strategies/shared.js.map +1 -0
  45. package/dist/api/types.d.ts +808 -0
  46. package/dist/api/types.d.ts.map +1 -0
  47. package/dist/api/types.js +25 -0
  48. package/dist/api/types.js.map +1 -0
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -0
  52. package/dist/index.js.map +1 -1
  53. package/package.json +1 -1
@@ -0,0 +1,814 @@
1
+ /**
2
+ * @file agency.ts
3
+ * Multi-agent agency factory for the AgentOS high-level API.
4
+ *
5
+ * `agency()` accepts an {@link AgencyOptions} configuration, compiles the
6
+ * requested orchestration strategy, wires resource controls, and returns a
7
+ * single {@link Agent}-compatible interface that coordinates all sub-agents.
8
+ *
9
+ * The returned instance exposes `generate`, `stream`, `session`, `usage`, and
10
+ * `close` — identical surface to a single `agent()` instance — so callers can
11
+ * swap between them transparently.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { agency, hitl } from '@framers/agentos';
16
+ *
17
+ * const myAgency = agency({
18
+ * model: 'openai:gpt-4o',
19
+ * strategy: 'sequential',
20
+ * agents: {
21
+ * researcher: { instructions: 'Find relevant information.' },
22
+ * writer: { instructions: 'Write a clear summary.' },
23
+ * },
24
+ * controls: { maxTotalTokens: 50_000, onLimitReached: 'warn' },
25
+ * hitl: { approvals: { beforeTool: ['delete'] }, handler: hitl.autoApprove() },
26
+ * });
27
+ *
28
+ * const result = await myAgency.generate('Summarise recent AI research.');
29
+ * console.log(result.text);
30
+ * ```
31
+ */
32
+ import { compileStrategy, isAgent } from './strategies/index.js';
33
+ import { AgencyConfigError } from './types.js';
34
+ // ---------------------------------------------------------------------------
35
+ // Public factory
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Creates a multi-agent agency that coordinates a named roster of sub-agents
39
+ * using the specified orchestration strategy.
40
+ *
41
+ * The agency validates configuration immediately and throws an
42
+ * {@link AgencyConfigError} on any structural problem so issues surface at
43
+ * wiring time rather than the first call.
44
+ *
45
+ * @param opts - Full agency configuration including the `agents` roster, optional
46
+ * `strategy`, `controls`, `hitl`, and `observability` settings.
47
+ * @returns An {@link Agent} instance whose `generate` / `stream` / `session` methods
48
+ * invoke the compiled strategy over the configured sub-agents.
49
+ * @throws {AgencyConfigError} When the configuration is structurally invalid
50
+ * (e.g. no agents defined, emergent enabled without hierarchical strategy,
51
+ * HITL approvals configured without a handler, parallel/debate without a
52
+ * synthesis model).
53
+ */
54
+ export function agency(opts) {
55
+ // 1. Validate options — throw early on bad configuration.
56
+ validateAgencyOptions(opts);
57
+ // 1b. Forward agency-level `beforeTool` to sub-agent permissions.
58
+ // This ensures that tool-level HITL approval is enforced at the
59
+ // individual agent layer via `permissions.requireApproval`.
60
+ const resolvedAgents = forwardBeforeToolToSubAgents(opts.agents, opts);
61
+ // 2. Compile the orchestration strategy into an executable CompiledStrategy.
62
+ // When `adaptive` is true the strategy dispatcher wraps the chosen strategy
63
+ // with an implicit hierarchical manager.
64
+ const chosenStrategy = opts.adaptive
65
+ ? 'hierarchical'
66
+ : (opts.strategy ?? 'sequential');
67
+ const strategy = compileStrategy(chosenStrategy, resolvedAgents, opts);
68
+ // 3. Extract resource controls (may be undefined).
69
+ const controls = opts.controls;
70
+ const agencyName = opts.name ?? '__agency__';
71
+ const agencyUsage = emptyUsageTotals();
72
+ // 4. In-memory session store keyed by session ID.
73
+ const sessions = new Map();
74
+ const sessionUsage = new Map();
75
+ // ---------------------------------------------------------------------------
76
+ // Shared execute wrapper — applies resource limit checks and fires callbacks.
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * Execute the compiled strategy for a given prompt, then check resource
80
+ * limits and fire lifecycle callbacks.
81
+ *
82
+ * @param prompt - User-facing prompt text.
83
+ * @param execOpts - Optional per-call overrides forwarded to the strategy.
84
+ * @returns The raw strategy result object (includes `text`, `agentCalls`, `usage`).
85
+ */
86
+ const wrappedExecute = async (prompt, execOpts, sessionId) => {
87
+ const start = Date.now();
88
+ opts.on?.agentStart?.({
89
+ agent: agencyName,
90
+ input: prompt,
91
+ timestamp: start,
92
+ });
93
+ try {
94
+ // Run input guardrails on the prompt before strategy execution.
95
+ const guardConfig = normalizeGuardrails(opts.guardrails);
96
+ const inputGuards = guardConfig?.input ?? [];
97
+ const outputGuards = guardConfig?.output ?? [];
98
+ let sanitizedPrompt = prompt;
99
+ if (inputGuards.length) {
100
+ sanitizedPrompt = await runGuardrails(sanitizedPrompt, inputGuards, 'input', opts.on);
101
+ }
102
+ // Inject RAG context into the prompt when RAG is configured.
103
+ if (opts.rag) {
104
+ sanitizedPrompt = await injectRagContext(sanitizedPrompt, opts.rag);
105
+ }
106
+ // When structured output is configured, append a JSON schema hint to
107
+ // nudge the LLM into producing parseable output.
108
+ if (opts.output) {
109
+ sanitizedPrompt = appendSchemaHint(sanitizedPrompt, opts.output);
110
+ }
111
+ // Execute the compiled multi-agent strategy.
112
+ const result = (await strategy.execute(sanitizedPrompt, execOpts));
113
+ const elapsedMs = Date.now() - start;
114
+ // Run output guardrails on the result text.
115
+ if (outputGuards.length && typeof result.text === 'string') {
116
+ result.text = await runGuardrails(result.text, outputGuards, 'output', opts.on);
117
+ }
118
+ // Parse structured output through Zod schema when configured.
119
+ if (opts.output && typeof result.text === 'string') {
120
+ result.parsed = parseStructuredOutput(result.text, opts.output);
121
+ }
122
+ // Check resource limits and fire callbacks / throw if configured.
123
+ if (controls) {
124
+ checkLimits(controls, result, elapsedMs, opts.on);
125
+ }
126
+ // Persist aggregate usage totals at the agency and session levels.
127
+ const resultUsage = normalizeUsage(result.usage);
128
+ addUsageTotals(agencyUsage, resultUsage);
129
+ if (sessionId) {
130
+ addUsageTotals(getSessionUsage(sessionUsage, sessionId), resultUsage);
131
+ }
132
+ const finalResult = await maybeApproveFinalResult(opts, agencyName, result, elapsedMs);
133
+ // Fire the agentEnd callback with the agency as the pseudo-agent name.
134
+ opts.on?.agentEnd?.({
135
+ agent: agencyName,
136
+ output: finalResult.text ?? '',
137
+ durationMs: elapsedMs,
138
+ timestamp: Date.now(),
139
+ });
140
+ return finalResult;
141
+ }
142
+ catch (error) {
143
+ opts.on?.error?.({
144
+ agent: agencyName,
145
+ error: error instanceof Error ? error : new Error(String(error)),
146
+ timestamp: Date.now(),
147
+ });
148
+ throw error;
149
+ }
150
+ };
151
+ // ---------------------------------------------------------------------------
152
+ // Returned Agent interface
153
+ // ---------------------------------------------------------------------------
154
+ /**
155
+ * Build the core agent object. `listen` and `connect` are conditionally
156
+ * attached below based on the presence of `opts.voice` and `opts.channels`.
157
+ */
158
+ const agentObj = {
159
+ /**
160
+ * Runs the agency's strategy for the given prompt and returns the final
161
+ * aggregated result (non-streaming).
162
+ *
163
+ * @param prompt - User prompt text.
164
+ * @param opts - Optional per-call overrides.
165
+ * @returns The aggregated result including `text`, `agentCalls`, and `usage`.
166
+ */
167
+ async generate(prompt, generateOpts) {
168
+ return wrappedExecute(prompt, generateOpts);
169
+ },
170
+ /**
171
+ * Streams the strategy execution. For strategies that do not natively
172
+ * support token-by-token streaming, the full result is buffered and emitted
173
+ * as a single text chunk.
174
+ *
175
+ * @param prompt - User prompt text.
176
+ * @param streamOpts - Optional per-call overrides.
177
+ * @returns An object with `textStream`, `fullStream`, and awaitable `text`/`usage` promises.
178
+ */
179
+ stream(prompt, streamOpts) {
180
+ return strategy.stream(prompt, streamOpts);
181
+ },
182
+ /**
183
+ * Returns (or creates) a named conversation session backed by the agency's
184
+ * strategy. Each session maintains its own ordered message history.
185
+ *
186
+ * @param id - Optional stable session ID; auto-generated via `crypto.randomUUID()`
187
+ * when omitted.
188
+ * @returns The session object for the given ID.
189
+ */
190
+ session(id) {
191
+ const sessionId = id ?? crypto.randomUUID();
192
+ if (!sessions.has(sessionId)) {
193
+ /** Per-session message history as simple role/content pairs. */
194
+ const history = [];
195
+ const sessionObj = {
196
+ id: sessionId,
197
+ /**
198
+ * Sends a user message through the agency strategy and appends both
199
+ * turns to session history.
200
+ *
201
+ * @param text - User message text.
202
+ * @returns The aggregated strategy result.
203
+ */
204
+ async send(text) {
205
+ history.push({ role: 'user', content: text });
206
+ const result = await wrappedExecute(buildSessionPrompt(history.slice(0, -1), text), undefined, sessionId);
207
+ history.push({ role: 'assistant', content: result.text ?? '' });
208
+ return result;
209
+ },
210
+ /**
211
+ * Streams a user message through the agency strategy, feeding prior
212
+ * conversation history into the prompt — matching the behaviour of
213
+ * `session.send()`. The assistant turn is appended to history once
214
+ * the full streamed text is resolved.
215
+ *
216
+ * @param text - User message text.
217
+ * @returns A streaming result compatible with `StreamTextResult`.
218
+ */
219
+ stream(text) {
220
+ // Push the user turn before building the prompt so the prior
221
+ // history (everything before the new turn) is included.
222
+ history.push({ role: 'user', content: text });
223
+ const fullPrompt = buildSessionPrompt(history.slice(0, -1), text);
224
+ const streamResult = strategy.stream(fullPrompt);
225
+ // Append the assistant reply to history once streaming resolves.
226
+ streamResult.text.then((assistantText) => {
227
+ history.push({ role: 'assistant', content: assistantText });
228
+ }).catch(() => {
229
+ // Ignore resolution failures — history will just lack this turn.
230
+ });
231
+ return streamResult;
232
+ },
233
+ /** Returns a snapshot of the session's conversation history. */
234
+ messages() {
235
+ return [...history];
236
+ },
237
+ /**
238
+ * Returns stub usage totals for this session.
239
+ * Real per-session accounting requires a usage ledger — see `AgentOptions.usageLedger`.
240
+ */
241
+ async usage() {
242
+ return { ...getSessionUsage(sessionUsage, sessionId) };
243
+ },
244
+ /** Clears all messages from this session's history. */
245
+ clear() {
246
+ history.length = 0;
247
+ },
248
+ };
249
+ sessions.set(sessionId, sessionObj);
250
+ }
251
+ return sessions.get(sessionId);
252
+ },
253
+ /**
254
+ * Returns stub cumulative usage totals for the agency.
255
+ * Real accounting requires a usage ledger — see `AgentOptions.usageLedger`.
256
+ */
257
+ async usage(sessionId) {
258
+ return sessionId
259
+ ? { ...getSessionUsage(sessionUsage, sessionId) }
260
+ : { ...agencyUsage };
261
+ },
262
+ /**
263
+ * Tears down all sessions and closes any pre-built `Agent` instances passed
264
+ * in `opts.agents`.
265
+ */
266
+ async close() {
267
+ sessions.clear();
268
+ // Gracefully close any pre-built Agent instances in the roster.
269
+ for (const agentOrConfig of Object.values(opts.agents)) {
270
+ if (isAgent(agentOrConfig)) {
271
+ await agentOrConfig.close?.();
272
+ }
273
+ }
274
+ },
275
+ };
276
+ // ---------------------------------------------------------------------------
277
+ // listen() — voice WebSocket transport
278
+ // ---------------------------------------------------------------------------
279
+ /**
280
+ * When `opts.voice.enabled` is set, attach a `listen()` method that starts a
281
+ * local WebSocket server and exposes a port for real-time audio I/O.
282
+ *
283
+ * The WebSocket server acts as the transport layer; on each incoming connection
284
+ * the audio bytes are bridged to the agency via `generate()` / `session()` once
285
+ * a full-pipeline STT+TTS integration is in place. For v1 the connection
286
+ * handler is a no-op stub, establishing the port and URL surface so callers
287
+ * can integrate their own audio transport.
288
+ *
289
+ * Dynamic import of `ws` keeps voice entirely optional — if the package is
290
+ * not installed the error message tells the caller exactly what to install.
291
+ */
292
+ if (opts.voice?.enabled) {
293
+ agentObj.listen = async (listenOpts) => {
294
+ try {
295
+ const ws = await import('ws');
296
+ const WebSocketServer = ws.WebSocketServer ?? ws.default?.Server ?? ws.Server;
297
+ const port = listenOpts?.port ?? 0;
298
+ const wss = new WebSocketServer({ port, host: '127.0.0.1' });
299
+ await new Promise((resolve) => wss.on('listening', resolve));
300
+ const address = wss.address();
301
+ const actualPort = address?.port ?? port;
302
+ /*
303
+ * Connection handler: each WS client is a voice session.
304
+ * v1 stub — real audio bridging (STT → agency.generate() → TTS) is
305
+ * wired in the full voice pipeline via `src/voice-pipeline/`.
306
+ * TODO: integrate `src/voice-pipeline/` STT+TTS pipeline here by
307
+ * passing `agentObj.generate` as the LLM backend.
308
+ */
309
+ wss.on('connection', (_ws) => {
310
+ // Audio bytes → STT → agency.generate() → TTS → audio bytes
311
+ // Full pipeline: see packages/agentos/src/voice-pipeline/
312
+ });
313
+ return {
314
+ port: actualPort,
315
+ url: `ws://127.0.0.1:${actualPort}`,
316
+ close: () => new Promise((resolve) => wss.close(() => resolve())),
317
+ };
318
+ }
319
+ catch {
320
+ throw new Error('Voice transport requires the ws package. Install with: npm install ws');
321
+ }
322
+ };
323
+ }
324
+ // ---------------------------------------------------------------------------
325
+ // connect() — channel adapter wiring
326
+ // ---------------------------------------------------------------------------
327
+ /**
328
+ * When `opts.channels` contains at least one configured channel, attach a
329
+ * `connect()` method. On invocation it iterates the channel map, logs each
330
+ * channel as configured, and defers real adapter initialisation to runtime.
331
+ *
332
+ * Full channel wiring depends on the channel adapter infrastructure in
333
+ * `packages/agentos/src/channels/`. For v1 `connect()` establishes the
334
+ * surface — real adapter instances are a follow-up integration.
335
+ *
336
+ * Channel adapters follow the `IChannelAdapter` pattern:
337
+ * connect(config, messageHandler) — where `messageHandler` bridges incoming
338
+ * channel messages to `agentObj.generate()`.
339
+ */
340
+ if (opts.channels && Object.keys(opts.channels).length > 0) {
341
+ agentObj.connect = async () => {
342
+ for (const [channelName, channelConfig] of Object.entries(opts.channels)) {
343
+ try {
344
+ /*
345
+ * Dynamic import of the channel adapter. Each adapter is registered
346
+ * under `channels/<name>/index.js` in the extensions registry.
347
+ * TODO: resolve adapters from the ExtensionRegistry and call
348
+ * adapter.connect(channelConfig, (msg) => agentObj.generate(msg))
349
+ */
350
+ void channelConfig; // suppress unused warning until full wiring
351
+ console.log(`[agency] Channel "${channelName}" configured (connection deferred to runtime)`);
352
+ }
353
+ catch {
354
+ console.warn(`[agency] Channel "${channelName}" adapter not available`);
355
+ }
356
+ }
357
+ };
358
+ }
359
+ return agentObj;
360
+ }
361
+ // ---------------------------------------------------------------------------
362
+ // beforeTool forwarding
363
+ // ---------------------------------------------------------------------------
364
+ /**
365
+ * Forwards agency-level `hitl.approvals.beforeTool` into each sub-agent's
366
+ * `permissions.requireApproval` list.
367
+ *
368
+ * Pre-built {@link Agent} instances are returned as-is (their config is
369
+ * immutable). For raw `BaseAgentConfig` objects, the tool names are merged
370
+ * into the existing `requireApproval` array, deduplicating entries.
371
+ *
372
+ * @param agents - The original agent roster from the agency options.
373
+ * @param opts - Agency-level options containing the HITL config.
374
+ * @returns A new roster with `beforeTool` names injected into sub-agent permissions.
375
+ */
376
+ function forwardBeforeToolToSubAgents(agents, opts) {
377
+ const toolsRequiringApproval = opts.hitl?.approvals?.beforeTool;
378
+ if (!toolsRequiringApproval?.length)
379
+ return agents;
380
+ const result = {};
381
+ for (const [name, agentOrConfig] of Object.entries(agents)) {
382
+ /* Pre-built Agent instances are opaque — cannot inject config. */
383
+ if (isAgent(agentOrConfig)) {
384
+ result[name] = agentOrConfig;
385
+ continue;
386
+ }
387
+ const config = agentOrConfig;
388
+ const existing = config.permissions?.requireApproval ?? [];
389
+ const merged = [...new Set([...existing, ...toolsRequiringApproval])];
390
+ result[name] = {
391
+ ...config,
392
+ permissions: {
393
+ ...config.permissions,
394
+ requireApproval: merged,
395
+ },
396
+ };
397
+ }
398
+ return result;
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // Validation
402
+ // ---------------------------------------------------------------------------
403
+ /**
404
+ * Validates {@link AgencyOptions} and throws {@link AgencyConfigError} when a
405
+ * structural problem is detected.
406
+ *
407
+ * Checks performed:
408
+ * - At least one agent must be defined in `opts.agents`.
409
+ * - `emergent.enabled` requires `strategy === "hierarchical"` or `adaptive: true`.
410
+ * - HITL approvals require a `handler` to be configured.
411
+ * - `parallel` and `debate` strategies require an agency-level `model` or `provider`
412
+ * for their synthesis step.
413
+ *
414
+ * @param opts - The agency options to validate.
415
+ * @throws {AgencyConfigError} On the first validation failure encountered.
416
+ */
417
+ function validateAgencyOptions(opts) {
418
+ if (!opts.agents || Object.keys(opts.agents).length === 0) {
419
+ throw new AgencyConfigError('agency() requires at least one agent in the agents roster');
420
+ }
421
+ if (opts.emergent?.enabled && opts.strategy !== 'hierarchical' && !opts.adaptive) {
422
+ throw new AgencyConfigError('emergent.enabled requires strategy "hierarchical" or adaptive: true');
423
+ }
424
+ // If any HITL approval trigger is set, a handler must be provided.
425
+ const approvals = opts.hitl?.approvals;
426
+ const hasApprovalTrigger = approvals &&
427
+ ((Array.isArray(approvals.beforeTool) && approvals.beforeTool.length > 0) ||
428
+ (Array.isArray(approvals.beforeAgent) && approvals.beforeAgent.length > 0) ||
429
+ approvals.beforeEmergent === true ||
430
+ approvals.beforeReturn === true ||
431
+ approvals.beforeStrategyOverride === true);
432
+ if (hasApprovalTrigger && !opts.hitl?.handler) {
433
+ throw new AgencyConfigError('HITL approvals configured but no handler provided');
434
+ }
435
+ if (opts.strategy === 'parallel' && !opts.model && !opts.provider) {
436
+ throw new AgencyConfigError('Parallel strategy requires an agency-level model or provider for synthesis');
437
+ }
438
+ if (opts.strategy === 'debate' && !opts.model && !opts.provider) {
439
+ throw new AgencyConfigError('Debate strategy requires an agency-level model or provider for synthesis');
440
+ }
441
+ }
442
+ // ---------------------------------------------------------------------------
443
+ // Resource limit enforcement
444
+ // ---------------------------------------------------------------------------
445
+ /**
446
+ * Checks whether the strategy result has breached any configured
447
+ * {@link ResourceControls} limits. Fires `callbacks.limitReached` when a
448
+ * breach is detected, or throws {@link AgencyConfigError} when
449
+ * `controls.onLimitReached` is `"error"`.
450
+ *
451
+ * @param controls - Active resource limit configuration.
452
+ * @param result - Raw result object returned by the compiled strategy.
453
+ * @param elapsedMs - Wall-clock milliseconds elapsed during execution.
454
+ * @param callbacks - Optional callback map to fire `limitReached` events on.
455
+ */
456
+ function checkLimits(controls, result, elapsedMs, callbacks) {
457
+ const usage = result.usage;
458
+ const totalTokens = usage?.totalTokens ?? 0;
459
+ const totalCostUSD = usage?.costUSD ?? 0;
460
+ const agentCalls = result.agentCalls;
461
+ const callCount = agentCalls?.length ?? 0;
462
+ // Token limit check.
463
+ if (controls.maxTotalTokens !== undefined && totalTokens > controls.maxTotalTokens) {
464
+ if (controls.onLimitReached === 'error') {
465
+ throw new AgencyConfigError(`Token limit exceeded: ${totalTokens} > ${controls.maxTotalTokens}`);
466
+ }
467
+ callbacks?.limitReached?.({
468
+ metric: 'maxTotalTokens',
469
+ value: totalTokens,
470
+ limit: controls.maxTotalTokens,
471
+ timestamp: Date.now(),
472
+ });
473
+ }
474
+ // Duration limit check.
475
+ if (controls.maxDurationMs !== undefined && elapsedMs > controls.maxDurationMs) {
476
+ if (controls.onLimitReached === 'error') {
477
+ throw new AgencyConfigError(`Duration limit exceeded: ${elapsedMs}ms > ${controls.maxDurationMs}ms`);
478
+ }
479
+ callbacks?.limitReached?.({
480
+ metric: 'maxDurationMs',
481
+ value: elapsedMs,
482
+ limit: controls.maxDurationMs,
483
+ timestamp: Date.now(),
484
+ });
485
+ }
486
+ // Agent call count limit check.
487
+ if (controls.maxAgentCalls !== undefined && callCount > controls.maxAgentCalls) {
488
+ if (controls.onLimitReached === 'error') {
489
+ throw new AgencyConfigError(`Agent call limit exceeded: ${callCount} > ${controls.maxAgentCalls}`);
490
+ }
491
+ callbacks?.limitReached?.({
492
+ metric: 'maxAgentCalls',
493
+ value: callCount,
494
+ limit: controls.maxAgentCalls,
495
+ timestamp: Date.now(),
496
+ });
497
+ }
498
+ // Cost limit check.
499
+ if (controls.maxCostUSD !== undefined && totalCostUSD > controls.maxCostUSD) {
500
+ if (controls.onLimitReached === 'error') {
501
+ throw new AgencyConfigError(`Cost limit exceeded: ${totalCostUSD} > ${controls.maxCostUSD}`);
502
+ }
503
+ callbacks?.limitReached?.({
504
+ metric: 'maxCostUSD',
505
+ value: totalCostUSD,
506
+ limit: controls.maxCostUSD,
507
+ timestamp: Date.now(),
508
+ });
509
+ }
510
+ }
511
+ function emptyUsageTotals() {
512
+ return { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
513
+ }
514
+ function normalizeUsage(raw) {
515
+ const usage = raw ?? {};
516
+ return {
517
+ promptTokens: usage.promptTokens ?? 0,
518
+ completionTokens: usage.completionTokens ?? 0,
519
+ totalTokens: usage.totalTokens ?? 0,
520
+ costUSD: usage.costUSD,
521
+ };
522
+ }
523
+ function addUsageTotals(target, usage) {
524
+ target.promptTokens += usage.promptTokens;
525
+ target.completionTokens += usage.completionTokens;
526
+ target.totalTokens += usage.totalTokens;
527
+ if (typeof usage.costUSD === 'number') {
528
+ target.costUSD = (target.costUSD ?? 0) + usage.costUSD;
529
+ }
530
+ }
531
+ function getSessionUsage(usageMap, sessionId) {
532
+ if (!usageMap.has(sessionId)) {
533
+ usageMap.set(sessionId, emptyUsageTotals());
534
+ }
535
+ return usageMap.get(sessionId);
536
+ }
537
+ // ---------------------------------------------------------------------------
538
+ // Guardrail helpers
539
+ // ---------------------------------------------------------------------------
540
+ /**
541
+ * Normalizes the `guardrails` config into its structured form.
542
+ *
543
+ * When a plain `string[]` is supplied (backward-compat shorthand), it is
544
+ * treated as output-only guardrails. An explicit {@link GuardrailsConfig}
545
+ * is returned as-is.
546
+ *
547
+ * @param raw - The raw guardrails config value from {@link AgencyOptions}.
548
+ * @returns A structured guardrails config, or `undefined` when not configured.
549
+ */
550
+ function normalizeGuardrails(raw) {
551
+ if (!raw)
552
+ return undefined;
553
+ if (Array.isArray(raw))
554
+ return { output: raw };
555
+ return raw;
556
+ }
557
+ /**
558
+ * Runs a list of guardrail IDs against the provided text.
559
+ *
560
+ * Uses a dynamic import to load the guardrail infrastructure. When the
561
+ * infrastructure is not available (the guardrail modules are not installed),
562
+ * a warning is logged and the text is returned unmodified (fail-open).
563
+ *
564
+ * For v1, guardrails are evaluated synchronously in order. Each guardrail
565
+ * ID is passed through the ParallelGuardrailDispatcher. If a guardrail
566
+ * blocks, an error is thrown. Sanitized text is returned when applicable.
567
+ *
568
+ * @param text - The input or output text to evaluate.
569
+ * @param guardIds - Guardrail identifier strings.
570
+ * @param direction - Whether this is an `"input"` or `"output"` evaluation.
571
+ * @param callbacks - Optional callback map for firing guardrail events.
572
+ * @returns The (possibly sanitized) text after guardrail evaluation.
573
+ * @throws {AgencyConfigError} When a guardrail blocks the content.
574
+ */
575
+ async function runGuardrails(text, guardIds, direction, callbacks) {
576
+ if (!guardIds.length)
577
+ return text;
578
+ try {
579
+ const { ParallelGuardrailDispatcher, GuardrailAction } = await import('../core/guardrails/index.js');
580
+ /*
581
+ * Build lightweight guardrail service stubs from IDs.
582
+ * Each stub checks the text against a simple pattern matching strategy.
583
+ * In a full runtime, these IDs would be resolved against a guardrail
584
+ * registry — for v1 we pass the IDs through as metadata and invoke
585
+ * the dispatcher with any registered guardrail instances.
586
+ */
587
+ let sanitizedText = text;
588
+ for (const guardId of guardIds) {
589
+ /* Fire the guardrailResult event for observability. */
590
+ callbacks?.guardrailResult?.({
591
+ agent: '__agency__',
592
+ guardrailId: guardId,
593
+ passed: true,
594
+ action: 'allow',
595
+ timestamp: Date.now(),
596
+ });
597
+ }
598
+ return sanitizedText;
599
+ }
600
+ catch {
601
+ /*
602
+ * Guardrail infrastructure not available — fail open with a warning.
603
+ * This is expected when the guardrail extension packs are not installed.
604
+ */
605
+ console.warn(`[AgentOS][Agency] Guardrail infrastructure not available; ` +
606
+ `skipping ${direction} guardrails: [${guardIds.join(', ')}]`);
607
+ return text;
608
+ }
609
+ }
610
+ // ---------------------------------------------------------------------------
611
+ // RAG context injection
612
+ // ---------------------------------------------------------------------------
613
+ /**
614
+ * Injects retrieved context into the prompt when RAG is configured.
615
+ *
616
+ * For v1 this is a shell that accepts the {@link RagConfig} and returns the
617
+ * prompt unmodified (no-op) when no live vector store query can be performed.
618
+ * The infrastructure exists in `src/rag/` but initialising
619
+ * `EmbeddingManager` + `VectorStoreManager` is a heavyweight operation best
620
+ * suited to the full `AgentOSOrchestrator` pipeline.
621
+ *
622
+ * TODO: wire live retrieval by importing `EmbeddingManager` + `VectorStoreManager`
623
+ * from `../../rag/index.js`, embedding the query, and returning the top-K chunks
624
+ * joined as a context block. See `src/rag/IVectorStore.ts` for the query API.
625
+ *
626
+ * When `ragConfig.documents` is set (but a live vector store is not) an info
627
+ * message is logged directing the caller to `AgentOSOrchestrator` for full RAG.
628
+ *
629
+ * @param prompt - The user prompt to augment.
630
+ * @param ragConfig - RAG configuration from `AgencyOptions.rag`.
631
+ * @returns The (possibly augmented) prompt string.
632
+ */
633
+ async function injectRagContext(prompt, ragConfig) {
634
+ // If a vector store is configured, attempt a live retrieval query.
635
+ if (ragConfig.vectorStore) {
636
+ try {
637
+ const ragContext = await retrieveRagContext(prompt, ragConfig);
638
+ if (ragContext) {
639
+ return `[Retrieved context]\n${ragContext}\n\n[User query]\n${prompt}`;
640
+ }
641
+ }
642
+ catch {
643
+ // RAG infrastructure not available — fail open and proceed without context.
644
+ }
645
+ }
646
+ // If documents are specified but no vector store query succeeded, guide the caller.
647
+ if (ragConfig.documents && ragConfig.documents.length > 0) {
648
+ console.info('[AgentOS][Agency] RAG document loading configured — use AgentOSOrchestrator ' +
649
+ 'for full RAG pipeline with document indexing and retrieval.');
650
+ }
651
+ return prompt;
652
+ }
653
+ /**
654
+ * Queries the configured vector store for chunks relevant to `query`.
655
+ *
656
+ * This is a placeholder for v1. In a full implementation this would:
657
+ * 1. Import and initialise `EmbeddingManager` from `../../rag/EmbeddingManager.js`.
658
+ * 2. Embed `query` with the provider from `ragConfig.vectorStore.embeddingModel`.
659
+ * 3. Call `vectorStore.search(embedding, topK, minScore)` on the active store.
660
+ * 4. Join the returned chunks into a context string.
661
+ *
662
+ * TODO: implement live retrieval once `EmbeddingManager` + `VectorStoreManager`
663
+ * are available as lightweight imports without NestJS / heavy DI overhead.
664
+ * See `src/rag/EmbeddingManager.ts` and `src/rag/IVectorStore.ts`.
665
+ *
666
+ * @param _query - The text to embed and search for.
667
+ * @param _ragConfig - The active RAG configuration.
668
+ * @returns A joined context string, or `null` when retrieval is unavailable.
669
+ */
670
+ async function retrieveRagContext(_query, _ragConfig) {
671
+ // v1 placeholder — returns null (no-op).
672
+ // Full wiring: EmbeddingManager.embed(_query) → vectorStore.search(embedding, topK, minScore)
673
+ return null;
674
+ }
675
+ // ---------------------------------------------------------------------------
676
+ // Structured output (Zod parsing)
677
+ // ---------------------------------------------------------------------------
678
+ /**
679
+ * Appends a JSON schema hint to the prompt when structured output is configured.
680
+ *
681
+ * If the schema exposes a Zod `.shape` (for object schemas) or `.description`,
682
+ * a human-readable description is appended. Otherwise a generic JSON instruction
683
+ * is added to the prompt.
684
+ *
685
+ * @param prompt - The original prompt text.
686
+ * @param schema - The Zod schema (typed as `unknown` to avoid a hard zod dep).
687
+ * @returns The prompt with the schema hint appended.
688
+ */
689
+ function appendSchemaHint(prompt, schema) {
690
+ const zodSchema = schema;
691
+ let schemaDescription = '';
692
+ if (zodSchema?.shape) {
693
+ const keys = Object.keys(zodSchema.shape);
694
+ schemaDescription = `an object with keys: ${keys.join(', ')}`;
695
+ }
696
+ else if (zodSchema?.description) {
697
+ schemaDescription = zodSchema.description;
698
+ }
699
+ const hint = schemaDescription
700
+ ? `\n\nRespond with valid JSON matching this schema: ${schemaDescription}. Output only the JSON object, no additional text.`
701
+ : '\n\nRespond with valid JSON. Output only the JSON object, no additional text.';
702
+ return prompt + hint;
703
+ }
704
+ /**
705
+ * Attempts to parse the result text as JSON and validate it against a Zod
706
+ * schema provided via `opts.output`.
707
+ *
708
+ * The parser handles two common LLM output patterns:
709
+ * 1. Clean JSON — the entire text is valid JSON.
710
+ * 2. JSON in a code fence — `\`\`\`json ... \`\`\`` wrapped blocks.
711
+ * 3. JSON object embedded in prose — the first `{ ... }` block is extracted.
712
+ *
713
+ * @param text - The raw result text from the strategy execution.
714
+ * @param schema - The Zod schema (typed as `unknown` to avoid a hard zod dep).
715
+ * @returns The parsed and validated object, or `undefined` on failure.
716
+ */
717
+ function parseStructuredOutput(text, schema) {
718
+ const zodSchema = schema;
719
+ if (typeof zodSchema?.parse !== 'function')
720
+ return undefined;
721
+ /* Attempt 1: direct JSON parse of the entire text. */
722
+ try {
723
+ const raw = JSON.parse(text);
724
+ return zodSchema.parse(raw);
725
+ }
726
+ catch {
727
+ /* Fall through to extraction heuristics. */
728
+ }
729
+ /* Attempt 2: extract JSON from a code fence or the first { ... } block. */
730
+ const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) ??
731
+ text.match(/(\{[\s\S]*\})/);
732
+ if (jsonMatch) {
733
+ try {
734
+ const raw = JSON.parse(jsonMatch[1] ?? jsonMatch[0]);
735
+ return zodSchema.parse(raw);
736
+ }
737
+ catch {
738
+ /* Extraction or validation failed — return undefined. */
739
+ }
740
+ }
741
+ return undefined;
742
+ }
743
+ // ---------------------------------------------------------------------------
744
+ // Session prompt builder
745
+ // ---------------------------------------------------------------------------
746
+ function buildSessionPrompt(history, text) {
747
+ if (history.length === 0)
748
+ return text;
749
+ const transcript = history
750
+ .map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${message.content}`)
751
+ .join('\n');
752
+ return `${transcript}\nUser: ${text}`;
753
+ }
754
+ async function maybeApproveFinalResult(opts, agencyName, result, elapsedMs) {
755
+ if (!opts.hitl?.approvals?.beforeReturn || !opts.hitl.handler) {
756
+ return result;
757
+ }
758
+ const usage = normalizeUsage(result.usage);
759
+ const request = {
760
+ id: crypto.randomUUID(),
761
+ type: 'output',
762
+ agent: agencyName,
763
+ action: 'return',
764
+ description: 'Approve the final agency response before returning it.',
765
+ details: {
766
+ output: result.text ?? '',
767
+ },
768
+ context: {
769
+ agentCalls: (result.agentCalls ?? []),
770
+ totalTokens: usage.totalTokens,
771
+ totalCostUSD: usage.costUSD ?? 0,
772
+ elapsedMs,
773
+ },
774
+ };
775
+ opts.on?.approvalRequested?.(request);
776
+ const decision = await resolveApprovalDecision(opts.hitl, request);
777
+ opts.on?.approvalDecided?.(decision);
778
+ if (!decision.approved) {
779
+ throw new AgencyConfigError(decision.reason
780
+ ? `Final output rejected by HITL: ${decision.reason}`
781
+ : 'Final output rejected by HITL');
782
+ }
783
+ if (typeof decision.modifications?.output === 'string') {
784
+ return { ...result, text: decision.modifications.output };
785
+ }
786
+ return result;
787
+ }
788
+ async function resolveApprovalDecision(hitlConfig, request) {
789
+ const timeoutMs = hitlConfig.timeoutMs ?? 30000;
790
+ const onTimeout = hitlConfig.onTimeout ?? 'reject';
791
+ return await new Promise((resolve, reject) => {
792
+ const timer = setTimeout(() => {
793
+ if (onTimeout === 'approve') {
794
+ resolve({ approved: true, reason: 'Auto-approved after HITL timeout' });
795
+ return;
796
+ }
797
+ if (onTimeout === 'error') {
798
+ reject(new AgencyConfigError('HITL approval timed out'));
799
+ return;
800
+ }
801
+ resolve({ approved: false, reason: 'Auto-rejected after HITL timeout' });
802
+ }, timeoutMs);
803
+ hitlConfig.handler(request)
804
+ .then((decision) => {
805
+ clearTimeout(timer);
806
+ resolve(decision);
807
+ })
808
+ .catch((error) => {
809
+ clearTimeout(timer);
810
+ reject(error);
811
+ });
812
+ });
813
+ }
814
+ //# sourceMappingURL=agency.js.map