Skip to content

Commit a152041

Browse files
committed
docs: explain four-layer lazy/optional DI and end-to-end guardrail load flow
1 parent df98aae commit a152041

2 files changed

Lines changed: 152 additions & 3 deletions

File tree

AUTO_LOADING_EXTENSIONS.md

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,96 @@ sequenceDiagram
5151
A->>TO: initialize tool calling
5252
```
5353

54-
## What Lazy Loading Means (Today)
54+
## What "Lazy Loading" Means (Today)
5555

56-
Youll see lazy used in a few different ways:
56+
You'll see "lazy" used in a few different ways:
5757

5858
- **Lazy optional dependencies**: the curated registry uses dynamic `import()` to include only installed extension packages.
59-
- **Lazy heavy deps inside tools**: a tool’s `execute()` can dynamically `import()` heavy libraries on first call.
59+
- **Lazy heavy deps inside tools**: a tool's `execute()` can dynamically `import()` heavy libraries on first call.
60+
- **Lazy heavy deps shared across descriptors**: `SharedServiceRegistry.getOrCreate()` registers a factory at activation and only constructs the resource on the first call that needs it. Subsequent callers (other tools, the streaming guardrail) share the same instance.
6061
- **Lazy skills**: `SkillRegistry` from `@framers/agentos/skills` exposes `skills_list` / `skills_read` tools so the model can fetch `SKILL.md` content on demand. Curated skill content ships in `@framers/agentos-skills`.
6162

6263
What AgentOS does not do by default: wait for the model to request a tool and then reveal its schema. Tool calling requires schemas up front, so the host decides which tools to expose for a given session/turn.
6364

65+
## End-to-End Walkthrough: A Lazy-Loaded Guardrail Pack
66+
67+
Walking through what actually happens when a host installs the PII redaction guardrail extension and runs a request. This is the same shape every guardrail pack follows.
68+
69+
### Step 1. Install only what you need
70+
71+
```bash
72+
npm install @framers/agentos @framers/agentos-extensions-registry @framers/agentos-ext-pii-redaction
73+
```
74+
75+
Nothing else from the 100+ extension catalog enters `node_modules`. The registry is a small SDK; the catalog of metadata it carries is independent of which packages are installed.
76+
77+
### Step 2. Build the manifest
78+
79+
```ts
80+
import { AgentOS } from '@framers/agentos';
81+
import { createCuratedManifest } from '@framers/agentos-extensions-registry';
82+
83+
const manifest = await createCuratedManifest({
84+
tools: 'all',
85+
channels: 'none',
86+
secrets: {
87+
// Optional: unlocks the LLM-judge tier in pii-redaction.
88+
'anthropic.apiKey': process.env.ANTHROPIC_API_KEY,
89+
},
90+
});
91+
// → manifest.packs.length === 1 (only the installed pii-redaction pack).
92+
```
93+
94+
`createCuratedManifest()` calls `import.meta.resolve()` on every catalog entry. The 99 entries that are not installed get dropped with no error. The one installed pack lands in `manifest.packs`.
95+
96+
### Step 3. Activate the pack
97+
98+
```ts
99+
const agentos = new AgentOS();
100+
await agentos.initialize({ extensionManifest: manifest });
101+
```
102+
103+
`ExtensionManager.loadManifest()` runs `pack.onActivate(ctx)`, which registers a `pii:ner-model` factory in `ctx.services` (`SharedServiceRegistry`). The 110MB BERT NER model file does not enter the module graph yet. The factory is the only thing registered.
104+
105+
### Step 4. Register descriptors
106+
107+
The pack emits three descriptors:
108+
109+
- `pii_scan` (kind: `tool`)
110+
- `pii_redact` (kind: `tool`)
111+
- The PII redaction guardrail itself (kind: `guardrail`, with `config.canSanitize = true` and `config.evaluateStreamingChunks = true`)
112+
113+
`ExtensionManager` checks `requiredSecrets` per descriptor before activating. If `anthropic.apiKey` was not provided, the optional LLM-judge resolver descriptor is skipped, and the rest of the pack activates.
114+
115+
### Step 5. First request loads the model
116+
117+
A user message hits the input pipeline. The two-phase guardrail dispatcher runs:
118+
119+
1. **Phase 1 (sequential sanitizers).** The PII guardrail's `evaluateInput()` fires. On this first call, the NER pipeline reaches into `services.getOrCreate('pii:ner-model', ...)` and the model loads. The guardrail returns a `SANITIZE` result with redacted text.
120+
2. **Phase 2 (parallel classifiers).** Any remaining classifier guardrails run concurrently against the sanitized text via `Promise.allSettled`, with worst-action aggregation (`BLOCK > FLAG > ALLOW`).
121+
122+
### Step 6. Streaming output reuses the same model
123+
124+
As the LLM streams tokens, each `TEXT_DELTA` chunk passes through the guardrail's `evaluateOutput()`. The NER model is already loaded and cached in `SharedServiceRegistry`, so chunk evaluation is fast. `SANITIZE` results returned in Phase 1 chain deterministically; Phase 2 `SANITIZE` is downgraded to `FLAG` to keep output deterministic when classifiers run in parallel.
125+
126+
### Step 7. Tear-down releases everything
127+
128+
`AgentOS.shutdown()` calls `pack.onDeactivate(ctx)` in reverse order. `SharedServiceRegistry` releases the NER model. The kind-specific registries clear. Future processes start cold.
129+
130+
### What changes for the other guardrail packs
131+
132+
The pattern is identical. Only the heavy resource changes:
133+
134+
| Pack | Heavy resource | Lazy via |
135+
|------|---------------|----------|
136+
| `agentos-ext-pii-redaction` | BERT NER model (~110MB) | `services.getOrCreate('pii:ner-model', ...)` |
137+
| `agentos-ext-ml-classifiers` | ONNX BERT classifiers (toxicity, injection, NSFW) | `services.getOrCreate('ml:classifier-orchestrator', ...)` |
138+
| `agentos-ext-grounding-guard` | NLI entailment pipeline | `services.getOrCreate('grounding:nli-pipeline', ...)` |
139+
| `agentos-ext-topicality` | Embedding model + drift tracker | `services.getOrCreate('topicality:embedder', ...)` |
140+
| `agentos-ext-code-safety` | None (regex-only) | No lazy load needed |
141+
142+
For the dispatcher mechanics and configuration flags, see [Guardrails](https://docs.agentos.sh/features/guardrails). For the kind-specific registry internals, see [Extension Loading](https://docs.agentos.sh/architecture/extension-loading).
143+
64144
## What Is Not Automatic (Yet)
65145

66146
- No core auto-install (`npm install`) of missing extensions.

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,75 @@ await agentos.initialize({
223223
});
224224
```
225225

