tandem-mode 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -204
- package/README.sr.md +29 -0
- package/dist/agent/compact.js +80 -0
- package/dist/agent/compact.js.map +1 -0
- package/dist/agent/loop.js +12 -2
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/tools/gitDiff.js +37 -14
- package/dist/agent/tools/gitDiff.js.map +1 -1
- package/dist/agent/tools/shell.js +25 -7
- package/dist/agent/tools/shell.js.map +1 -1
- package/dist/benchmark/runner.js.map +1 -1
- package/dist/benchmark/tasks/{bitwiseBulkPriceWizard.js → bulkPriceWizard.js} +3 -3
- package/dist/benchmark/tasks/bulkPriceWizard.js.map +1 -0
- package/dist/cli.js +104 -7
- package/dist/cli.js.map +1 -1
- package/dist/config/profiles.js +108 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/i18n/en.js +41 -15
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/sr.js +41 -15
- package/dist/i18n/sr.js.map +1 -1
- package/dist/orchestrator/notify.js +38 -0
- package/dist/orchestrator/notify.js.map +1 -0
- package/dist/orchestrator/orchestrate.js +15 -0
- package/dist/orchestrator/orchestrate.js.map +1 -1
- package/dist/repl/commands.js +99 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/ui/status.js +37 -0
- package/dist/ui/status.js.map +1 -0
- package/dist/ui/toolFormat.js +89 -0
- package/dist/ui/toolFormat.js.map +1 -0
- package/package.json +49 -49
- package/dist/benchmark/tasks/bitwiseBulkPriceWizard.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,204 +1,249 @@
|
|
|
1
|
-
# Tandem Mode
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/tandem-mode)
|
|
4
|
-
[](./LICENSE)
|
|
5
|
-
[](./package.json)
|
|
6
|
-
[](https://github.com/zdrave08/Tandem-Mode/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
|
7
|
-
|
|
8
|
-
**English** | [Srpski](./README.sr.md)
|
|
9
|
-
|
|
10
|
-
A terminal coding agent built specifically for the DeepSeek API — not a generic
|
|
11
|
-
LLM harness with DeepSeek bolted on. Interactive REPL, planner/worker/reviewer
|
|
12
|
-
orchestration, cache-aware prompt architecture, and first-class support for
|
|
13
|
-
Windows, macOS, and Linux.
|
|
14
|
-
|
|
15
|
-
> **Independent open-source project. Not affiliated with or endorsed by
|
|
16
|
-
> DeepSeek.**
|
|
17
|
-
|
|
18
|
-
## Status
|
|
19
|
-
|
|
20
|
-
M0–M4 of the [development plan](./deepseek-cli-development-plan.md) are done.
|
|
21
|
-
Published on npm, usable today for real coding tasks against a local repo.
|
|
22
|
-
|
|
23
|
-
First benchmark run is in — see [full results](./docs/benchmark-results.md).
|
|
24
|
-
Headline: all three configurations (Pro-only, Flash-only, orchestration)
|
|
25
|
-
fixed 4/4 injected bugs correctly in a real WooCommerce plugin, with a clean
|
|
26
|
-
`php -l` and zero unnecessary file changes. But for this class of task
|
|
27
|
-
(single-file, well-scoped fixes), orchestration cost ~9.5× more and took
|
|
28
|
-
~19× longer than Pro-only for the same correctness — the opposite of what
|
|
29
|
-
you'd want orchestration for. Read as: orchestration's overhead needs a
|
|
30
|
-
bigger, more decomposable task to pay for itself, which this first run didn't
|
|
31
|
-
test. Reported as-is, not adjusted to fit expectations.
|
|
32
|
-
|
|
33
|
-
## Features
|
|
34
|
-
|
|
35
|
-
- **Agentic loop** — `deepseek-v4-pro`/`deepseek-v4-flash` with tool calling:
|
|
36
|
-
`read_file`, `list_dir`, `search`, `edit`, `git_diff`, `shell`, `view_image`,
|
|
37
|
-
`web_search`.
|
|
38
|
-
- **Two entry points** — interactive REPL for normal work, single-shot mode
|
|
39
|
-
(`tandem "task" --yes`) for scripting and CI.
|
|
40
|
-
- **Orchestration** (`/plan`) — a planner produces a JSON task list, workers
|
|
41
|
-
execute each task with a fresh minimal context, a reviewer checks the git
|
|
42
|
-
diff against the original task and either approves or requests corrections
|
|
43
|
-
(capped by `max_review_loops`).
|
|
44
|
-
- **Cache-aware prompts** — the worker's system prompt + repo map + plan stay
|
|
45
|
-
byte-identical across calls in one orchestration run, so DeepSeek's context
|
|
46
|
-
cache actually hits. Measured live: cache-hit tokens grew call over call
|
|
47
|
-
within a single `/plan` run.
|
|
48
|
-
- **Vision** — `view_image` lets the agent look at a screenshot you point it
|
|
49
|
-
to; `/paste` grabs an image straight from the clipboard (Windows, macOS,
|
|
50
|
-
and Linux via X11/`xclip` or Wayland/`wl-clipboard`).
|
|
51
|
-
- **Web search** — isolated calls to DeepSeek's Responses API when the agent
|
|
52
|
-
needs something outside the repo (an error message, a library's docs).
|
|
53
|
-
- **Session persistence** — every session is a JSONL file; `/resume` continues
|
|
54
|
-
the latest (or a named) one, `/fork` branches off without touching the
|
|
55
|
-
original.
|
|
56
|
-
- **Safety** — destructive actions (file edits, shell commands not on a small
|
|
57
|
-
read-only allowlist) always ask for confirmation; `y`/`n`/`a` where `a`
|
|
58
|
-
remembers the choice for that tool for the rest of the session. `--yes`
|
|
59
|
-
bypasses this for CI but always prints what it auto-approved.
|
|
60
|
-
- **Cost control** — usage is read only from the API's own `usage` field,
|
|
61
|
-
never estimated. A budget (`/budget`, `--budget`) hard-stops the loop before
|
|
62
|
-
the next API call if exceeded, without losing the session. `/usage` also
|
|
63
|
-
shows what the session would have cost off-peak, and the REPL warns you at
|
|
64
|
-
startup if you're currently in a peak-pricing window.
|
|
65
|
-
- **
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
119
|
-
| `--
|
|
120
|
-
| `--
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
|
132
|
-
|
|
133
|
-
| `/
|
|
134
|
-
| `/
|
|
135
|
-
| `/
|
|
136
|
-
| `/
|
|
137
|
-
| `/
|
|
138
|
-
| `/
|
|
139
|
-
| `/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1
|
+
# Tandem Mode
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/tandem-mode)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](./package.json)
|
|
6
|
+
[](https://github.com/zdrave08/Tandem-Mode/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
|
7
|
+
|
|
8
|
+
**English** | [Srpski](./README.sr.md)
|
|
9
|
+
|
|
10
|
+
A terminal coding agent built specifically for the DeepSeek API — not a generic
|
|
11
|
+
LLM harness with DeepSeek bolted on. Interactive REPL, planner/worker/reviewer
|
|
12
|
+
orchestration, cache-aware prompt architecture, and first-class support for
|
|
13
|
+
Windows, macOS, and Linux.
|
|
14
|
+
|
|
15
|
+
> **Independent open-source project. Not affiliated with or endorsed by
|
|
16
|
+
> DeepSeek.**
|
|
17
|
+
|
|
18
|
+
## Status
|
|
19
|
+
|
|
20
|
+
M0–M4 of the [development plan](./deepseek-cli-development-plan.md) are done.
|
|
21
|
+
Published on npm, usable today for real coding tasks against a local repo.
|
|
22
|
+
|
|
23
|
+
First benchmark run is in — see [full results](./docs/benchmark-results.md).
|
|
24
|
+
Headline: all three configurations (Pro-only, Flash-only, orchestration)
|
|
25
|
+
fixed 4/4 injected bugs correctly in a real WooCommerce plugin, with a clean
|
|
26
|
+
`php -l` and zero unnecessary file changes. But for this class of task
|
|
27
|
+
(single-file, well-scoped fixes), orchestration cost ~9.5× more and took
|
|
28
|
+
~19× longer than Pro-only for the same correctness — the opposite of what
|
|
29
|
+
you'd want orchestration for. Read as: orchestration's overhead needs a
|
|
30
|
+
bigger, more decomposable task to pay for itself, which this first run didn't
|
|
31
|
+
test. Reported as-is, not adjusted to fit expectations.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Agentic loop** — `deepseek-v4-pro`/`deepseek-v4-flash` with tool calling:
|
|
36
|
+
`read_file`, `list_dir`, `search`, `edit`, `git_diff`, `shell`, `view_image`,
|
|
37
|
+
`web_search`.
|
|
38
|
+
- **Two entry points** — interactive REPL for normal work, single-shot mode
|
|
39
|
+
(`tandem "task" --yes`) for scripting and CI.
|
|
40
|
+
- **Orchestration** (`/plan`) — a planner produces a JSON task list, workers
|
|
41
|
+
execute each task with a fresh minimal context, a reviewer checks the git
|
|
42
|
+
diff against the original task and either approves or requests corrections
|
|
43
|
+
(capped by `max_review_loops`).
|
|
44
|
+
- **Cache-aware prompts** — the worker's system prompt + repo map + plan stay
|
|
45
|
+
byte-identical across calls in one orchestration run, so DeepSeek's context
|
|
46
|
+
cache actually hits. Measured live: cache-hit tokens grew call over call
|
|
47
|
+
within a single `/plan` run.
|
|
48
|
+
- **Vision** — `view_image` lets the agent look at a screenshot you point it
|
|
49
|
+
to; `/paste` grabs an image straight from the clipboard (Windows, macOS,
|
|
50
|
+
and Linux via X11/`xclip` or Wayland/`wl-clipboard`).
|
|
51
|
+
- **Web search** — isolated calls to DeepSeek's Responses API when the agent
|
|
52
|
+
needs something outside the repo (an error message, a library's docs).
|
|
53
|
+
- **Session persistence** — every session is a JSONL file; `/resume` continues
|
|
54
|
+
the latest (or a named) one, `/fork` branches off without touching the
|
|
55
|
+
original.
|
|
56
|
+
- **Safety** — destructive actions (file edits, shell commands not on a small
|
|
57
|
+
read-only allowlist) always ask for confirmation; `y`/`n`/`a` where `a`
|
|
58
|
+
remembers the choice for that tool for the rest of the session. `--yes`
|
|
59
|
+
bypasses this for CI but always prints what it auto-approved.
|
|
60
|
+
- **Cost control** — usage is read only from the API's own `usage` field,
|
|
61
|
+
never estimated. A budget (`/budget`, `--budget`) hard-stops the loop before
|
|
62
|
+
the next API call if exceeded, without losing the session. `/usage` also
|
|
63
|
+
shows what the session would have cost off-peak, and the REPL warns you at
|
|
64
|
+
startup if you're currently in a peak-pricing window.
|
|
65
|
+
- **Status line** — the last line of every turn shows model, mode (chat/plan),
|
|
66
|
+
cost (and budget), and the current call out of the iteration limit; it turns
|
|
67
|
+
yellow, then red, as you approach the budget or the limit.
|
|
68
|
+
- **Readable tool output** — `edit` shows a red/green diff, `read_file`, `search`
|
|
69
|
+
and `list_dir` collapse to a one-line summary (`120 lines read`, `no matches`).
|
|
70
|
+
- **ESC to interrupt** — cancels the in-flight call (and any running shell
|
|
71
|
+
command) cleanly, keeping whatever was generated so far.
|
|
72
|
+
- **Cross-platform** — the shell tool runs PowerShell natively on Windows and
|
|
73
|
+
`sh` on macOS/Linux, file edits preserve the original CRLF/LF line ending
|
|
74
|
+
style, paths are handled with `path.resolve` throughout.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install -g tandem-mode
|
|
80
|
+
tandem # interactive REPL
|
|
81
|
+
tandem "explain what this repo does" --yes # one-shot
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or from source (useful for contributing, or to track `main` instead of the
|
|
85
|
+
latest npm release):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/zdrave08/Tandem-Mode.git
|
|
89
|
+
cd Tandem-Mode
|
|
90
|
+
pnpm install
|
|
91
|
+
pnpm dev # interactive REPL
|
|
92
|
+
pnpm dev "explain what this repo does" --yes # one-shot
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
On first run you'll be asked for a DeepSeek API key (get one at
|
|
96
|
+
[platform.deepseek.com](https://platform.deepseek.com)) and a default
|
|
97
|
+
model/reasoning effort. The key is stored in your OS's own secure credential
|
|
98
|
+
store — never in a config file or log:
|
|
99
|
+
|
|
100
|
+
- **Windows** — DPAPI-encrypted (tied to your OS user account) in
|
|
101
|
+
`%LOCALAPPDATA%\TandemMode\credentials.dat`.
|
|
102
|
+
- **macOS** — the login Keychain, via the built-in `security` CLI.
|
|
103
|
+
- **Linux** — the Secret Service (gnome-keyring, KWallet, or similar), via
|
|
104
|
+
`secret-tool` (install `libsecret-tools` if it's missing).
|
|
105
|
+
|
|
106
|
+
`DEEPSEEK_API_KEY` works everywhere and always takes priority, for CI use.
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
tandem # interactive REPL
|
|
112
|
+
tandem "fix the off-by-one in foo.ts"
|
|
113
|
+
tandem --plan "add dark mode to the settings page"
|
|
114
|
+
tandem "task" --model deepseek-v4-flash --effort low --budget 0.50 --yes
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Flag | Effect |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `--resume` / `-r` | Continue the latest session in this directory |
|
|
120
|
+
| `--yes` / `-y` | Auto-approve destructive actions (prints what it approved) |
|
|
121
|
+
| `--plan` | Run planner/worker/reviewer orchestration instead of a single turn |
|
|
122
|
+
| `--model` | `deepseek-v4-pro` or `deepseek-v4-flash` |
|
|
123
|
+
| `--effort` | `low`, `high`, or `max` reasoning effort |
|
|
124
|
+
| `--budget` | Hard USD cap for the run |
|
|
125
|
+
| `--max-review-loops` | Cap on orchestration correction cycles (default 3) |
|
|
126
|
+
| `--max-iterations` | Model calls per chunk of a turn (default 25); at the limit the agent asks whether to continue |
|
|
127
|
+
| `--auto-continue` | Continue at the iteration limit without asking — only together with a budget (`--budget`) |
|
|
128
|
+
|
|
129
|
+
### REPL commands
|
|
130
|
+
|
|
131
|
+
| Command | Effect |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `/plan <task>` | Planner/worker/reviewer orchestration |
|
|
134
|
+
| `/model [name]` | Show or switch model |
|
|
135
|
+
| `/thinking [on\|off]` | Toggle thinking mode |
|
|
136
|
+
| `/effort [low\|high\|max]` | Show or set reasoning effort |
|
|
137
|
+
| `/status` | Current session/settings |
|
|
138
|
+
| `/diff [--staged] [path]` | Working-tree diff, no API call |
|
|
139
|
+
| `/usage` | Tokens, cost, off-peak projection |
|
|
140
|
+
| `/budget [amount]` | Show or set the session budget |
|
|
141
|
+
| `/new` | Start a fresh session |
|
|
142
|
+
| `/save-profile <name>` | Save model, effort, budget and review loops as a named profile |
|
|
143
|
+
| `/profile [name]` | Load a saved profile, or list them |
|
|
144
|
+
| `/compact` | Summarize the history into a new session to free up context; the old session stays on disk |
|
|
145
|
+
| `/fork` | New session, carrying over the current history |
|
|
146
|
+
| `/resume [id]` | Continue the latest (or a named) session |
|
|
147
|
+
| `/paste [message]` | Send a clipboard image to the agent |
|
|
148
|
+
| `/clear` | Clear the screen |
|
|
149
|
+
| `/help` | This list |
|
|
150
|
+
| `/exit` | Quit |
|
|
151
|
+
|
|
152
|
+
Press **ESC** at any time during a turn to interrupt it.
|
|
153
|
+
|
|
154
|
+
## Architecture
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
tandem (no args) ─┬─> interactive REPL ─┬─> single turn: agent loop (tools)
|
|
158
|
+
│ └─> /plan: planner → worker(s) → reviewer
|
|
159
|
+
tandem "task" ──┘ (single-shot, same agent loop, for scripts/CI)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `src/deepseek/` — thin client for DeepSeek's OpenAI-compatible API
|
|
163
|
+
(streaming + non-streaming chat completions, vision, web search via
|
|
164
|
+
Responses API). See [`docs/api-notes.en.md`](./docs/api-notes.en.md)
|
|
165
|
+
(Serbian original: [`docs/api-notes.md`](./docs/api-notes.md)) for the
|
|
166
|
+
API behavior this is built against, verified live rather than assumed.
|
|
167
|
+
- `src/agent/` — the tool-calling loop, tool implementations, session
|
|
168
|
+
persistence, usage/cost tracking.
|
|
169
|
+
- `src/orchestrator/` — planner/worker/reviewer, repo-map generation, the
|
|
170
|
+
cache-aware stable-prefix construction.
|
|
171
|
+
- `src/repl/` — slash commands, runtime config, the first-run wizard.
|
|
172
|
+
- `src/cli.tsx` — Ink-based terminal UI and the REPL/single-shot entry points.
|
|
173
|
+
|
|
174
|
+
## Configuration
|
|
175
|
+
|
|
176
|
+
Layered: **session** (in-REPL commands) > **project** (`.tandem/config.json`
|
|
177
|
+
in the current directory) > **global** (`~/.tandem/config.json`).
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"defaultModel": "deepseek-v4-pro",
|
|
182
|
+
"defaultReasoningEffort": "high",
|
|
183
|
+
"budgetUsd": 5,
|
|
184
|
+
"maxReviewLoops": 3,
|
|
185
|
+
"maxIterations": 25,
|
|
186
|
+
"autoContinue": false,
|
|
187
|
+
"verifyCommand": "pnpm typecheck && pnpm test",
|
|
188
|
+
"notifyCommand": "curl -s -d \"Tandem: $TANDEM_OUTCOME\" https://ntfy.sh/my-topic",
|
|
189
|
+
"language": "en"
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`maxIterations` is how many model calls one turn may make before Tandem stops and
|
|
194
|
+
asks "continue for N more calls? (y/n/a)"; `a` remembers the answer for the rest of
|
|
195
|
+
the session. With `autoContinue` (or `--auto-continue`) it continues on its own, but
|
|
196
|
+
only when a budget is set, because the budget is the hard stop before the next API
|
|
197
|
+
call. In non-interactive runs (`--yes`) without `--auto-continue` it stops instead of
|
|
198
|
+
waiting for an answer, and the session is saved so you can send "continue".
|
|
199
|
+
|
|
200
|
+
`verifyCommand` runs before each `/plan` review round and its result goes to
|
|
201
|
+
the reviewer. `language` is the UI language (`en` or `sr`, default `en`); the
|
|
202
|
+
`--lang` flag and the `TANDEM_LANG` environment variable override it, in that
|
|
203
|
+
order. The first-run wizard asks for it.
|
|
204
|
+
|
|
205
|
+
`notifyCommand` runs when a `/plan` run ends: reviewer approved, `max_review_loops`
|
|
206
|
+
reached, or an error (not when you interrupt it with ESC). The details come in as
|
|
207
|
+
environment variables, never spliced into the command text: `TANDEM_OUTCOME`
|
|
208
|
+
(`approved`, `max_review_loops` or `error`), `TANDEM_TASK`, `TANDEM_NOTES` (the
|
|
209
|
+
reviewer's notes, or the error message), `TANDEM_REVIEW_LOOPS` and `TANDEM_COST_USD`.
|
|
210
|
+
The command runs in your shell (PowerShell on Windows, `sh` elsewhere) with a 15 s
|
|
211
|
+
limit, and a failure only prints a warning. Any shell command works, so Slack,
|
|
212
|
+
ntfy, a desktop notification or a webhook are all just examples:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{ "notifyCommand": "curl -s -d \"Tandem: $TANDEM_OUTCOME\" https://ntfy.sh/my-topic" }
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
On Windows (PowerShell) the variables are written `$env:TANDEM_OUTCOME`.
|
|
219
|
+
|
|
220
|
+
Like `verifyCommand`, `notifyCommand` runs without asking, so only put it in a
|
|
221
|
+
project's `.tandem/config.json` if you trust that project.
|
|
222
|
+
|
|
223
|
+
**Profiles:** `/save-profile <name>` stores the current model, reasoning effort,
|
|
224
|
+
budget and review loops in `~/.tandem/profiles/<name>.json`, and `/profile <name>`
|
|
225
|
+
loads them back into the running session (only the fields present in the file are
|
|
226
|
+
applied). `/profile` alone lists the saved ones. Names use `a-z`, `0-9`, `-` and `_`.
|
|
227
|
+
Profiles never include `verifyCommand` or the API key.
|
|
228
|
+
|
|
229
|
+
The API key is never stored in these files — it lives in the OS credential
|
|
230
|
+
store (or `DEEPSEEK_API_KEY` env var, which takes priority, for CI use).
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
pnpm install
|
|
238
|
+
pnpm typecheck
|
|
239
|
+
pnpm dev "some task"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Disclaimer
|
|
243
|
+
|
|
244
|
+
This is an independent, community-run project. It is not affiliated with,
|
|
245
|
+
endorsed by, or sponsored by DeepSeek.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
[MIT](./LICENSE)
|
package/README.sr.md
CHANGED
|
@@ -130,9 +130,13 @@ tandem "task" --model deepseek-v4-flash --effort low --budget 0.50 --yes
|
|
|
130
130
|
| `/thinking [on\|off]` | Uključi/isključi thinking mode |
|
|
131
131
|
| `/effort [low\|high\|max]` | Prikaži ili postavi reasoning effort |
|
|
132
132
|
| `/status` | Trenutna sesija/podešavanja |
|
|
133
|
+
| `/diff [--staged] [putanja]` | Diff radnog stabla, bez API poziva |
|
|
133
134
|
| `/usage` | Tokeni, cena, off-peak projekcija |
|
|
134
135
|
| `/budget [iznos]` | Prikaži ili postavi budžet sesije |
|
|
135
136
|
| `/new` | Nova sesija |
|
|
137
|
+
| `/save-profile <ime>` | Sačuvaj model, effort, budžet i review krugove kao imenovan profil |
|
|
138
|
+
| `/profile [ime]` | Učitaj sačuvan profil, ili prikaži listu |
|
|
139
|
+
| `/compact` | Sažmi istoriju u novu sesiju da se oslobodi kontekst; stara sesija ostaje na disku |
|
|
136
140
|
| `/fork` | Nova sesija sa kopijom trenutne istorije |
|
|
137
141
|
| `/resume [id]` | Nastavi poslednju (ili navedenu) sesiju |
|
|
138
142
|
| `/paste [poruka]` | Pošalji sliku iz clipboard-a agentu |
|
|
@@ -175,6 +179,7 @@ Slojevito: **sesija** (komande u REPL-u) > **projekat**
|
|
|
175
179
|
"budgetUsd": 5,
|
|
176
180
|
"maxReviewLoops": 3,
|
|
177
181
|
"verifyCommand": "pnpm typecheck && pnpm test",
|
|
182
|
+
"notifyCommand": "curl -s -d \"Tandem: $TANDEM_OUTCOME\" https://ntfy.sh/my-topic",
|
|
178
183
|
"language": "sr"
|
|
179
184
|
}
|
|
180
185
|
```
|
|
@@ -184,6 +189,30 @@ ide reviewer-u. `language` je jezik interfejsa (`en` ili `sr`, podrazumevano
|
|
|
184
189
|
`en`); `--lang` flag i `TANDEM_LANG` env promenljiva ga nadjačavaju, tim
|
|
185
190
|
redom. First-run wizard ga pita.
|
|
186
191
|
|
|
192
|
+
`notifyCommand` se pokreće kad se `/plan` završi: reviewer odobrio, dostignut
|
|
193
|
+
`max_review_loops`, ili greška (ne i kad ga prekineš sa ESC). Detalji dolaze kao
|
|
194
|
+
promenljive okruženja, nikad ubačeni u tekst komande: `TANDEM_OUTCOME`
|
|
195
|
+
(`approved`, `max_review_loops` ili `error`), `TANDEM_TASK`, `TANDEM_NOTES`
|
|
196
|
+
(beleške reviewer-a, ili poruka greške), `TANDEM_REVIEW_LOOPS` i `TANDEM_COST_USD`.
|
|
197
|
+
Komanda radi u tvojoj školjki (PowerShell na Windows-u, `sh` drugde) sa limitom
|
|
198
|
+
od 15 s, a neuspeh samo ispiše upozorenje. Može bilo koja shell komanda, pa su
|
|
199
|
+
Slack, ntfy, desktop obaveštenje ili webhook samo primeri:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{ "notifyCommand": "curl -s -d \"Tandem: $TANDEM_OUTCOME\" https://ntfy.sh/my-topic" }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Na Windows-u (PowerShell) promenljive se pišu `$env:TANDEM_OUTCOME`.
|
|
206
|
+
|
|
207
|
+
Kao i `verifyCommand`, `notifyCommand` se pokreće bez pitanja, pa ga stavljaj u
|
|
208
|
+
`.tandem/config.json` projekta samo ako tom projektu veruješ.
|
|
209
|
+
|
|
210
|
+
**Profili:** `/save-profile <ime>` čuva trenutni model, reasoning effort, budžet i
|
|
211
|
+
review krugove u `~/.tandem/profiles/<ime>.json`, a `/profile <ime>` ih vraća u
|
|
212
|
+
sesiju koja radi (primenjuju se samo polja koja postoje u fajlu). `/profile` bez
|
|
213
|
+
argumenta prikazuje sačuvane. Imena koriste `a-z`, `0-9`, `-` i `_`.
|
|
214
|
+
Profili nikad ne sadrže `verifyCommand` ni API ključ.
|
|
215
|
+
|
|
187
216
|
API ključ se nikad ne čuva u ovim fajlovima — živi u OS-ovom skladištu
|
|
188
217
|
kredencijala (ili u `DEEPSEEK_API_KEY` env promenljivoj, koja ima
|
|
189
218
|
prioritet, za CI upotrebu).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { chatCompletionOnce } from "../deepseek/client.js";
|
|
2
|
+
// Tool output is where long sessions grow (file dumps, shell output), so it is
|
|
3
|
+
// cut hard in the transcript the summarizer sees. The summary only needs the gist.
|
|
4
|
+
const MAX_TOOL_OUTPUT_CHARS = 1_500;
|
|
5
|
+
const MAX_TOOL_ARGS_CHARS = 300;
|
|
6
|
+
const MAX_TRANSCRIPT_CHARS = 300_000;
|
|
7
|
+
export const SUMMARY_MODEL = "deepseek-v4-flash";
|
|
8
|
+
const SUMMARIZER_SYSTEM_PROMPT = `You compact the history of a coding-agent session so work can continue from your
|
|
9
|
+
summary alone. Write a dense summary with these parts, skipping any that are empty:
|
|
10
|
+
- Goal: what the user is trying to achieve, plus constraints and preferences they stated.
|
|
11
|
+
- Done: files read or changed (exact paths), commands run and their results.
|
|
12
|
+
- Decisions: what was decided and why.
|
|
13
|
+
- State: where things stand right now, including failing tests or errors still open.
|
|
14
|
+
- Next: what was about to happen or is still to do.
|
|
15
|
+
Keep exact file paths, identifiers and error messages that matter. Do not invent anything
|
|
16
|
+
that is not in the conversation. No preamble. Reply in the language the user wrote in.`;
|
|
17
|
+
export const SUMMARY_PREFIX = "Summary of our conversation so far (the earlier messages were compacted to save context):";
|
|
18
|
+
const SUMMARY_ACK = "Understood. I'll continue from this summary.";
|
|
19
|
+
function truncate(text, max) {
|
|
20
|
+
return text.length > max ? `${text.slice(0, max)} … (truncated, ${text.length} chars)` : text;
|
|
21
|
+
}
|
|
22
|
+
/** Plain-text rendering of the history. Text avoids tool_call/tool pairing rules in the summarizer request. */
|
|
23
|
+
export function buildTranscript(messages) {
|
|
24
|
+
const toolNames = new Map();
|
|
25
|
+
for (const m of messages) {
|
|
26
|
+
for (const call of m.toolCalls ?? [])
|
|
27
|
+
toolNames.set(call.id, call.function.name);
|
|
28
|
+
}
|
|
29
|
+
const parts = [];
|
|
30
|
+
for (const m of messages) {
|
|
31
|
+
if (m.role === "system")
|
|
32
|
+
continue;
|
|
33
|
+
if (m.role === "user") {
|
|
34
|
+
parts.push(`USER:\n${m.content ?? ""}`);
|
|
35
|
+
}
|
|
36
|
+
else if (m.role === "assistant") {
|
|
37
|
+
const calls = (m.toolCalls ?? [])
|
|
38
|
+
.map((c) => `[called ${c.function.name}(${truncate(c.function.arguments, MAX_TOOL_ARGS_CHARS)})]`)
|
|
39
|
+
.join("\n");
|
|
40
|
+
const text = [m.content ?? "", calls].filter(Boolean).join("\n");
|
|
41
|
+
if (text)
|
|
42
|
+
parts.push(`ASSISTANT:\n${text}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const name = (m.toolCallId && toolNames.get(m.toolCallId)) || "tool";
|
|
46
|
+
parts.push(`TOOL RESULT (${name}):\n${truncate(m.content ?? "", MAX_TOOL_OUTPUT_CHARS)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const transcript = parts.join("\n\n");
|
|
50
|
+
if (transcript.length <= MAX_TRANSCRIPT_CHARS)
|
|
51
|
+
return transcript;
|
|
52
|
+
// Too long even after trimming tool output: keep the most recent part.
|
|
53
|
+
return `[earlier part of the conversation omitted]\n\n${transcript.slice(-MAX_TRANSCRIPT_CHARS)}`;
|
|
54
|
+
}
|
|
55
|
+
/** One model call. Throws on API errors or an empty summary; the caller's history is never touched here. */
|
|
56
|
+
export async function compactConversation(env, messages, systemPrompt, signal) {
|
|
57
|
+
const response = await chatCompletionOnce(env, {
|
|
58
|
+
model: SUMMARY_MODEL,
|
|
59
|
+
messages: [
|
|
60
|
+
{ role: "system", content: SUMMARIZER_SYSTEM_PROMPT },
|
|
61
|
+
{ role: "user", content: `Conversation to summarize:\n\n${buildTranscript(messages)}` },
|
|
62
|
+
],
|
|
63
|
+
...(signal ? { signal } : {}),
|
|
64
|
+
});
|
|
65
|
+
const summary = response.content.trim();
|
|
66
|
+
if (!summary)
|
|
67
|
+
throw new Error("The model returned an empty summary.");
|
|
68
|
+
// The acknowledgement keeps user/assistant alternation, so the next user turn
|
|
69
|
+
// never follows another user message.
|
|
70
|
+
return {
|
|
71
|
+
messages: [
|
|
72
|
+
{ role: "system", content: systemPrompt },
|
|
73
|
+
{ role: "user", content: `${SUMMARY_PREFIX}\n\n${summary}` },
|
|
74
|
+
{ role: "assistant", content: SUMMARY_ACK },
|
|
75
|
+
],
|
|
76
|
+
summary,
|
|
77
|
+
usage: response.usage,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=compact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact.js","sourceRoot":"","sources":["../../src/agent/compact.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -17,7 +17,17 @@ const MAX_REASONING_CHARS = 200_000;
|
|
|
17
17
|
export async function* runAgentLoop(messages, opts) {
|
|
18
18
|
const ctx = { cwd: opts.cwd, env: opts.env, usage: opts.usage, ...(opts.signal ? { signal: opts.signal } : {}) };
|
|
19
19
|
const maxIterations = opts.maxIterations ?? 25;
|
|
20
|
-
|
|
20
|
+
// Granica se pomera za još jedan "komad" svaki put kad korisnik (ili autoContinue) dozvoli nastavak.
|
|
21
|
+
let limit = maxIterations;
|
|
22
|
+
for (let iteration = 0;; iteration++) {
|
|
23
|
+
if (iteration >= limit) {
|
|
24
|
+
const proceed = opts.onIterationLimit ? await opts.onIterationLimit({ iterations: iteration }) : false;
|
|
25
|
+
if (!proceed) {
|
|
26
|
+
yield { type: "max_iterations_reached" };
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
limit += maxIterations;
|
|
30
|
+
}
|
|
21
31
|
if (opts.signal?.aborted) {
|
|
22
32
|
yield { type: "interrupted" };
|
|
23
33
|
return;
|
|
@@ -29,6 +39,7 @@ export async function* runAgentLoop(messages, opts) {
|
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
31
41
|
}
|
|
42
|
+
yield { type: "iteration", n: iteration + 1, max: limit };
|
|
32
43
|
let reasoning = "";
|
|
33
44
|
let content = "";
|
|
34
45
|
const toolCallBuf = new Map();
|
|
@@ -159,6 +170,5 @@ export async function* runAgentLoop(messages, opts) {
|
|
|
159
170
|
await appendSessionMessage(opts.session, toolMsg);
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
|
-
yield { type: "max_iterations_reached" };
|
|
163
173
|
}
|
|
164
174
|
//# sourceMappingURL=loop.js.map
|