@manifesto-ai/agent
v0.3.0
Published
Session layer that executes LLM as a pure policy function and standardizes all side effects as Effects
Maintainers
Readme
@manifesto-ai/agent
A session layer that executes LLM as a "pure policy function," standardizing all side effects as Effects with Runtime control.
f(snapshot) → effects[]Overview
@manifesto-ai/agent treats the LLM as a deterministic component—not the master, but a CPU-level policy executor. The Runtime owns all control flow, validates all outputs, and manages all side effects.
Core Principles
| Principle | Description |
|-----------|-------------|
| LLM as Component | f(snapshot) → effects[] — LLM declares intent, Runtime executes |
| Runtime Owns Control | step/run, budget, stop — Control flow always belongs to Runtime |
| Enforce, Don't Inject | Schema → Constraints compilation + Validator Gatekeeping |
| Errors Become State | No exception crashes — failures are recorded and available for self-correction |
| Sequential + Stop-on-Failure | Partial failure discards remaining effects |
Installation
npm install @manifesto-ai/agent
# or
pnpm add @manifesto-ai/agentQuick Start
import {
createSimpleSession,
createMockClient,
generateEffectId,
type AgentDecision,
} from '@manifesto-ai/agent';
// Define decisions the mock LLM will return
const decisions: AgentDecision[] = [
{
effects: [
{
type: 'snapshot.patch',
id: generateEffectId(),
ops: [{ op: 'set', path: 'data.status', value: 'processing' }],
},
{
type: 'log.emit',
id: generateEffectId(),
level: 'info',
message: 'Started processing',
},
],
},
{ effects: [] }, // Empty effects = session complete
];
// Create a simple session
const { session, getSnapshot } = createSimpleSession({
initialSnapshot: {
data: { status: 'idle' },
state: { phase: 'init' },
derived: {},
},
client: createMockClient(decisions),
});
// Run the session
const result = await session.run();
console.log(result);
// { done: true, totalSteps: 2, totalEffects: 2, reason: 'Empty effects' }
console.log(getSnapshot().data.status);
// 'processing'Core Concepts
Effect Types
Effects are declarations of intent. The LLM declares what it wants; the Runtime decides whether and how to execute.
type Effect =
| ToolCallEffect // Call an external tool
| SnapshotPatchEffect // Modify snapshot state
| LogEmitEffect; // Emit a log message
// Tool call
{
type: 'tool.call',
id: 'eff_abc123',
tool: 'search',
input: { query: 'manifesto architecture' }
}
// Snapshot patch
{
type: 'snapshot.patch',
id: 'eff_def456',
ops: [
{ op: 'set', path: 'data.results', value: [...] },
{ op: 'append', path: 'data.history', value: 'searched' }
],
reason: 'Store search results'
}
// Log emit
{
type: 'log.emit',
id: 'eff_ghi789',
level: 'info',
message: 'Processing complete'
}HumanAskEffect (v0.2 Preview)
Type definition only in v0.1. Runtime support planned for v0.2.
type HumanAskEffect = {
type: 'human.ask';
id: string;
question: string;
options?: string[];
};PatchOp Operations
Only two operations are allowed in v0.1:
| Operation | Description | Example |
|-----------|-------------|---------|
| set | Set a value at path | { op: 'set', path: 'data.name', value: 'John' } |
| append | Append to array | { op: 'append', path: 'data.items', value: 'new' } |
Path Rules:
- Dot-separated:
"data.user.name" - Array indices:
"data.items.0.status" - 0-based indexing with bounds checking
Forbidden: delete, move, replace, copy
Snapshot Structure & ACL
{
data: { ... }, // LLM writable
state: { ... }, // LLM writable (phase, etc.)
derived: { ... } // LLM write FORBIDDEN (Runtime managed)
}The derived.* namespace is exclusively managed by the Runtime. Any LLM attempt to write to derived.* will be rejected with a validation error.
Constraints
Constraints define what the LLM can and cannot do in the current phase:
import {
createDefaultConstraints,
addTypeRule,
addInvariant,
} from '@manifesto-ai/agent';
let constraints = createDefaultConstraints('processing');
// Add type rules
constraints = addTypeRule(constraints, 'data.count', 'number');
constraints = addTypeRule(constraints, 'data.name', 'string');
// Add invariants
constraints = addInvariant(
constraints,
'count_positive',
'Count must be positive'
);Validation Pipeline
Every snapshot.patch effect goes through a validation pipeline:
PatchOp received
↓
1. Schema check (op/path/value structure)
↓
2. ACL check (derived.* write forbidden)
↓
3. Bounds check (array index range)
↓
4. Type rules check (expected types)
↓
5. Invariant check (phase-specific rules)
↓
Pass → Apply Fail → Record Error StateError as State
When validation fails, errors are recorded as state—not thrown as exceptions:
const errors = getErrors();
// [
// {
// kind: 'patch_validation_error',
// at: 'derived.x',
// issue: 'Forbidden path',
// advice: 'derived.* paths are Runtime-managed',
// effectId: 'eff_abc123',
// ts: 1704067200000
// }
// ]The LLM receives these errors in the next step and can self-correct.
Projection (v0.1.x)
Projection allows you to control what portion of the snapshot is sent to the LLM, managing token budgets and focusing context on relevant data.
Why Projection?
- Token Budget Management: Large snapshots can exceed LLM context limits
- Focused Context: Send only relevant data for current task
- Cost Optimization: Smaller prompts reduce API costs
Using Projection
Simple Configuration
import { createAgentSession } from '@manifesto-ai/agent';
const session = createAgentSession({
runtime: myRuntime,
client: myClient,
projection: {
paths: ['data.currentItem', 'state.phase', 'derived.observations'],
tokenBudget: 4000,
compressionStrategy: 'truncate',
requiredPaths: ['state.phase'],
excludePaths: ['data.largeBlob'],
},
});Custom Projection Provider
import { createSimpleProjectionProvider } from '@manifesto-ai/agent';
const provider = createSimpleProjectionProvider({
paths: ['data.currentItem', 'state.phase'],
config: {
tokenBudget: 4000,
compressionStrategy: 'truncate',
},
});
const session = createAgentSession({
runtime: myRuntime,
client: myClient,
projectionProvider: provider,
});Dynamic Projection
import { createDynamicProjectionProvider } from '@manifesto-ai/agent';
const provider = createDynamicProjectionProvider({
pathResolver: (snapshot) => {
const paths = ['state.phase'];
if (snapshot.state.phase === 'editing') {
paths.push('data.currentItem', 'data.editHistory');
} else if (snapshot.state.phase === 'reviewing') {
paths.push('data.items', 'derived.summary');
}
return paths;
},
config: { tokenBudget: 4000 },
});Identity Projection (No Projection)
import { createIdentityProjectionProvider } from '@manifesto-ai/agent';
// Passes full snapshot through (for small snapshots or testing)
const provider = createIdentityProjectionProvider();Projection Metadata
The LLM client receives projection metadata along with the snapshot:
type ProjectionMetadata = {
isProjected: boolean; // Whether projection was applied
tokenCount: number; // Estimated token count
includedPaths: string[]; // Paths included in projection
compressed?: boolean; // Whether compression was applied
compressionStrategy?: CompressionStrategy;
};Compression Strategies
| Strategy | Description |
|----------|-------------|
| truncate | Limit array sizes progressively until under budget |
| summarize | (Future) Use LLM to summarize large sections |
| prioritize | (Future) Keep high-priority paths, drop low-priority |
Session API
Creating a Session
import {
createAgentSession,
createDefaultHandlerRegistry,
createDefaultConstraints,
} from '@manifesto-ai/agent';
const session = createAgentSession({
runtime: agentRuntime, // AgentRuntime implementation
client: llmClient, // AgentClient implementation
policy: {
maxSteps: 100,
maxEffectsPerStep: 16,
},
handlers: createDefaultHandlerRegistry(),
compileConstraints: (snapshot) => createDefaultConstraints(snapshot.state.phase),
});Running a Session
// Single step
const stepResult = await session.step();
// { done: boolean, effectsExecuted: number, errorsEncountered: number }
// Run until completion
const runResult = await session.run();
// { done: boolean, totalSteps: number, totalEffects: number, reason?: string }Done Checkers
Control when the session completes:
import { phaseDoneChecker } from '@manifesto-ai/agent';
const session = createAgentSession({
runtime: myRuntime,
client: myClient,
isDone: phaseDoneChecker('complete'),
});
// Custom done checker
const session = createAgentSession({
runtime: myRuntime,
client: myClient,
isDone: (snapshot) => ({
done: snapshot.data.items.length >= 10,
reason: 'Maximum items reached',
}),
});Simple Session (for Testing)
import { createSimpleSession } from '@manifesto-ai/agent';
const { session, getSnapshot, getErrors, getObservations } = createSimpleSession({
initialSnapshot: { data: {}, state: {}, derived: {} },
client: yourClient,
policy: { maxSteps: 50 },
isDone: (snapshot) => ({
done: snapshot.state.phase === 'complete',
reason: 'Phase complete',
}),
});AgentRuntime
Using with @manifesto-ai/core
import { createRuntime } from '@manifesto-ai/core';
import { createAgentRuntime, createAgentSession } from '@manifesto-ai/agent';
// Create domain runtime from core
const domainRuntime = createRuntime({ domain: myDomain });
// Wrap with agent runtime adapter
const agentRuntime = createAgentRuntime({
domainRuntime,
maxErrors: 100,
maxObservations: 1000,
});
// Create session with agent runtime
const session = createAgentSession({
runtime: agentRuntime,
client: myClient,
});AgentRuntime Interface
interface AgentRuntime<S> {
getSnapshot(): S;
applyPatch(ops: PatchOp[]): ApplyResult<S>;
appendError(error: ErrorState): void;
getRecentErrors(limit?: number): PatchErrorState[];
clearErrors(): void;
appendObservation(obs: Observation): void;
}Note:
ManifestoCoreLikeis deprecated. UseAgentRuntimeinstead.
Tools
Defining Tools
import { defineTool, createToolRegistry } from '@manifesto-ai/agent';
import { z } from 'zod';
// defineTool(name, inputSchema, execute)
const searchTool = defineTool(
'search',
z.object({ query: z.string() }),
async (input) => {
const results = await performSearch(input.query);
return { results };
}
);
const calculatorTool = defineTool(
'calculate',
z.object({ expression: z.string() }),
async (input) => {
return { result: eval(input.expression) };
}
);
const toolRegistry = createToolRegistry([searchTool, calculatorTool]);Tool Results as Observations
When a tool executes, its result is automatically pushed to derived.observations:
// After tool.call effect executes:
snapshot.derived.observations = [
{
id: 'obs_xyz789',
source: 'tool:search',
content: { results: [...] },
triggeredBy: 'eff_abc123',
ts: 1704067200000
}
];Prompt Building
System Prompt
import { SYSTEM_PROMPT, buildSystemPrompt } from '@manifesto-ai/agent';
// Base system prompt with Iron Laws
console.log(SYSTEM_PROMPT);
// With tools and context
const prompt = buildSystemPrompt({
includeToolList: true,
tools: [
{ name: 'search', description: 'Search the web' },
],
additionalContext: 'You are working on a todo application.',
});Step Prompt
import { buildStepPrompt, createDefaultConstraints } from '@manifesto-ai/agent';
const prompt = buildStepPrompt({
snapshot: currentSnapshot,
constraints: createDefaultConstraints('processing'),
recentErrors: errors,
instruction: 'Process the next item in the queue.',
});AgentClient Interface
Implement this interface to connect your LLM:
import type { AgentClient, AgentDecision, Constraints } from '@manifesto-ai/agent';
const myClient: AgentClient = {
async decide(input) {
const { snapshot, constraints, recentErrors, instruction, projectionMeta } = input;
// Call your LLM here
const response = await callLLM({
systemPrompt: buildSystemPrompt(),
userPrompt: buildStepPrompt({
snapshot,
constraints,
recentErrors,
instruction,
}),
});
// Parse and return AgentDecision
return JSON.parse(response) as AgentDecision;
},
};AgentDecision Schema
type AgentDecision = {
effects: Effect[];
trace?: {
model?: string;
tokensIn?: number;
tokensOut?: number;
raw?: unknown;
};
};AgentClientInput
type AgentClientInput<S = unknown> = {
snapshot: S;
constraints: Constraints;
recentErrors?: PatchErrorState[];
instruction?: string;
projectionMeta?: ProjectionMetadata; // Included when using projection
};Testing
Mock Client
import { createMockClient, generateEffectId } from '@manifesto-ai/agent';
const decisions = [
{
effects: [
{
type: 'snapshot.patch',
id: generateEffectId(),
ops: [{ op: 'set', path: 'data.x', value: 1 }],
},
],
},
{ effects: [] },
];
const client = createMockClient(decisions);Fixed Client
import { createFixedClient, generateEffectId } from '@manifesto-ai/agent';
// Returns the same effects every time
const client = createFixedClient([
{
type: 'log.emit',
id: generateEffectId(),
level: 'info',
message: 'Hello',
},
]);Invariant Helpers
import {
requiredFieldInvariant,
rangeInvariant,
arrayLengthInvariant,
customInvariant,
} from '@manifesto-ai/agent';
// Field must exist and not be null/undefined
const hasName = requiredFieldInvariant('data.user.name');
// Number must be in range
const validAge = rangeInvariant('data.user.age', 0, 150);
// Array length constraint
const maxItems = arrayLengthInvariant('data.items', 0, 100);
// Custom validation
const customRule = customInvariant(
'positive_balance',
'Balance must be positive',
(snapshot) => snapshot.data.balance > 0
);Error Types
import type {
PatchErrorState,
EffectErrorState,
HandlerErrorState,
} from '@manifesto-ai/agent';
// Patch validation error
type PatchErrorState = {
kind: 'patch_validation_error';
at: string; // Problem path
issue: string; // Error description
expected?: unknown; // Expected type/value
got?: unknown; // Actual type/value
advice?: string; // Correction hint
effectId: string;
ts: number;
};Configuration
Policy
type Policy = {
maxSteps: number; // Maximum steps before forced stop
maxEffectsPerStep?: number; // Default: 16
};Architecture
┌─────────────────────────────────────────────────────────────┐
│ @manifesto-ai/agent │
│ (LLM policy execution + Effect standardization + │
│ Projection + Runtime enforcement) │
└─────────────────────────┬───────────────────────────────────┘
│ uses
┌─────────────────────────▼───────────────────────────────────┐
│ @manifesto-ai/core │
│ (Snapshot storage/transition/logging — │
│ Constitution + minimal infra) │
└─────────────────────────────────────────────────────────────┘API Reference
Types
Effect,ToolCallEffect,SnapshotPatchEffect,LogEmitEffect,HumanAskEffectPatchOpConstraints,TypeRule,InvariantPolicyPatchErrorState,EffectErrorState,HandlerErrorState,ErrorStateAgentClient,AgentDecision,AgentClientInputAgentSession,StepResult,RunResultAgentRuntime,ApplyResult,DoneCheckerObservationTool,ToolRegistryProjectedSnapshot,ProjectionMetadata,ProjectionResultCompressionStrategy,ProjectionProviderConfig,ProjectionProvider
Session
createAgentSession(options)- Create a full sessioncreateSimpleSession(options)- Create a simple session for testingcreateAgentRuntime(options)- Create AgentRuntime from DomainRuntime
Projection
createSimpleProjectionProvider(options)- Path-based projectioncreateIdentityProjectionProvider(config?)- No projection (passthrough)createDynamicProjectionProvider(options)- Dynamic path resolution
Validation
validatePathAcl(path, effectId)- Check write ACLvalidatePathBounds(path, snapshot, effectId)- Check array boundsvalidateTypeRule(path, value, rule, effectId)- Check type rulevalidateInvariant(snapshot, invariant)- Check invariantvalidatePatchPipeline(ops, snapshot, constraints, effectId)- Full validation
Handlers
createDefaultHandlerRegistry()- Create registry with default handlerscreateToolCallHandler(tools, core)- Create tool call handlercreateSnapshotPatchHandler(core, constraints)- Create patch handlercreateLogEmitHandler(collector?)- Create log handler
Prompt
SYSTEM_PROMPT- Base system prompt with Iron LawsbuildSystemPrompt(options?)- Build customized system promptbuildStepPrompt(input, options?)- Build per-step promptbuildLLMMessages(systemPrompt, stepInput)- Build message array
Utilities
generateEffectId()- Generate unique effect IDgenerateObservationId()- Generate unique observation IDcreateDefaultConstraints(phase?)- Create default constraintscreateMockClient(decisions)- Create mock client for testingcreateFixedClient(effects)- Create fixed-response clientdefaultDoneChecker()- Always returns not donephaseDoneChecker(targetPhase)- Done when phase matches
Tools
defineTool(name, inputSchema, execute)- Define a tool with name, Zod schema, and handlercreateToolRegistry(tools)- Create a registry from tool array
Invariant Helpers
requiredFieldInvariant(path)- Field must existrangeInvariant(path, min, max)- Number in rangearrayLengthInvariant(path, min, max)- Array length constraintcustomInvariant(id, message, predicate)- Custom validation
License
MIT
Contributing
See CONTRIBUTING.md for guidelines.
Related Packages
- @manifesto-ai/core - Core snapshot management
