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 Concept | Noetic Equivalent |
|---|---|
createDeepAgent() | spawn wrapping a loop + callModel, with context layers on the spawn boundary |
| todoListMiddleware | tool() definitions with ToolContextDeclaration + toolCalls() |
| filesystemMiddleware | tool() definitions using Node.js fs |
| subAgentMiddleware | createConfigurableDelegateTool() using toolCtx.harness |
| summarizationMiddleware | observations() with custom observer |
| skillsMiddleware | Custom ContextLayer with progressive disclosure |
| memoryMiddleware (upstream) | instructions() 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.context.set()update_todo— Updates an item's status (pending, in_progress, completed, blocked)list_todos— Reads current state fromtoolCtx.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 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);
},
});
}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:
StorageAdapterinterface for persistent state across sessions- Custom
SubAgentResolverfor dynamic sub-agent configuration