mem-deep-research
v0.1.0
Published
AI Agent deep research framework - TypeScript rewrite
Downloads
104
Readme
Mem Deep Research (TypeScript)
An extensible AI Agent framework for deep research tasks. Built on MCP (Model Context Protocol) with multi-LLM provider support.
TypeScript rewrite of mem-deep-research (Python).
Features
- MCP Tool System: stdio, streamable-http, and SSE transport modes
- Three-tier Context Management: Observation Masking → LLM Summarization → Binary Reduction
- Tool Call Deduplication: Cross-turn dedup with hit-count tracking and progressive escalation
- Execution Monitoring: 3-tier escalation (WARN → INJECT_HINT → TERMINATE) with loop detection
- Skill System: Rules-based, LLM-based, and inline selection modes
- Hook System: Lifecycle hooks with chain-of-responsibility pattern
- SecureContext: Automatic sensitive data isolation with
[SECURE:xxx]placeholder substitution - Prompt Templates: Flexible template system with
{{var}}substitution and preset combinations - Multi-LLM Support: Anthropic, OpenAI, OpenRouter, DeepSeek
- Deep Research Mode: Reflection checkpoints and automatic task planning
Requirements
- Node.js >= 20.0.0
- pnpm (recommended)
Installation
# From npm (when published)
pnpm add mem-deep-research
# From source
git clone <repo-url>
cd mem-deep-research-js
pnpm install
pnpm buildQuick Start
Option 1: TypeScript / JavaScript API
import { DeepResearch } from "mem-deep-research";
// Load from a project directory
const dr = await DeepResearch.fromProject("./my_project");
const result = await dr.run("Research the latest developments in AI agents");
console.log(result.answer);Option 2: CLI
# Create a new project
npx mem-deep-research init my_project
# Run a research task
npx mem-deep-research run "Your research task"
# Run with a specific config
npx mem-deep-research run --config ./config/agent.yaml "Your task"Option 3: Programmatic Configuration
import { DeepResearch } from "mem-deep-research";
const dr = await DeepResearch.fromConfigDir("./config", {
context: {
user_name: "Alice",
_secure: { user_id: "real-123" },
},
});
const result = await dr.run("Your task");Project Structure
A user project loaded via DeepResearch.fromProject():
my_project/
├── config/
│ ├── agent.yaml # Agent configuration (LLM, tools, parameters)
│ ├── tool/ # Tool configs (override framework defaults)
│ ├── skills/definitions/ # Custom skill definitions (.md)
│ └── prompts/ # Custom prompt templates
├── hooks.ts # Lifecycle hooks (auto-loaded)
├── .env # API keys
└── run.ts # Entry scriptMinimal config/agent.yaml
main_agent:
llm:
provider_class: "ClaudeOpenRouterClient"
model_name: "anthropic/claude-sonnet-4"
temperature: 0.3
max_tokens: 32000
tool_config:
- tool-searching-serper
max_turns: 20Full Configuration Reference
main_agent:
prompt:
agent_type: main # main | worker
tool_format: xml # xml | native
presets: [] # e.g. [research, time_sensitive]
llm:
provider_class: "ClaudeOpenRouterClient"
model_name: "anthropic/claude-sonnet-4"
temperature: 0.3
max_tokens: 32000
max_context_length: 128000 # -1 = unlimited
keep_tool_result: 5 # -1 = keep all, N = keep last N
tool_config: [tool-searching-serper]
max_turns: 20
max_tool_calls_per_turn: 10
chinese_context: false
skill_selection:
enabled: true
method: inline # rules | llm | inline
max_skills: 3
context_manager:
enable_dedup: true
enable_compact: true
compact_at_ratio: 0.6
summarize_at_ratio: 0.8
compact_keep_recent: 3
monitoring:
stall_detection_threshold: 120.0
max_total_time: 600.0
enable_loop_detection: true
loop_escalation_terminate_threshold: 3
deep_research:
enabled: false
reflection_interval: 5
auto_planning: falseTool Configuration
Local Tool (stdio)
# config/tool/tool-my-custom.yaml
name: "tool-my-custom"
tool_command: "node"
args:
- "tools/my_tool_server.js"
env:
MY_API_KEY: "${MY_API_KEY}"Remote Tool (streamable-http)
# config/tool/tool-remote.yaml
name: "tool-remote"
url: "https://api.example.com/mcp"
transport: "streamable-http"
headers:
Authorization: "Bearer ${API_TOKEN}"Hook System
// hooks.ts (in your project directory, auto-loaded)
import { HookRegistry, createHookContext } from "mem-deep-research";
export function registerHooks(hooks: HookRegistry) {
// Inject environment variables into MCP server params
hooks.registerFn("on_env_inject", (ctx, original) => {
const params = original(ctx);
params.env = { ...params.env, MY_KEY: process.env.MY_KEY ?? "" };
return params;
}, 10); // priority
// Custom tool result formatting
hooks.registerFn("on_tool_result_format", (ctx, original) => {
if (ctx.toolName === "my_tool") {
return "Custom format";
}
return original(ctx);
});
}Available Hooks
| Hook | Timing | Modifiable |
|------|--------|------------|
| on_agent_start | Agent starts | — |
| on_agent_end | Agent completes | — |
| on_turn_start | Each turn starts | — |
| on_turn_end | Each turn ends | — |
| on_tool_start | Before tool call | arguments |
| on_tool_end | After tool call | tool_result |
| on_tool_result_format | Result formatting | return value |
| on_thinking_generate | Thinking description | return value |
| on_env_inject | MCP env vars | server_params |
| on_message_intercept | Message interception | — |
SecureContext
Sensitive fields in the context are automatically masked in the system prompt and restored before tool execution:
const context = {
user_name: "Alice", // Visible to LLM
_secure: {
user_id: "real-123", // LLM sees [SECURE:user_id]
api_token: "secret-456", // LLM sees [SECURE:api_token]
},
};
// Tool calls with [SECURE:user_id] are auto-replaced with "real-123"LLM Providers
| Provider | Class | Transport |
|----------|-------|-----------|
| Anthropic (native) | ClaudeAnthropicClient | Anthropic SDK |
| OpenAI (native) | GPTOpenAIClient | OpenAI SDK |
| OpenRouter Claude | ClaudeOpenRouterClient | OpenAI-compatible |
| OpenRouter GPT-5 | GPT5OpenRouterClient | OpenAI-compatible |
| OpenAI GPT-5 | GPT5OpenAIClient | OpenAI SDK |
| DeepSeek | DeepSeekOpenRouterClient | OpenAI-compatible |
Registering Custom Providers
import { LLMClient, registerProvider, LLMProviderClientBase } from "mem-deep-research";
class MyCustomClient extends LLMProviderClientBase {
// ... implement abstract methods
}
registerProvider("MyCustomClient", MyCustomClient);
// Now usable in config:
// provider_class: "MyCustomClient"Environment Variables
# .env
OPENROUTER_API_KEY=your_key
ANTHROPIC_API_KEY=your_key
OPENAI_API_KEY=your_key
DEEPSEEK_API_KEY=your_key
SERPER_API_KEY=your_keyFramework Directory Structure
src/
├── index.ts # Public API exports
├── deep-research.ts # Main entry (DeepResearch class)
├── config-schema.ts # Zod config validation
├── types.ts # Shared TypeScript interfaces
├── exceptions.ts # Error hierarchy
├── core/ # Core modules
│ ├── orchestrator.ts # Agent orchestrator
│ ├── main-loop.ts # Main loop runner
│ ├── pipeline.ts # Task pipeline
│ ├── agent-factory.ts # Agent factory
│ ├── monitoring.ts # Execution monitor + loop detection
│ ├── context-manager.ts # Context management (masking + dedup)
│ ├── window-strategy.ts # 3-tier compression strategies
│ ├── secure-context.ts # Sensitive data isolation
│ ├── hooks.ts # Hook system
│ ├── task-planner.ts # LLM task decomposition
│ ├── tool-executor.ts # Tool execution with hooks
│ ├── llm-call-handler.ts # LLM call handling with retry
│ ├── sub-agent-runner.ts # Sub-agent execution
│ ├── stream-handler.ts # SSE streaming
│ ├── interceptor-config.ts # Message interceptor config
│ ├── message-interceptor.ts # Message filtering
│ └── user-context.ts # User context builder
├── llm/ # LLM clients
│ ├── provider-client-base.ts # Abstract base class
│ ├── client.ts # Factory with provider registry
│ └── providers/ # Provider implementations
├── tool/ # MCP tool module
│ └── manager.ts # ToolManager (stdio/SSE/HTTP)
├── prompts/ # Prompt system
│ ├── agent-prompt.ts # AgentPrompt class
│ ├── template-loader.ts # Template loader
│ └── templates/ # Markdown templates
├── skills/ # Skill selection system
│ ├── matcher.ts # Skill matching + injection
│ ├── inline-selector.ts # Inline <next_skills> selection
│ └── llm-selector.ts # LLM-based selection
├── logging/ # Logging
│ ├── logger.ts # Winston + AsyncLocalStorage
│ └── task-tracer.ts # JSON task tracing
├── cli/ # CLI
│ ├── main.ts # Commander CLI entry
│ └── templates.ts # Project scaffolding
└── utils/ # Utilities
├── parsing-utils.ts # JSON parsing, XML tool calls
├── stream-parsing-utils.ts # Streaming tag extraction
├── io-utils.ts # I/O formatting
├── tool-utils.ts # MCP helper functions
├── summary-utils.ts # Language detection, hints
└── external-loader.ts # Config loader
config/ # Default configs (YAML)
tests/ # Vitest unit testsDevelopment
# Install dependencies
pnpm install
# Build (ESM + CJS + .d.ts)
pnpm build
# Run tests
pnpm test
# Type-check without emitting
pnpm typecheck
# Watch mode
pnpm dev # rebuild on change
pnpm test:watch # rerun tests on changeArchitecture
The framework follows a pipeline architecture:
User Query
→ TaskPlanner (optional decomposition)
→ Orchestrator
→ MainLoopRunner (turn-by-turn)
→ LLMCallHandler (LLM interaction with retry)
→ ToolExecutor (MCP tool calls with hooks)
→ ContextManager (dedup + compression)
→ ExecutionMonitor (loop detection + escalation)
→ AnswerHandler (final answer extraction)
→ ResultKey design principles:
- Composition over inheritance: Provider registry
Mapinstead of dynamic imports - Zod schemas: Runtime validation with inferred TypeScript types
- Hook system: Chain-of-responsibility for extensibility without subclassing
- Three-tier context management: Graceful degradation as context fills up
- Secure by default: Sensitive data never reaches the LLM in cleartext
License
MIT
