Skip to content

Commit b58fc71

Browse files
oratisclaude
andauthored
feat(core): M1 kernel MVP — DeepSeek provider + 6 P0 tools + agent loop + sessions (#1)
Implements DEVELOPMENT_PLAN.md §6 M1 in full (except trust dialog, deferred to M2 where the CLI/onboarding surface lives — kernel exposes the session/snapshot primitives that M2 will consume). What ships ---------- - DeepSeekProvider (packages/core/src/providers/deepseek.ts) - OpenAI-compatible streaming via injected `fetch` - Handles `content`, `reasoning_content`, `tool_calls` deltas - Dual credential (apiKey X-Api-Key OR authToken Bearer) - DEEPSEEK_MODELS and EFFORT_PARAMS enforce 8192 max_tokens hard limit - anthropicShapeToOpenAI boundary converter (StoredMessage[] ↔ chat.completions) - 6 P0 tools (packages/core/src/tools/{read,write,edit,bash,grep,glob}.ts) - Read: numbered lines + offset/limit + line-width truncation - Write: creates parent dirs - Edit: exact-string replacement, fails on non-unique unless replace_all - Bash: spawn /bin/sh -c, timeout, stdout/stderr capture, 30KB cap each - Grep: ripgrep via execFile, graceful (no matches) handling - Glob: built-in fs.glob (Node 22+), mtime-desc sort - ToolRegistry + BUILTIN_TOOLS for one-line wire-up - Sessions (packages/core/src/sessions/) - jsonl message log + .meta.json sidecar - Snapshots (sha256-keyed blobs + manifest.jsonl) — pre/post Edit/Write - SessionManager facade (create / load / list / append / snapshot) - Agent loop (packages/core/src/agent.ts) - provider ↔ tools ↔ session orchestration - history-snapshotting per turn (provider sees stable input) - automatic snapshot on Edit/Write tool calls - AbortSignal-aware, maxTurns cap, comprehensive AgentEvent stream Tests (62 passed, 4 skipped, 0 failed in ~1.3s) ----------------------------------------------- - providers/deepseek.test.ts (13) — streaming text / reasoning_content / tool calls / msg-shape conversion / auth variants / model invariants - tools/{read,write,edit,bash,grep,glob}.test.ts (31 tests) — real fs / real exec - sessions/{storage,snapshots}.test.ts (11) — round-trip, manifest, restore - agent.test.ts (7) — end_turn / tool dispatch / unknown tool / maxTurns / abort / session+snapshots / multi-turn history feedback Mock strategy: MockProvider for agent tests (deterministic), mockFetch returning SSE chunks for provider tests. Zero new test deps. Docs ---- - docs/core-api.md — full public API surface + storage layout + what M1 doesn't - docs/milestones/M1.md — milestone postmortem, design decisions, known gaps Verified -------- pnpm typecheck → green pnpm build → all packages emit dist/ pnpm test → 62 passed / 4 skipped (ripgrep-dependent) / 0 failed pnpm format:check → all conformant Bugs found and fixed in-flight ------------------------------ - `apiKey ?? authToken` failed for `apiKey: ''` (nullish only) — switched to `||` - `messages: history` passed by reference let later turns mutate earlier calls' record → `messages: [...history]` snapshot per call Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b39d99 commit b58fc71

31 files changed

Lines changed: 2810 additions & 42 deletions

docs/core-api.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# `@deepcode/core` API Reference
2+
3+
> **Status**: M1 — kernel MVP shipped. Surface area will grow per milestone.
4+
> **Spec**: `DEVELOPMENT_PLAN.md` §3.1 (provider), §3.2 (tools), §3.5 (sessions).
5+
6+
## At a glance
7+
8+
```ts
9+
import {
10+
runAgent,
11+
DeepSeekProvider,
12+
ToolRegistry,
13+
SessionManager,
14+
BUILTIN_TOOLS,
15+
} from '@deepcode/core';
16+
17+
const provider = new DeepSeekProvider({ apiKey: process.env.DEEPSEEK_API_KEY! });
18+
const tools = new ToolRegistry(); // 6 P0 tools auto-registered
19+
const sessions = new SessionManager(); // ~/.deepcode/sessions/ by default
20+
const session = await sessions.create(process.cwd());
21+
22+
const result = await runAgent({
23+
provider,
24+
tools,
25+
systemPrompt: 'You are DeepCode. Help with code.',
26+
userMessage: 'List the TypeScript files in src/.',
27+
model: 'deepseek-chat',
28+
cwd: process.cwd(),
29+
session: { manager: sessions, id: session.id },
30+
enableSnapshots: true,
31+
onEvent: (e) => {
32+
if (e.type === 'text_delta') process.stdout.write(e.text);
33+
},
34+
});
35+
36+
console.log(`\n— ${result.turnsUsed} turns, ${result.usage.outputTokens} output tokens`);
37+
```
38+
39+
## Exports
40+
41+
### Providers
42+
43+
| Symbol | Purpose |
44+
| ------------------------------------ | ----------------------------------------------------------------------------------------- |
45+
| `DeepSeekProvider` | OpenAI-compatible streaming provider for DeepSeek (`api.deepseek.com/v1`). |
46+
| `DEEPSEEK_MODELS` | Per-model metadata: `ctx` (128k) + `maxOutput` (8192 hard limit). |
47+
| `EFFORT_PARAMS` | 5-tier effort → `{ maxTokens, temperature }` mapping. See `docs/design/effort-levels.md`. |
48+
| `Provider` | Interface — extend to add new LLM backends. |
49+
| `ProviderResult` / `ProviderRunOpts` | Provider contract types. |
50+
51+
`DeepSeekProvider` options:
52+
53+
```ts
54+
new DeepSeekProvider({
55+
apiKey: 'sk-...', // OR
56+
authToken: 'bearer-...', // Bearer alternative (§3.4 dual-header)
57+
baseURL: 'https://api.deepseek.com/v1', // default
58+
fetch: customFetch, // for tests
59+
});
60+
```
61+
62+
Streaming events flow through `ProviderStreamHandlers.onTextDelta` and `.onThinkingDelta`. The provider returns assembled `ContentBlock[]` (text / thinking / tool_use).
63+
64+
### Tools
65+
66+
Six P0 tools registered by default via `BUILTIN_TOOLS` and `ToolRegistry`:
67+
68+
| Tool | Input schema highlights |
69+
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
70+
| `ReadTool` | `file_path` (abs or cwd-relative) + optional `offset` / `limit`. Returns line-numbered content. |
71+
| `WriteTool` | `file_path` + `content`. Creates parent dirs. |
72+
| `EditTool` | `file_path` + `old_string` + `new_string` (+ `replace_all`). Fails on missing or non-unique `old_string` (unless `replace_all`). |
73+
| `BashTool` | `command` (+ `timeout`, `description`, `run_in_background` [M3.15.3 only]). Captures stdout/stderr/exitCode. |
74+
| `GrepTool` | `pattern` + optional `path` / `glob` / `type` / `output_mode` / `-i` / `-n` / `head_limit`. Uses ripgrep. |
75+
| `GlobTool` | `pattern` + optional `path` / `limit`. Built-in `fs.glob`. Sorts by mtime desc. |
76+
77+
Extend the registry:
78+
79+
```ts
80+
const tools = new ToolRegistry();
81+
tools.register(myCustomTool);
82+
```
83+
84+
### Sessions
85+
86+
```ts
87+
const sessions = new SessionManager({ root: '~/.deepcode/sessions' });
88+
89+
const meta = await sessions.create(cwd, { model: 'deepseek-chat', title: 'fix bug' });
90+
await sessions.append(meta.id, message);
91+
const loaded = await sessions.load(meta.id); // { meta, messages }
92+
const list = await sessions.list(); // sorted by updatedAt desc
93+
94+
// Snapshots (pre/post Edit-Write, drives §3.15.9 rewind)
95+
await sessions.snapshot({ sessionId: meta.id, cwd, filePath: 'a.ts', reason: 'pre-Edit', seq: 1 });
96+
const snaps = await sessions.snapshots(meta.id);
97+
await restoreSnapshot(snaps[0]!);
98+
```
99+
100+
Storage layout:
101+
102+
```
103+
<root>/<sessionId>.meta.json # meta JSON
104+
<root>/<sessionId>.jsonl # one StoredMessage per line
105+
<root>/<sessionId>/snapshots/ # blob files + manifest.jsonl
106+
```
107+
108+
### Agent loop
109+
110+
```ts
111+
const result = await runAgent({
112+
provider, tools, systemPrompt, userMessage,
113+
history: [], // resume from previous turns
114+
model: 'deepseek-chat',
115+
maxTokens: 4096,
116+
temperature: 0.4,
117+
maxTurns: 16, // safety cap
118+
cwd: process.cwd(),
119+
signal, // AbortSignal
120+
session: { manager, id },
121+
enableSnapshots: true,
122+
onEvent: (e) => { ... },
123+
});
124+
// result.stopReason: 'end_turn' | 'max_turns' | 'aborted' | 'error'
125+
// result.history: accumulated messages
126+
// result.turnsUsed: provider round-trips
127+
// result.usage: aggregate tokens
128+
```
129+
130+
`AgentEvent` discriminants: `text_delta` / `thinking_delta` / `tool_use` / `tool_result` / `turn_complete` / `usage` / `error`.
131+
132+
## Type re-exports
133+
134+
All of `types.ts` is re-exported. Highlights:
135+
136+
- `ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock`
137+
- `StoredMessage = { role, content, timestamp? }`
138+
- `ToolDefinition` / `ToolContext` / `ToolResult` / `ToolHandler`
139+
- `Mode` / `Effort` / `DeepSeekModel` / `HookEvent` / `HookHandlerType`
140+
141+
## What M1 does NOT include
142+
143+
Coming in later milestones (see `DEVELOPMENT_PLAN.md` §6):
144+
145+
| Feature | Milestone |
146+
| -------------------------------------------------------------- | --------- |
147+
| `--mode`, permissions matcher, trust dialog | M2 |
148+
| 30+ slash commands wiring | M2 |
149+
| settings.json three-layer config | M2 |
150+
| Hooks (9 events × 5 handlers), MCP, memory, compaction | M3 |
151+
| Sandbox subsystem (bwrap / sandbox-exec) | M3.5 |
152+
| Skills, sub-agents, output styles, effort levels full plumbing | M4 |
153+
| Plugin system + marketplace | M5 |
154+
| Mac desktop client + auto-update | M6 |
155+
| Right-side file panel + rewind UX | M7 |
156+
| Vim mode, voice input, headless `-p` | M8 |
157+
158+
## Tests
159+
160+
`pnpm --filter @deepcode/core test` — 62 tests pass, 4 skipped (ripgrep-dependent if not installed). Coverage:
161+
162+
- 6 tool handlers (read/write/edit/bash/grep/glob)
163+
- Sessions storage + snapshots roundtrip
164+
- DeepSeekProvider mocked-fetch streaming + tool calls + reasoning_content + message-shape conversion
165+
- Agent loop: end_turn / tool dispatch / unknown tool / maxTurns cap / abort signal / session persistence + snapshots / multi-turn history feeding
166+
167+
Run a single test file:
168+
169+
```bash
170+
pnpm --filter @deepcode/core test -- src/agent.test.ts
171+
```

docs/milestones/M1.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# M1 — Kernel MVP
2+
3+
> **Status**: ✅ complete
4+
> **Branch**: `feat/m1-kernel-mvp`
5+
6+
## Scope (planned)
7+
8+
> DEVELOPMENT_PLAN.md §6:
9+
> `@deepcode/core`: DeepSeekProvider + agent loop + 6 P0 tools + sessions(jsonl) + 文件快照(与 §3.15.9 rewind / §3.11 文件面板共底层)+ trust dialog 基础
10+
> Tests: 单测:deepseek-chat 改文件;DeepSeek tool-calling 兼容性 matrix;reasoner 流式 fixture
11+
> Docs: `docs/core-api.md`
12+
13+
## Delivered
14+
15+
| Module | Lines | Tests |
16+
| ----------------------- | ---------- | ------------------------------------------------------------------------------------------------ |
17+
| `providers/deepseek.ts` | 220 | 13 (streaming text / reasoning / tool calls / 5 message-shape conversions) |
18+
| `providers/types.ts` | 38 ||
19+
| `tools/read.ts` | 80 | 6 |
20+
| `tools/write.ts` | 56 | 5 |
21+
| `tools/edit.ts` | 105 | 6 |
22+
| `tools/bash.ts` | 102 | 5 (incl 1s timeout test) |
23+
| `tools/grep.ts` | 113 | 5 (auto-skip if `rg` missing) |
24+
| `tools/glob.ts` | 80 | 4 |
25+
| `tools/registry.ts` | 42 ||
26+
| `sessions/storage.ts` | 113 | 6 |
27+
| `sessions/snapshots.ts` | 96 | 5 |
28+
| `sessions/manager.ts` | 75 | (via agent.test) |
29+
| `agent.ts` | 195 | 7 (end_turn / tool dispatch / unknown / maxTurns / abort / session+snapshots / history feedback) |
30+
| **Total** | **~1,400** | **62 passed · 4 skipped** |
31+
32+
## Verification
33+
34+
```bash
35+
pnpm typecheck # green
36+
pnpm build # green
37+
pnpm test # 62 passed / 4 skipped / 0 failed
38+
```
39+
40+
The 4 skipped tests are `tools/grep.test.ts` cases that require `ripgrep` (rg) on PATH. CI (GitHub Actions Ubuntu) has it; local dev machines may not.
41+
42+
## NOT delivered (M1 spec said "trust dialog basics")
43+
44+
Trust dialog deferred to M2 — it needs the CLI/onboarding surface and `settings.json` integration to be useful, both of which are M2 scope. The kernel exposes session/snapshot primitives that the M2 trust dialog will use.
45+
46+
## Effort levels
47+
48+
`EFFORT_PARAMS` is exported with the design values from `docs/design/effort-levels.md` §3.2. **The numbers are not yet measured against real DeepSeek API** — that benchmark (`scripts/effort-bench.ts`) is deferred until a real `DEEPSEEK_API_KEY` is configured in CI secrets. The values stay within the documented 8,192 max_tokens hard limit (asserted in tests).
49+
50+
## Key design decisions made
51+
52+
1. **DeepSeek-internal `ContentBlock` types** (not Anthropic-shape) — `text / tool_use / tool_result / thinking`. Providers convert at the boundary. Avoids a hard `@anthropic-ai/sdk` dep.
53+
54+
2. **`anthropicShapeToOpenAI` boundary converter** — DeepCode-internal history → OpenAI chat-completions shape. Handles assistant tool_calls + role:"tool" results + thinking-block stripping.
55+
56+
3. **History is snapshotted into each provider call** (`messages: [...history]`) — prevents subsequent turns' mutations from changing what an earlier call "saw". Also makes provider-call replay deterministic.
57+
58+
4. **Snapshots live alongside sessions**`<sessionsRoot>/<sessionId>/snapshots/{NNNNN-ts-hash.blob, manifest.jsonl}`. Same storage that §3.15.9 rewind will use; not a separate subsystem.
59+
60+
5. **Bash `run_in_background` is a deliberate stub** — returns an error pointing to M3.15.3. Defers the entire background-task infrastructure to where its design doc (TaskCreate / Monitor / TaskOutput) lives.
61+
62+
6. **No `nock`/`msw` dep for provider tests** — OpenAI SDK accepts a `fetch` injection; tests pass a `mockFetch(chunks)` that returns SSE `data:` lines. Zero extra deps, full streaming coverage.
63+
64+
## Pivots & learnings
65+
66+
- **Initial `messages` type cast** — OpenAI SDK's generated types are stricter than the DeepSeek wire format actually requires. Cast at the API boundary (one place) rather than fight strict types throughout.
67+
- **`apiKey ?? authToken` was wrong** — empty string `''` isn't nullish so `??` doesn't fall through. Fixed to `||`. Test caught this immediately.
68+
- **History mutation bug**`messages: history` passed by reference, leading to a subtle test failure where the second provider call's recorded messages were "later" than the moment of the call. Fixed with `[...history]` snapshot.
69+
70+
## Next: M2
71+
72+
M2 adds the CLI: onboarding (with API key entry + Keychain + `apiKeyHelper`), REPL, 30+ slash commands, `settings.json` three-layer loader, `permissions` matcher (both glob syntaxes), trust dialog. The kernel exposes everything M2 needs.

0 commit comments

Comments
 (0)