Deep Agent (DeepAgentsJS Recreation)
A full-featured coding agent with filesystem access, task planning, sub-agent delegation, skills, and memory — 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 + memory layers. Every piece of DeepAgentsJS middleware decomposes into either a tool (for mutations/actions) or a memory layer (for context projection), composed together with react() + memory for isolation.
Architecture Mapping
| DeepAgentsJS Concept | Noetic Equivalent |
|---|---|
createDeepAgent() | react() with memory param (wraps in spawn automatically) |
| todoListMiddleware | tool() definitions with ToolMemoryDeclaration + toolMemoryLayer() |
| filesystemMiddleware | tool() definitions using Node.js fs |
| subAgentMiddleware | createConfigurableDelegateTool() using toolCtx.harness |
| summarizationMiddleware | observationalMemory() with custom observer |
| skillsMiddleware | Custom MemoryLayer with progressive disclosure |
| memoryMiddleware | staticContent() built-in layer |
| promptCachingMiddleware | N/A — adapter-level concern |
| patchToolCallsMiddleware | N/A — Noetic interpreter handles natively |
| humanInTheLoopMiddleware | tool({ needsApproval: true }) |
Task Planning Tools
Three tools mirror the todoListMiddleware pattern:
write_todos— Creates todo items, writes state viatoolCtx.memory.set()update_todo— Updates an item's status (pending, in_progress, completed, blocked)list_todos— Reads current state fromtoolCtx.memory.get()
Tools declare a shared ToolMemoryDeclaration with id: 'todos' — toolMemoryLayer() materializes this into a single MemoryLayer that projects the todo state into the LLM context:
const todoMemory: ToolMemoryDeclaration<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 `memory: todoMemory` — shared id means shared stateFilesystem Tools
Six tools with real Node.js fs operations:
ls— Directory listing with file type inforead_file— File reading with optional offset/limitwrite_file— File writing with automatic parent directory creationedit_file— String replacement in filesglob_files— Pattern matching viaBun.Globgrep_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);
},
});
}Memory Layers
Instructions (via staticContent)
Uses the built-in staticContent() layer to load instruction files from disk:
staticContent({
load: async () => {
const contents = await Promise.all(paths.map(p => Bun.file(p).text()));
return contents.join('\n\n---\n\n');
},
tag: 'instructions',
})- Slot:
WORKING_MEMORY + 5(105) - Scope:
resource
Todo Memory (via toolMemoryLayer)
Generated automatically from ToolMemoryDeclaration on the tools:
toolMemoryLayer(allTools) // produces one layer per unique memory.id- Slot:
WORKING_MEMORY + 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 observationalMemory with a custom observer function:
observationalMemory({
bufferThreshold: 4_000,
observer: async (buffer) => [
`Summary: processed ${buffer.length} exchanges...`,
],
})Main Composition
The buildDeepAgent function composes everything using react() with memory:
import { observationalMemory, react, staticContent, toolMemoryLayer } from '@noetic/core';
import type { MemoryLayer, StepLoop, StepSpawn, Tool } from '@noetic/core';
interface DeepAgentConfig {
model: string;
instructions: string;
}
declare const fsTools: Tool[];
declare const todoTools: Tool[];
declare const delegateTool: Tool;
declare function skillsLayer(opts: unknown): MemoryLayer;
declare function loadInstructions(): Promise<string>;
declare const observer: (buffer: ReadonlyArray<unknown>) => Promise<string[]>;
export function buildDeepAgent(config: DeepAgentConfig): StepLoop | StepSpawn {
const allTools = [...fsTools, ...todoTools, delegateTool];
const layers: MemoryLayer[] = [
staticContent({ load: loadInstructions, tag: 'instructions' }),
...toolMemoryLayer(allTools),
skillsLayer({}),
observationalMemory({ bufferThreshold: 4_000, observer }),
];
return react({
model: config.model,
instructions: config.instructions,
tools: allTools,
memory: layers,
});
}When memory is provided, react() automatically wraps the loop in a spawn that creates a context boundary with those memory layers.
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:
StorageAdapterinterface for persistent memory across sessions- Custom
SubAgentResolverfor dynamic sub-agent configuration