226+
## How extensions stay optional and lazy
227+
228+
The extension surface is dependency-injected at four layers. Each layer is opt-in by default, so a host that installs five extensions does not pay the cost of the other 95, and an extension that needs a 110MB NER model does not load it until the first call.
229+
230+
### Layer 1: package install gates everything
231+
232+
Every extension is its own npm package. Nothing is imported until you `npm install` it. The `@framers/agentos-extensions-registry` SDK depends on the extension packages as `peerDependenciesMeta.optional`, so installing the registry alone gives you the catalog metadata without pulling any extension source. Add `@framers/agentos-ext-pii-redaction` to your `package.json` and the registry sees it; remove it and the registry drops it on the next manifest build.
233+
234+
### Layer 2: registry resolve skips uninstalled packages
235+
236+
`createCuratedManifest()` calls `import.meta.resolve()` on every catalog entry before adding it to the manifest. If the resolution throws, the entry is omitted. The runtime never sees it. There is no error, no warning, no missing-module crash. This is the difference between a catalog (metadata) and a registry that only emits packs you can actually run.
237+
238+
```typescript
239+
// Even if you ask for `tools: 'all'`, only installed packages reach the runtime.
240+
const manifest = await createCuratedManifest({ tools: 'all' });
241+
// → manifest.packs contains only @framers/agentos-ext-* packages found on disk.
242+
```
243+
244+
### Layer 3: `requiredSecrets` gates descriptor activation
245+
246+
Inside a pack, each descriptor (a single tool, guardrail, channel, or workflow) can declare `requiredSecrets`. Before activation, `ExtensionManager` checks whether each non-optional secret is resolvable from the secret store, the pack options, or the environment. Missing a secret? The descriptor is skipped and the rest of the pack still activates. Optional secrets unlock optional features (like the PII redaction extension's LLM-judge tier) without making them required.
247+
248+
```typescript
249+
// pii-redaction declares its LLM judge as an optional dependency.
250+
{
251+
id: 'pii_judge_resolver',
252+
kind: 'tool',
253+
requiredSecrets: [{ id: 'anthropic.apiKey', optional: true }],
254+
// ...
255+
}
256+
// No ANTHROPIC_API_KEY in env? The judge is skipped, the rest of the pack works.
257+
```
258+
259+
### Layer 4: `SharedServiceRegistry.getOrCreate()` defers heavy resources
260+
261+
ML models, embedding indexes, ONNX runtimes, NER pipelines, and database pools are not loaded at activation. They are registered as factories and only constructed on the first call that needs them. The same instance is shared across descriptors in the same pack and (with a namespaced key) across packs.
262+
263+
```typescript
264+
async onActivate(ctx) {
265+
// The 110MB BERT NER model. Not loaded yet.
266+
const nerModel = await ctx.services.getOrCreate('pii:ner-model', async () => {
267+
const { NerModel } = await import('./NerModel.js');
268+
return NerModel.load();
269+
});
270+
this.scanTool.setNerModel(nerModel);
271+
this.streamingGuardrail.setNerModel(nerModel);
272+
}
273+
```
274+
275+
The `import('./NerModel.js')` is a dynamic import: the model file itself does not enter the module graph until something asks for the service. First call pays the load cost; subsequent calls hit the cache. Tear-down releases everything via the registry's lifecycle.
276+
277+
### What this looks like end-to-end for a guardrail
278+
279+
A host installing `@framers/agentos-ext-pii-redaction`:
280+
281+
1. **Install.** `npm install @framers/agentos-ext-pii-redaction @framers/agentos-extensions-registry @framers/agentos`. Nothing else.
282+
2. **Manifest build.** `createCuratedManifest({ tools: 'all' })` resolves only the installed PII pack and emits a single-pack manifest.
283+
3. **Activation.** `ExtensionManager.loadManifest()` runs `pack.onActivate(ctx)`, which registers a `pii:ner-model` factory in `SharedServiceRegistry`. The model file is not loaded.
284+
4. **Descriptor registration.** Two tool descriptors (`pii_scan`, `pii_redact`) and one guardrail descriptor land in the kind-specific registries. The guardrail descriptor's `config.canSanitize = true` and `config.evaluateStreamingChunks = true` flag it for the two-phase dispatcher.
285+
5. **First request.** A user message hits the input pipeline. The two-phase dispatcher runs Phase 1 sequentially: the PII guardrail's `evaluateInput()` fires, calls into the NER pipeline, and the model loads on this first call. Subsequent requests hit the cached model.
286+
6. **Streaming output.** As the model generates a response, each `TEXT_DELTA` chunk passes through the guardrail's `evaluateOutput()`. The dispatcher returns `SANITIZE` results with redacted text deterministically (Phase 1 chains sequentially), then runs all Phase 2 classifiers in parallel for any remaining checks.
287+
7. **Tear-down.** `pack.onDeactivate(ctx)` runs in reverse order on shutdown. The shared service registry releases the model.
288+
289+
Same pattern for the four other guardrail packs: `@framers/agentos-ext-ml-classifiers` lazy-loads ONNX BERT models, `@framers/agentos-ext-grounding-guard` lazy-loads the NLI pipeline, `@framers/agentos-ext-topicality` lazy-loads embeddings, `@framers/agentos-ext-code-safety` is regex-only and pays no load cost.
290+
291+
This is the same auto-discovery surface that runtime-forged tools join: an agent that invents a function in session N can promote it via `SkillExporter` into a `SKILL.md` that the registry picks up on the next process start. Forging grows the surface mid-run; auto-discovery ships it as a first-class capability.
292+
293+
For the dispatcher mechanics (Phase 1 sanitizers, Phase 2 parallel classifiers, worst-action aggregation, mid-stream override), see [Guardrails](https://docs.agentos.sh/features/guardrails). For lifecycle internals, see [Extension Loading](https://docs.agentos.sh/architecture/extension-loading).
294+
226295
### Registry options
227296

228297
`createCuratedManifest()` accepts:

0 commit comments

Comments
 (0)