NOETIC
Framework
Examples

Deep Agent (DeepAgentsJS Recreation)

A full-featured coding agent with filesystem access, task planning, sub-agent delegation, skills, and context — built entirely from Noetic primitives.

Overview

This example recreates the core capabilities of DeepAgentsJS — a LangGraph-based framework for building AI coding agents — using only Noetic's seven step primitives.

The key insight: middleware = tools + context layers. Every piece of DeepAgentsJS middleware decomposes into either a tool (for mutations/actions) or a context layer (for context projection), composed together with spawn + loop + callModel for isolation.

Architecture Mapping

DeepAgentsJS ConceptNoetic Equivalent
createDeepAgent()spawn wrapping a loop + callModel, with context layers on the spawn boundary
todoListMiddlewaretool() definitions with ToolContextDeclaration + toolCalls()
filesystemMiddlewaretool() definitions using Node.js fs
subAgentMiddlewarecreateConfigurableDelegateTool() using toolCtx.harness
summarizationMiddlewareobservations() with custom observer
skillsMiddlewareCustom ContextLayer with progressive disclosure
memoryMiddleware (upstream)instructions() built-in layer
promptCachingMiddlewareN/A — adapter-level concern
patchToolCallsMiddlewareN/A — Noetic interpreter handles natively
humanInTheLoopMiddlewaretool({ needsApproval: true })

Task Planning Tools

Three tools mirror the todoListMiddleware pattern:

  • write_todos — Creates todo items, writes state via toolCtx.context.set()
  • update_todo — Updates an item's status (pending, in_progress, completed, blocked)
  • list_todos — Reads current state from toolCtx.context.get()

Tools declare a shared ToolContextDeclaration with id: 'todos'toolCalls() materializes this into a single ContextLayer that projects the todo state into the LLM context:

interface TodoState {
  items: { id: string; description: string; status: string }[];
}

declare const STATUS_ICONS: Record<string, string>;

const todoContext: ToolContextDeclaration<TodoState> = {
  id: 'todos',
  init: () => ({ items: [] }),
  recall: (state) => {
    if (!state.items.length) return null;
    const lines = state.items.map(item =>
      `${STATUS_ICONS[item.status]} ${item.id}: ${item.description}`
    );
    return `<todos>\n${lines.join('\n')}\n</todos>`;
  },
};

// Each tool gets `context: todoContext` — shared id means shared state

Filesystem Tools

Six tools with real Node.js fs operations:

  • ls — Directory listing with file type info
  • read_file — File reading with optional offset/limit
  • write_file — File writing with automatic parent directory creation
  • edit_file — String replacement in files
  • glob_files — Pattern matching via Bun.Glob
  • grep_files — Regex search across files

All tools validate paths against rootDir using resolve() + startsWith() to prevent directory traversal.

Sub-Agent Delegation

The createConfigurableDelegateTool uses toolCtx.harness to run sub-agents — no need to pass the agent harness at construction time:

export function createConfigurableDelegateTool(resolver: SubAgentResolver): Tool {
  return tool({
    name: 'delegate',
    execute: async (args, toolCtx) => {
      const config = resolver(args.task);
      const spawnStep = buildConfiguredSubAgentStep(config);
      return toolCtx.harness.run(spawnStep, args.task, toolCtx.ctx);
    },
  });
}

Context Layers

Instructions (via instructions)

Uses the built-in instructions() layer to load instruction files from disk:

instructions({
  load: async () => {
    const contents = await Promise.all(paths.map(p => Bun.file(p).text()));
    return contents.join('\n\n---\n\n');
  },
  tag: 'instructions',
})
  • Slot: SCRATCHPAD + 5 (105)
  • Scope: resource

Todo State (via toolCalls)

Generated automatically from ToolContextDeclaration on the tools:

declare const allTools: Tool[];

toolCalls(allTools); // produces one layer per unique context.id
  • Slot: SCRATCHPAD + 10 (110)
  • Scope: execution

Skills Layer (Progressive Disclosure)

Lists all skill names in recall (string shorthand). When the LLM calls activateSkill, the store hook uses findFunctionCall() to detect it and adds the skill to the activated set.

  • Slot: PROCEDURAL (250)
  • Scope: execution
  • onSpawn: Clones state so sub-agents inherit activated skills

Summarization

Reuses the built-in observations with a custom observer function:

observations({
  bufferThreshold: 4_000,
  observer: async (buffer) => [
    `Summary: processed ${buffer.length} exchanges...`,
  ],
})

Main Composition

The buildDeepAgent function composes everything from spawn, loop, and callModel:

import { any, callModel, instructions, loop, observations, spawn, toolCalls, until } from '@noetic-tools/core';
import type { ContextData, ContextLayer, StepSpawn, Tool } from '@noetic-tools/core';

interface DeepAgentConfig {
  model: string;
  instructions: string;
}

declare const fsTools: Tool[];
declare const todoTools: Tool[];
declare const delegateTool: Tool;
declare function skillsLayer(opts: unknown): ContextLayer;
declare function loadInstructions(): Promise<string>;
declare const observer: (buffer: ReadonlyArray<unknown>) => Promise<string[]>;

export function buildDeepAgent(
  config: DeepAgentConfig,
): StepSpawn<ContextData, string, string> {
  const allTools = [...fsTools, ...todoTools, delegateTool];
  const layers: ContextLayer[] = [
    instructions({ load: loadInstructions, tag: 'instructions' }),
    ...toolCalls(allTools),
    skillsLayer({}),
    observations({ bufferThreshold: 4_000, observer }),
  ];

  const agentLoop = loop({
    id: 'deep-agent-loop',
    steps: [callModel<ContextData, string, string>({
      id: 'deep-agent-llm',
      model: config.model,
      instructions: config.instructions,
      tools: allTools,
    })],
    until: any(until.noToolCalls(), until.maxSteps(50)),
  });

  return spawn<ContextData, string, string>({
    id: 'deep-agent',
    child: agentLoop,
    context: layers,
  });
}

The spawn wrapper creates a context boundary with those context layers, so the loop — and every sub-agent it delegates to — sees them.

What's Different from DeepAgentsJS

Handled natively by Noetic:

  • Tool call lifecycle (parsing, validation, execution) — the interpreter manages this
  • Structured output — Zod schemas on tools handle validation

Adapter-level concerns (not in scope):

  • Prompt caching — handled by the model provider adapter
  • Human-in-the-loop — supported via tool({ needsApproval: true })

Extension points:

  • StorageAdapter interface for persistent state across sessions
  • Custom SubAgentResolver for dynamic sub-agent configuration

On this page