npm package: @manifesto-ai/agent
Description: Session layer that executes LLM as a pure policy function and standardizes all side effects as Effects
Author: Manifesto Team
Latest version: 0.3.0

# @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

```bash
npm install @manifesto-ai/agent
# or
pnpm add @manifesto-ai/agent
```

## Quick Start

```typescript
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.

```typescript
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.

```typescript
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

```typescript
{
  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:

```typescript
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 State
```

### Error as State

When validation fails, errors are recorded as state—not thrown as exceptions:

```typescript
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

```typescript
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

```typescript
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

```typescript
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)

```typescript
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:

```typescript
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

```typescript
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

```typescript
// 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:

```typescript
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)

```typescript
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

```typescript
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

```typescript
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**: `ManifestoCoreLike` is deprecated. Use `AgentRuntime` instead.

## Tools

### Defining Tools

```typescript
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`:

```typescript
// 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

```typescript
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

```typescript
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:

```typescript
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

```typescript
type AgentDecision = {
  effects: Effect[];
  trace?: {
    model?: string;
    tokensIn?: number;
    tokensOut?: number;
    raw?: unknown;
  };
};
```

### AgentClientInput

```typescript
type AgentClientInput<S = unknown> = {
  snapshot: S;
  constraints: Constraints;
  recentErrors?: PatchErrorState[];
  instruction?: string;
  projectionMeta?: ProjectionMetadata;  // Included when using projection
};
```

## Testing

### Mock Client

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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`, `HumanAskEffect`
- `PatchOp`
- `Constraints`, `TypeRule`, `Invariant`
- `Policy`
- `PatchErrorState`, `EffectErrorState`, `HandlerErrorState`, `ErrorState`
- `AgentClient`, `AgentDecision`, `AgentClientInput`
- `AgentSession`, `StepResult`, `RunResult`
- `AgentRuntime`, `ApplyResult`, `DoneChecker`
- `Observation`
- `Tool`, `ToolRegistry`
- `ProjectedSnapshot`, `ProjectionMetadata`, `ProjectionResult`
- `CompressionStrategy`, `ProjectionProviderConfig`, `ProjectionProvider`

### Session

- `createAgentSession(options)` - Create a full session
- `createSimpleSession(options)` - Create a simple session for testing
- `createAgentRuntime(options)` - Create AgentRuntime from DomainRuntime

### Projection

- `createSimpleProjectionProvider(options)` - Path-based projection
- `createIdentityProjectionProvider(config?)` - No projection (passthrough)
- `createDynamicProjectionProvider(options)` - Dynamic path resolution

### Validation

- `validatePathAcl(path, effectId)` - Check write ACL
- `validatePathBounds(path, snapshot, effectId)` - Check array bounds
- `validateTypeRule(path, value, rule, effectId)` - Check type rule
- `validateInvariant(snapshot, invariant)` - Check invariant
- `validatePatchPipeline(ops, snapshot, constraints, effectId)` - Full validation

### Handlers

- `createDefaultHandlerRegistry()` - Create registry with default handlers
- `createToolCallHandler(tools, core)` - Create tool call handler
- `createSnapshotPatchHandler(core, constraints)` - Create patch handler
- `createLogEmitHandler(collector?)` - Create log handler

### Prompt

- `SYSTEM_PROMPT` - Base system prompt with Iron Laws
- `buildSystemPrompt(options?)` - Build customized system prompt
- `buildStepPrompt(input, options?)` - Build per-step prompt
- `buildLLMMessages(systemPrompt, stepInput)` - Build message array

### Utilities

- `generateEffectId()` - Generate unique effect ID
- `generateObservationId()` - Generate unique observation ID
- `createDefaultConstraints(phase?)` - Create default constraints
- `createMockClient(decisions)` - Create mock client for testing
- `createFixedClient(effects)` - Create fixed-response client
- `defaultDoneChecker()` - Always returns not done
- `phaseDoneChecker(targetPhase)` - Done when phase matches

### Tools

- `defineTool(name, inputSchema, execute)` - Define a tool with name, Zod schema, and handler
- `createToolRegistry(tools)` - Create a registry from tool array

### Invariant Helpers

- `requiredFieldInvariant(path)` - Field must exist
- `rangeInvariant(path, min, max)` - Number in range
- `arrayLengthInvariant(path, min, max)` - Array length constraint
- `customInvariant(id, message, predicate)` - Custom validation

## License

MIT

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.

## Related Packages

- [@manifesto-ai/core](../core) - Core snapshot management