docs/AGENCIES.md
Rama master · réplica de lectura del repositorio original.
# Agencies and orchestration strategies
[`agency()`](https://github.com/framerslab/agentos/blob/master/src/api/agency.ts) returns an `Agent` that coordinates a roster. Each roster member is an agent config or a built `Agent` (so an agency can hold another agency), and a strategy decides who runs when. The strategies are compiled from [`src/api/runtime/strategies`](https://github.com/framerslab/agentos/tree/master/src/api/runtime/strategies).
| Strategy | What it does |
|---|---|
| `sequential` | Members run one after another; each receives the output of the one before. |
| `parallel` | Members run at the same time on the same prompt; a synthesizer built from the agency's own `model` or `provider` merges their outputs, leaving out a member that failed. |
| `debate` | Members argue over `maxRounds` rounds (3 by default); in each round every member sees every prior argument; a synthesizer built from the agency's own `model` or `provider` distils the result. N members over R rounds cost N×R calls plus one. |
| `review-loop` | The first member produces or revises, the second reviews and answers `{ "approved": true or false, "feedback": "..." }`; the loop runs until approval or `maxRounds` (3 by default); a reply that is not valid JSON counts as not approved. |
| `hierarchical` | A manager sees every member as a `delegate_to_<name>` tool and decides what to delegate and in what order; with `emergent.enabled` on the agency, it can also call `spawn_specialist`. |
| `graph` | Members declare `dependsOn`; the strategy sorts the roster topologically, runs each topological tier concurrently (a member waits for its whole previous tier) and feeds each the outputs of its predecessors. |
| `panel` | Every member answers the same prompt at once on its own model, under an optional per-seat deadline and concurrency cap; the members that returned text are counted against `quorum`, and a `chair` synthesizes their answers without seeing a model or vendor name (`chair: false` returns the answers labelled). The result carries `seats`, `chair`, `quorum` and `seating`. |
`parallel` and `debate` need `model` or `provider` on the agency itself; compiling either without one throws `AgencyConfigError`. On the `parallel` and `panel` strategies, a `quorum` can require a minimum number of successful agents, of distinct providers and (under `panel`) of distinct vendors; `onShortfall: 'proceed'` turns a shortfall into a line through the host's logger, and the default throws. A `modelPool` with `seating` fills every seat that names neither `provider` nor `model` from named models on each call, with strict per-seat credentials and every key redacted from the records ([Model Pools, Seating and the Panel Strategy](./orchestration/AGENCY_API.md#model-pools-seating-and-the-panel-strategy)). `agency().session()` keeps per-session message history and usage totals only. `agency()` warns when it receives `cognitiveMechanisms`, which the lightweight helper does not run, and when it receives `agencies`, a `gmi()` agent's option that an agency applies to neither itself nor its seats: an agency launches no team, and a seat that may launch teams is a `gmi()` agent with `agencies` set on its own options. At the agency level it applies `output`, the `hitl` approval gates (after an approval, the `code-safety` and `pii-redaction` pattern checks run over the approved tool arguments, agent input or final text), `controls`, `provenance` and, on `hierarchical` (or with `adaptive: true`), `emergent`. It accepts `guardrails` and evaluates none of them: each guardrail id is reported through `on.guardrailResult` with `enforced: false` and the text passes unchanged. It accepts `rag` and adds no retrieved context: with a `vectorStore` the retrieval step returns nothing, and with `documents` it logs that the full runtime does the indexing ([`agency.ts`](https://github.com/framerslab/agentos/blob/master/src/api/agency.ts), `runGuardrails()` and `injectRagContext()`). `voice.enabled` attaches a `listen()` method that takes JSON text frames over a local WebSocket, and `channels` attaches a `connect()` that always rejects. `agency()` builds no shared memory store; members share only what the strategy passes between them.
Fixed pipelines with explicit stages belong to the mission and workflow compilers ([Orchestration Guide](./orchestration/ORCHESTRATION.md), [mission() API](./orchestration/MISSION_API.md), [workflow() DSL](./orchestration/WORKFLOW_DSL.md)); the roster API is documented in [Multi-Agent Agency API](./orchestration/AGENCY_API.md).
## Seats served by GMIs
`agency({ runtime: 'gmi' })` serves every config seat with a GMI ([GMIs from agent()](./GMI.md#gmis-from-agent)): each roster config, the `panel` chair and each specialist the `hierarchical` manager spawns, unless its config sets a `runtime` of its own, which wins. `runtime` takes `'legacy'` (the default) or `'gmi'`; any other value throws `AgencyConfigError`. The `hierarchical` manager and the `parallel` and `debate` synthesizers stay on the `generateText()` path, and a pre-built member keeps the engine it was built with. A strategy runs a config seat through its `generate()` or `stream()`, which run the `'light'` profile, so the agency takes no `cognition`. A pre-built member may carry `description`, which the hierarchical manager reads into its roster (without it the manager reads "General purpose"), and `dependsOn`, which selects and orders the graph strategy as a config's does.
`sessionSeat(member, options)` seats a persistent `gmi()` agent as itself, with its own persona, cognition profile and tools, through one session of its own for the run, so a manager that delegates to it twice finds the first exchange in the second:
```typescript
import { agency, gmi, sessionSeat } from '@framers/agentos';
const tutor = gmi({ provider: 'openai', model: 'gpt-4o', name: 'Tutor', instructions: 'You are a patient maths tutor.', cognition: 'full' });
const team = agency({
provider: 'openai',
model: 'gpt-4o',
strategy: 'hierarchical',
agents: { tutor: sessionSeat(tutor, { brief: 'Check each step of the proof.' }) },
});
```
`options` take `sessionId` (prefixed `seat:`; a seat whose id the member has open fails its first call with a `TypeError` that names the id), `description` (by default the member's persona description: the first line of its instructions, else its name), `dependsOn`, `tools` and `brief` (put before every input the seat receives, at most 1,000 characters). `tools` defaults to the member's own tools but the five self-modification tools, `launch_agency`, the ten memory tools (the eight cognitive-memory tools, `read_working_memory` and `update_working_memory`) and a composed tool whose step tools are not all among the rest; naming one of those, or a tool the member does not have, throws a `TypeError`, and so does a member that is not a `gmi()` agent. The seat's session has its memory scoped to itself and no learner: the member recalls nothing of its users there and files nothing under them, and its `memoryProvider` is neither asked for context nor told the turns. Each of the seat's calls is checked against the member's own `budget`, when it has one, beside the budget the agency's call carries, and charged to both. `agency().close()` closes the seat's session and leaves the member serving its own sessions; the seat's usage stays in the member's agent total.
## Teams a GMI launches
A GMI with `agencies.launch` has a `launch_agency` tool. Each call builds a temporary `agency()` team, runs its goal once, closes the team and returns one tool result; one seat makes a subagent.
```typescript
import { gmi } from '@framers/agentos';
const lead = gmi({
provider: 'openai',
model: 'gpt-4o',
instructions: 'You plan research and hand parts of it to teams.',
tools: { search },
maxTokens: 2_000,
agencies: { launch: true, maxLaunchTokens: 100_000 },
});
```
| Argument | What it holds |
|---|---|
| `goal` | What the team must produce, in full (1 to 8,000 characters); the team sees nothing of the launching conversation. |
| `strategy` | `sequential` (the default), `parallel`, `debate`, `review-loop`, `hierarchical` or `graph`: the strategies above but `panel`. `review-loop` takes exactly two seats, and `parallel` and `debate` at least two. |
| `seats` | `{ role, instructions }` for a temporary member (instructions of at most 8,192 characters); `{ role, agentKey, brief? }` for a member of `agencies.members` (`gmi()`); `{ role, personaId, brief? }` for a registered persona (the full runtime). A brief holds at most 1,000 characters. Each seat may name `tools` (default: none) and `dependsOn`, the other seats it waits for, with no cycle among them, which the `graph` strategy reads: a launch that names no strategy runs `sequential`. A role is unique in the team, starts with a letter, holds only letters, digits, `_` and `-`, is at most 52 characters, and is none of the names the hierarchical strategy uses: `spawn_specialist`, `final_answer`, `plan` and any name that starts with `delegate_to_`. |
| `budgetUsd`, `maxTokens` | Caps the model asks for, each above 0; the launch takes the least of them and its own bounds. |
| `deadlineMs` | A wall deadline of at least 1,000 ms, held to `maxDeadlineMs`; `defaultDeadlineMs` when unset. |
| `maxRounds` | Rounds of `debate` and `review-loop`: 1 to 3, default 3. |
An argument or a seat key the table does not name is refused.
The bounds are `agencies` on the agent (or `AgentOSConfig.agencies`), checked when it is built, `launch` on or off: a value outside its range throws a `RangeError` naming the field (on the full runtime, initialization fails with the code `SYS_CONFIGURATION_ERROR`), and a field set to `null` takes its default.
| Option | Default | Range |
|---|---|---|
| `maxDepth` | 2 | an integer from 1 to 4 |
| `maxSeats` | 5 | an integer from 1 to 12 |
| `maxConcurrent` | 1 | an integer from 1 to 4 |
| `budgetShare` | 0.5 | above 0, at most 1 |
| `maxLaunchTokens` | 200,000 | at least 2,000 |
| `maxLaunchCostUSD` | none | above 0 |
| `defaultDeadlineMs` | 120,000, or `maxDeadlineMs` when that is lower | 1,000 to `maxDeadlineMs` |
| `maxDeadlineMs` | 600,000 | 1,000 to 3,600,000 |
`agencies` also takes `hitl`, the approval policy of the team a root launch builds (approvals with no handler throw `AgencyConfigError`, as on `agency()`), `members` on `gmi()` and `coordinator` on the full runtime, both below.
- **Depth.** A turn no launch sits over is at depth 0, and its team is level 1; a seat at level *d* may launch only when *d* + 1 ≤ `maxDepth`. At the defaults a GMI's team may launch one more level of teams. A nested launch takes its tree's bounds from the root launch, never from the launching member's own options.
- **Concurrency.** At most `maxConcurrent` launches are in flight at once per scope: the launching session at the root, the launching team below it. A launch whose team outlived its grace counts until its seats are released, and no seat of it launches meanwhile (`parent_launch_closed`).
- **Spend.** All the root launches of one parent turn draw on one launch pool (`maxLaunchTokens`, `maxLaunchCostUSD`), their nested launches included. A root launch's budget is the least of its arguments, `budgetShare` of what the launching turn's budget has left (on `gmi()` the agent's `budget` or the send's; a full-runtime turn carries none), what the pool has left and `maxLaunchTokens`; a nested launch draws on its parent launch's budget the same way, and on a persistent member's own budget as well when the member launches it from its seat. A launch whose token cap cannot cover one call of the seat whose calls are largest, or that has no dollars left, is refused `budget_unavailable`. One call counts 2,000 tokens for the prompt and the output cap the seat's calls are sent with, held to its model's output ceiling, or 4,096 when they carry none: for a temporary seat the launching agent's `maxTokens` on `gmi()` and its persona's on the full runtime, for a member's seat the member's own (its `maxTokens`, else its `controls.maxTotalTokens`, on `gmi()`; its persona's on the full runtime). `gmi()` warns at construction when `maxLaunchTokens` cannot cover one call of the agent's own route. Every completion call of the team, the seats' and the coordinator's, is checked against the launch's budget before it is sent and charged to it; seats that run at once can each pass the cap by one call. A persistent member's memory embeddings are not counted.
- **Time.** One wall deadline per launch. At the deadline, or when the launching turn stops, each seat's model call in flight ends; a tool call already running finishes, and one whose checks or approval finish after that is refused (`STEP_ABORTED`). The launch then waits at most 5 seconds for the team to settle and close, and returns; a seat still running then is released when it settles.
- **Tools.** A seat's tools are its `tools` allowlist, among the launching agent's own (at a nested launch, the launching seat's). On `gmi()` a member's seat names only tools the member also has. On the full runtime the launching persona's tools are those its capabilities allow, and a persona's seat names only tools its own capabilities allow. A call to any other tool is answered that the tool is not available in this seat, and the tool does not run. The five self-modification tools (`forge_tool`, `create_workflow`, `manage_skills`, `self_evaluate`, `adapt_personality`) and the ten memory tools are never a seat's, `launch_agency` only where the seat's level can launch, and a composed tool only with every tool its steps call. `launch_agency` is never a step of a composition or a workflow ([Emergent Capabilities](./architecture/EMERGENT_CAPABILITIES.md)).
- **Approval.** A team runs under the approval gate the launching turn carries (an agency's, when the agent is one of its members), asked first on every seat's tool call, then under `agencies.hitl` at a root launch. A nested launch passes on the gate it received, so `agencies.hitl`'s `beforeTool` covers every seat of every team below the root, each call asked once, while its `beforeAgent` and `beforeReturn` are asked in the root team alone. A call whose turn has already stopped is refused without asking anyone. On the full runtime a seat's call to a composition the runtime's emergent engine holds is asked by the composition's name, and each step the composition reaches by the step tool's name, before the step runs; the runtime's own checks (the persona's capabilities, the approval of a tool with side effects) still apply to every call, and the output guardrails that judge the launching turn's tool calls (a guardrail's `config.evaluateToolCalls`) judge every seat's ([Human-in-the-loop](./safety/HUMAN_IN_THE_LOOP.md)). On `gmi()` every seat's tool call runs the launching turn's `onBeforeToolExecution` hooks first and a member's own hook after them.
A launch that started returns, whatever its status, `{ status, agencyId, text, truncated, seats, usage, coordinatorUsage, durationMs, reason?, provenance }`. `status` is the first that holds of `budget_exhausted` (a budget a call drew on refused it), `timed_out`, `cancelled` (the launching turn stopped first), `completed` and `failed`, and `reason` says why for every status but `completed`. `text` holds at most 16,000 characters (`truncated` says whether it was cut) and each seat's `summary` 1,000. A seat's `status` is `completed`, `cut` (the deadline, a budget or the launching turn's stop ended its last call, or that call was still running when the launch returned), `failed` or `not_run`. `usage` adds the seats' calls, each the usage its turn reported, `coordinatorUsage` the manager's and the synthesizers', and `provenance.budget` everything the launch's budget counted, nested launches included. A metaprompt or self-reflection call a seat's GMI makes after its turn is not in its turn's usage, as on any GMI, so `usage` leaves it out; the launch's budget counts it. A failed seat is named by its error's name and code, never its message, so no model of the team and no result reads a provider's or a handler's words; where the error itself goes is below. A launch that cannot start is a tool error, `<code>: <reason>`, with one of the codes `invalid_arguments`, `depth_exceeded`, `too_many_seats`, `concurrency_exceeded`, `unknown_tool`, `tool_not_allowed`, `seat_form_unsupported`, `unknown_member`, `member_unavailable` (a member that cannot take a seat, or a seat that cannot be built), `duplicate_member`, `budget_unavailable` and `parent_launch_closed`. A call the tool's input schema refuses (a strategy it does not list, a value that cannot take its type, a missing `goal` or `seats`) never reaches the launch: its error reads `Invalid arguments for tool 'launch_agency'`, with no code.
- **On `gmi()`** (and `agent({ runtime: 'gmi' })`): a temporary seat is a `gmi()` agent of its own, built from the launching agent's route (the provider and model its persona resolved, its key, base URL, fallback and router settings), completion settings, `onBeforeGeneration` and `onAfterGeneration` hooks and `controls.maxDurationMs`, with the seat's instructions and the launching agent's own tool instances its allowlist names, on the `'light'` profile with no memory and no learner, in one session that closes with the seat. `agencies.members` holds the persistent members a launch may seat by `agentKey`, each a `gmi()` agent, listed under its `learning.agentKey` when it has one; the agent reads the record once, when it is built. A member takes its seat as through `sessionSeat()`, the seat's role and brief put before every input. The team's manager and synthesizers run on the launching agent's provider and model with its key and base URL alone. The agent's `on` callbacks see each team as `launch:<agencyId>`; the agent's own turns fire none of them. A seat's failed call reaches `on.error` with its original error under `launch_agency seat <role>`, and a team whose run fails reports it there once, under `launch:<agencyId>`, with the error it rejected with (a failed seat's by its name and code); without `on.error` the console gets both, the run under `launch_agency run`, and the content-safe mode turns each into an event. A callback that throws or rejects fails no launch. The agent's usage ledger, when it has one, records the temporary seats' and the coordinator's calls under the source `launch_agency`; a member's own ledger records its seat's calls.
- **On the full runtime** (`AgentOSConfig.agencies`): the tool requires the `agency:launch` capability, so only a persona granted it lists and calls it, and a config tool of the host's own named `launch_agency` fails initialization. A seat is a GMI of the runtime's `GMIManager` with no cognitive memory, no retrieval and no learner, running as the launching turn's user (`'system'` when it names none) and organization: a registered persona by `personaId`, checked as a session's persona is (strict validation, the user's tier), or, for a temporary seat, a persona made from the launching GMI's (its route, completion options, capabilities and hard limits, with the seat's instructions as its prompt and no metaprompts or sentiment tracking). The team's manager and synthesizers call `generateText()` on `agencies.coordinator` (`{ provider, model, apiKey?, baseUrl? }`), else on the launching persona's provider and model (the GMIs' defaults, `gmiManagerConfig.defaultGMIBaseConfigDefaults`, when it names none; when neither names a provider, the one the runtime serves that model from, as for the launching GMI itself); a route that names neither `apiKey` nor `baseUrl` takes the key of the entry in `modelProviderManagerConfig.providers` the runtime serves its provider from, and sends it where the runtime's provider sends it: to that entry's base URL, else to the provider's own endpoint, never to a base URL a global default or an environment variable names (an Ollama route takes an entry's base URL with no key too). What such a route takes nothing for, `generateText()` resolves for the provider, key and base URL together; a route that names either keeps what it names and takes nothing of the entry. A seat's failed call reaches the runtime's logger (the one `AgentOS.create` takes, else its own) at warn, with its original error, under `launch_agency seat <role>`, and a failed run under `launch_agency run`. While a team runs it is in the runtime's agency registry, and `AgentOS.listAgencies()` returns it: its entry's `metadata` is a `LaunchRecordMetadata` (`kind: 'launched'`), each seat names its GMI and persona, each seat call adds a history entry, and the entry goes once every seat is released. A seat's session (`seat:<agencyId>:<role>`) is its team's until the seat is released: a request that names it is refused, whatever persona it names, an unknown one included (the `GMIManager` refuses the session with `PERMISSION_DENIED` before it looks the persona up, and the turn ends with an error chunk carrying that message), so no request runs a turn on a seat's GMI or shuts a running seat down, and feedback that names it (`receiveFeedback()`) is not recorded. The idle cleanup (`GMIManager.cleanupInactiveGMIs()`) passes a seat by: its launch releases it. The usage the runtime's spend meter (`spendMeter`) records for the launching turn is that turn's own model calls; the team's calls are in the launch's result.
On both, a seat's GMI whose turn fails also writes its own diagnostic of the failed turn to the console, the error's message included, as any GMI's failed turn does; the content-safe mode leaves that line as it is ([Logging](./observability/LOGGING.md#content-safe-server-telemetry)).
A launched team shares only what its strategy passes between its seats. Once every seat is released (by the launch's return, or later for a seat still running then), nothing of the team remains but the result the parent received, the usage its calls recorded and, for a persistent member on `gmi()`, the seat session's memory traces, which no later turn reads.
The package root and `@framers/agentos/api` export `sessionSeat()`, `SessionSeatOptions`, `SeatHandle`, `SeatMetadata`, `LAUNCH_AGENCY_TOOL_NAME` and the launch's types: `AgencyLaunchOptions` (`agencies` on an agent), `RuntimeAgencyLaunchOptions` (`AgentOSConfig.agencies`), `LaunchLimits`, `LaunchAgencyArgs`, `LaunchSeatSpec`, `LaunchAgencyResult`, `LaunchSeatRecord`, `LaunchUsage`, `LaunchStatus`, `LaunchRefusal`, `LaunchRefusalCode`, `AgencyLaunchFrame`, `CallerTurnContext`, `ToolCallHook` and `LaunchRecordMetadata` (a launched team's registry entry).