agen-vektor 0.3.8 → 0.3.9
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 +149 -14
- package/dist/agent/agent.js +19 -1
- package/dist/agent/light.js +65 -0
- package/dist/agent/loop.js +107 -6
- package/dist/agent/planner.js +6 -0
- package/dist/agent/prompts.js +39 -1
- package/dist/agent/rules.js +182 -0
- package/dist/agent/skills.js +684 -0
- package/dist/cli/index.js +52 -20
- package/dist/config/config.js +12 -0
- package/dist/config/credentials.js +23 -0
- package/dist/config/models.js +24 -2
- package/dist/config/providers.js +149 -0
- package/dist/providers/custom.js +4 -2
- package/dist/providers/factory.js +44 -14
- package/dist/providers/openai-compat.js +76 -9
- package/dist/tools/apply-patch.js +264 -0
- package/dist/tools/extras.js +386 -0
- package/dist/tools/filesystem.js +14 -0
- package/dist/tools/skills-tool.js +97 -0
- package/dist/tools/web.js +196 -23
- package/dist/tui/app.js +1715 -163
- package/dist/tui/chat.js +1071 -46
- package/dist/tui/commands.js +97 -0
- package/dist/tui/components.js +13 -5
- package/dist/tui/statusbar.js +33 -17
- package/dist/tui/suggest.js +63 -0
- package/dist/tui/theme.js +66 -23
- package/dist/tui/themes.js +428 -0
- package/dist/utils/paths.js +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,33 +12,42 @@
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
VectorHead ● Ready
|
|
16
|
-
Project: ~/my-project Mode: ask
|
|
15
|
+
VectorHead ● Ready deepseek-ai/DeepSeek-V4-Flash
|
|
17
16
|
───────────────────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
[09:41]
|
|
18
|
+
Perbaiki error authentication pada project ini
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
# Cek Auth ← kuning (heading H1–H6, Freebuff palette)
|
|
21
|
+
VectorHead akan memeriksa file auth, memperbaiki, lalu menjalankan test.
|
|
22
|
+
[09:42]
|
|
23
|
+
$ npm test
|
|
24
|
+
... (2 more lines above)
|
|
25
|
+
3 tests passed
|
|
24
26
|
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
27
|
+
│ │
|
|
25
28
|
│ ▍ Enter a coding task or / for commands │
|
|
29
|
+
│ │
|
|
26
30
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
27
31
|
● Ready ● ask
|
|
28
32
|
```
|
|
29
33
|
|
|
30
|
-
A Freebuff-style terminal UI
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
A Freebuff-style terminal UI with the EXACT Freebuff dark palette (`#9EFC62`
|
|
35
|
+
green accents, markdown headings H1–H6 all yellow `#facc15`, inline code
|
|
36
|
+
BOLD orange `#FF8534` on a `#374151` chip, links blue `#3B82F6`, fenced
|
|
37
|
+
code `// lang` header `#5b647a` on a `#374151` slab, plain prose — quoted
|
|
38
|
+
from [CodebuffAI/freebuff](https://github.com/CodebuffAI/freebuff)
|
|
39
|
+
theme-system.ts + markdown-renderer.tsx), a compact agent-activity strip
|
|
40
|
+
(⚙ tool calls), and a rounded input box with a block cursor — all rendered
|
|
41
|
+
incrementally with no flicker.
|
|
33
42
|
|
|
34
43
|
## Features
|
|
35
44
|
|
|
36
|
-
- **Interactive TUI** — chat, agent status, tool execution, diff viewer, input. Keyboard driven, mouse-aware, works on 80×24 terminals and survives resize.
|
|
45
|
+
- **Interactive TUI** — chat, agent status, tool execution, diff viewer, input. Keyboard driven, mouse-aware, works on 80×24 terminals and survives resize. Scroll the chat with PgUp/PgDn, the mouse wheel, or ↑/↓ while the input line is empty.
|
|
37
46
|
- **Agent loop** — plan → inspect → tool call → result → review → fix → retry until done (with an iteration cap).
|
|
38
47
|
- **Tools** — read/write/edit files, list directories, search files, run shell commands, git, fetch web pages.
|
|
39
48
|
- **Security first** — command policy classifies every shell command (SAFE / ASK / DANGEROUS / BLOCKED), permission prompts before risky actions, `--yolo` mode with warnings, and API keys are never printed or persisted to sessions.
|
|
40
49
|
- **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint.
|
|
41
|
-
- **Sessions & memory** — conversations persist to `~/.vector/sessions/`; `/continue` resumes
|
|
50
|
+
- **Sessions & memory** — conversations persist to `~/.vector/sessions/`; a relaunch in the same project resumes the conversation automatically (agent keeps the last replies in context), `/continue` resumes any session, and `Ctrl+L`/`/clear` starts fresh.
|
|
42
51
|
- **Context management** — only relevant context is sent to the model, with compaction (`/compact`).
|
|
43
52
|
|
|
44
53
|
## Installation
|
|
@@ -100,6 +109,7 @@ vector
|
|
|
100
109
|
| `vector "task"` | Start TUI and run the task immediately |
|
|
101
110
|
| `vector --model MODEL` | Set the model |
|
|
102
111
|
| `vector --provider P` | Set provider: `openai`, `anthropic`, `gemini`, `openrouter`, `ollama`, `custom` |
|
|
112
|
+
| `vector --theme NAME` | Set theme: built-in or `~/.vector/themes/<name>.json` |
|
|
103
113
|
| `vector --continue` | Continue the most recent session |
|
|
104
114
|
| `vector --session NAME` | Use/resume session `NAME` |
|
|
105
115
|
| `vector --plan` | Show a plan before executing |
|
|
@@ -113,11 +123,14 @@ Type `/` inside VectorHead:
|
|
|
113
123
|
| Command | Description |
|
|
114
124
|
|---|---|
|
|
115
125
|
| `/model` | Select a model |
|
|
126
|
+
| `/theme` | Select a theme (OpenCode-style) |
|
|
116
127
|
| `/provider` | Select a provider |
|
|
128
|
+
| `/provider-remove` | Remove the stored provider key (and custom URL/name) so you can re-enter fresh values |
|
|
117
129
|
| `/session` | Browse saved sessions |
|
|
118
130
|
| `/settings` | Show configuration |
|
|
119
131
|
| `/status` | Show agent status |
|
|
120
|
-
| `/clear` | Clear the conversation |
|
|
132
|
+
| `/clear` | Clear the conversation (keeps the session) |
|
|
133
|
+
| `/new` | Start a fresh conversation — rotates to a brand-new session (the old one is kept on disk) |
|
|
121
134
|
| `/continue` | Continue the current/last session |
|
|
122
135
|
| `/diff` | Show diff viewer |
|
|
123
136
|
| `/git` | Run `git status` |
|
|
@@ -138,7 +151,7 @@ export OPENROUTER_API_KEY=...
|
|
|
138
151
|
|
|
139
152
|
Or inside VectorHead: `/provider` → pick provider → paste key (stored masked in `~/.vector/credentials.json`, permissions `0600`).
|
|
140
153
|
|
|
141
|
-
Generic variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_HOME
|
|
154
|
+
Generic variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_THEME`, `VECTOR_HOME`, `VECTOR_AUTO_APPROVE` (=1 auto-approves ASK-level permission prompts, for automated runs/tests; dangerous actions still prompt).
|
|
142
155
|
|
|
143
156
|
### Custom API (OpenAI-compatible)
|
|
144
157
|
|
|
@@ -150,6 +163,128 @@ vector --provider custom --model deepseek-v3
|
|
|
150
163
|
|
|
151
164
|
The provider speaks `/v1/chat/completions` (and `/models`). Anthropic and Gemini protocols for custom endpoints are documented as adapters; OpenAI-compatible endpoints are fully supported.
|
|
152
165
|
|
|
166
|
+
### Multiple custom providers (OpenCode-style)
|
|
167
|
+
|
|
168
|
+
Any number of OpenAI-compatible providers can be defined side by side in
|
|
169
|
+
`~/.vector/config.json` under `providers` — the same shape OpenCode uses in
|
|
170
|
+
its `provider` section (minus the `npm` field, since every custom endpoint
|
|
171
|
+
here is OpenAI-compatible):
|
|
172
|
+
|
|
173
|
+
```jsonc
|
|
174
|
+
{
|
|
175
|
+
"provider": "bitdeer", // ACTIVE provider id
|
|
176
|
+
"model": "deepseek-ai/DeepSeek-V4-Flash",
|
|
177
|
+
"providers": {
|
|
178
|
+
"bitdeer": {
|
|
179
|
+
"name": "BitDeer",
|
|
180
|
+
"options": {
|
|
181
|
+
"baseURL": "https://api-inference.bitdeer.ai/v1",
|
|
182
|
+
"apiKey": "{env:BITDEER_API_KEY}" // or a plain key, or omit and store via /provider
|
|
183
|
+
},
|
|
184
|
+
"models": {
|
|
185
|
+
"deepseek-ai/DeepSeek-V4-Flash": { "name": "DeepSeek V4 Flash" }
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"groq2": {
|
|
189
|
+
"name": "Groq 2",
|
|
190
|
+
"options": { "baseURL": "https://groq2.example.com/v1" }
|
|
191
|
+
},
|
|
192
|
+
"agentrouter": {
|
|
193
|
+
"name": "AgentRouter",
|
|
194
|
+
"model": "deepseek-v4-flash", // default model when this provider is active
|
|
195
|
+
"options": {
|
|
196
|
+
"baseURL": "https://agentrouter.org/v1",
|
|
197
|
+
// Gateways that filter by client (AgentRouter only accepts official
|
|
198
|
+
// AI clients) need a spoofed User-Agent on every request:
|
|
199
|
+
"headers": {
|
|
200
|
+
"User-Agent": "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464",
|
|
201
|
+
"Originator": "codex_cli_rs"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- `provider` is the active id — a built-in, the legacy `custom` slot, or any
|
|
210
|
+
key of `providers`. Keys are **per provider id**: store each under its own
|
|
211
|
+
id in `~/.vector/credentials.json` (0600) via `/provider`, inline them in
|
|
212
|
+
`options.apiKey`, or reference an env var with `"{env:NAME}"`.
|
|
213
|
+
- `options.headers` adds extra HTTP headers to every request of that provider
|
|
214
|
+
(e.g. a `User-Agent` required by a client-filtering gateway like
|
|
215
|
+
AgentRouter). Values may be plain strings or `"{env:NAME}"` templates;
|
|
216
|
+
headers referencing a missing env var are dropped.
|
|
217
|
+
- `model` is the **default model** for that provider: when the provider
|
|
218
|
+
becomes active (`/provider`, `--provider`, or `VECTOR_PROVIDER`), the active
|
|
219
|
+
model automatically follows it unless an explicit `--model` / `VECTOR_MODEL`
|
|
220
|
+
was given. Handy when each provider speaks its own model ids (e.g.
|
|
221
|
+
AgentRouter uses `deepseek-v4-flash` while BitDeer uses
|
|
222
|
+
`deepseek-ai/DeepSeek-V4-Flash`).
|
|
223
|
+
- `/provider` lists built-ins + every configured custom and offers
|
|
224
|
+
**➕ Add custom provider** (id → name → base URL → key). `/model` then
|
|
225
|
+
auto-discovers models from the endpoint's `/models` route and falls back
|
|
226
|
+
to the `models` map above.
|
|
227
|
+
- The old single-custom setup (`provider: "custom"` + `apiUrl`/`customName`
|
|
228
|
+
+ key under `custom`) keeps working unchanged.
|
|
229
|
+
|
|
230
|
+
Switch at runtime with `/provider`, or per-run with
|
|
231
|
+
`VECTOR_PROVIDER=bitdeer vector …` / `--provider bitdeer`.
|
|
232
|
+
|
|
233
|
+
## Themes
|
|
234
|
+
|
|
235
|
+
VectorHead adopts [OpenCode's theme system](https://opencode.ai/docs/themes/):
|
|
236
|
+
select a built-in theme, or drop your own JSON files into
|
|
237
|
+
`~/.vector/themes/` (same files OpenCode uses work verbatim).
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
vector --theme tokyonight # one-off
|
|
241
|
+
# or persist it:
|
|
242
|
+
echo '{ "theme": "nord" }' >> ~/.vector/config.json # (merge manually)
|
|
243
|
+
# or inside the TUI: /theme
|
|
244
|
+
# or env: export VECTOR_THEME=matrix
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Built-ins: `vectorhead` (default, the Exact Freebuff dark palette),
|
|
248
|
+
`nord`, `tokyonight`, `gruvbox`, `catppuccin`, `one-dark`, `matrix`.
|
|
249
|
+
|
|
250
|
+
### Custom themes
|
|
251
|
+
|
|
252
|
+
A theme is a JSON file with an optional `defs` block of reusable colors
|
|
253
|
+
plus a semantic `theme` block. Color values may be hex (`#88C0D0`), ANSI
|
|
254
|
+
palette indexes (`3` for 0–255), references (`"nord8"` or `"primary"`),
|
|
255
|
+
dark/light variants (`{ "dark": "#000", "light": "#fff" }`), or
|
|
256
|
+
`"none"` (terminal default).
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"defs": { "brand": "#9EFC62" },
|
|
261
|
+
"theme": {
|
|
262
|
+
"primary": "brand",
|
|
263
|
+
"text": "#f1f5f9",
|
|
264
|
+
"textMuted": "#acb3bf",
|
|
265
|
+
"background": "#0b0f14",
|
|
266
|
+
"backgroundPanel": "#141a21",
|
|
267
|
+
"border": "#2b3542",
|
|
268
|
+
"error": "#ff6b6b",
|
|
269
|
+
"warning": "#ffa500",
|
|
270
|
+
"success": "#9EFC62"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Save it as `~/.vector/themes/my-theme.json`, then `/theme` (or
|
|
276
|
+
`vector --theme my-theme`) to use it. Semantic keys adopted from OpenCode
|
|
277
|
+
and their effect here: `primary` (brand accent), `accent`/`markdownHeading`
|
|
278
|
+
(headings, prompt), `text`, `textMuted`, `textFaint`, `markdownCode`,
|
|
279
|
+
`border`/`borderActive`, `error`/`warning`/`success`/`info`, `diffAdded`/
|
|
280
|
+
`diffRemoved`/`diffContext`, `backgroundPanel`/`backgroundElement`.
|
|
281
|
+
Unknown keys and unresolvable colors are ignored, so partial themes keep
|
|
282
|
+
the default palette for anything you don't override.
|
|
283
|
+
|
|
284
|
+
> Hex colors are emitted as truecolor (24-bit) escapes — terminals need
|
|
285
|
+
> `COLORTERM=truecolor` for full fidelity (most modern terminals). ANSI
|
|
286
|
+
> palette indexes and `vectorhead` work everywhere.
|
|
287
|
+
|
|
153
288
|
## Security model
|
|
154
289
|
|
|
155
290
|
Every shell command is classified:
|
package/dist/agent/agent.js
CHANGED
|
@@ -8,9 +8,14 @@ const search_1 = require("../tools/search");
|
|
|
8
8
|
const shell_1 = require("../tools/shell");
|
|
9
9
|
const git_1 = require("../tools/git");
|
|
10
10
|
const web_1 = require("../tools/web");
|
|
11
|
+
const extras_1 = require("../tools/extras");
|
|
12
|
+
const apply_patch_1 = require("../tools/apply-patch");
|
|
11
13
|
const permissions_1 = require("../security/permissions");
|
|
12
14
|
const config_1 = require("../config/config");
|
|
13
15
|
const loop_1 = require("./loop");
|
|
16
|
+
const rules_1 = require("./rules");
|
|
17
|
+
const skills_1 = require("./skills");
|
|
18
|
+
const skills_tool_1 = require("../tools/skills-tool");
|
|
14
19
|
class Agent {
|
|
15
20
|
config;
|
|
16
21
|
provider;
|
|
@@ -20,6 +25,11 @@ class Agent {
|
|
|
20
25
|
constructor(opts) {
|
|
21
26
|
this.config = (0, config_1.effectiveConfig)(opts.config);
|
|
22
27
|
this.cwd = opts.cwd || process.cwd();
|
|
28
|
+
// Seed Hermes-style SOUL.md/MEMORY.md/USER.md/SKILL.md on first run
|
|
29
|
+
// (never overwrites existing user files — see src/agent/rules.ts).
|
|
30
|
+
(0, rules_1.ensureRuleFiles)();
|
|
31
|
+
// Seed the bundled skill catalog into ~/.vector/skills/ (never overwrites).
|
|
32
|
+
(0, skills_1.seedBundledSkills)();
|
|
23
33
|
this.provider = (0, factory_1.createProvider)(this.config);
|
|
24
34
|
this.permissions = new permissions_1.PermissionManager({
|
|
25
35
|
mode: this.config.permissionMode,
|
|
@@ -31,7 +41,14 @@ class Agent {
|
|
|
31
41
|
...(0, search_1.createSearchTools)(),
|
|
32
42
|
(0, shell_1.createShellTool)(),
|
|
33
43
|
(0, git_1.createGitTool)(),
|
|
34
|
-
(0, web_1.
|
|
44
|
+
...(0, web_1.createWebTools)(),
|
|
45
|
+
// Freebuff-style extras: glob, read_subtree, task_completed,
|
|
46
|
+
// suggest_followups, render_ui.
|
|
47
|
+
...(0, extras_1.createExtrasTools)(),
|
|
48
|
+
// Unified-diff editing (Freebuff/Codebuff apply_patch format).
|
|
49
|
+
(0, apply_patch_1.createApplyPatchTool)(),
|
|
50
|
+
// Hermes-style progressive-disclosure skills (list_skills / read_skill).
|
|
51
|
+
...(0, skills_tool_1.createSkillTools)(),
|
|
35
52
|
]);
|
|
36
53
|
}
|
|
37
54
|
async run(request, signal) {
|
|
@@ -79,6 +96,7 @@ class Agent {
|
|
|
79
96
|
this.callbacks = {
|
|
80
97
|
onStatus: cb.onStatus,
|
|
81
98
|
onDelta: cb.onDelta,
|
|
99
|
+
onThinking: cb.onThinking,
|
|
82
100
|
onToolCall: cb.onToolCall,
|
|
83
101
|
onToolResult: cb.onToolResult,
|
|
84
102
|
onIteration: cb.onIteration,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Light small-talk detection — casual greetings, thanks, and questions about
|
|
4
|
+
* the agent itself do NOT need project context. Treating these as "light"
|
|
5
|
+
* saves tokens and, more importantly, stops the agent from inspecting or
|
|
6
|
+
* listing the whole project just because the user said "hallo" (it should
|
|
7
|
+
* simply answer back instead of reading the directory).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.isLightRequest = isLightRequest;
|
|
11
|
+
const LIGHT_PHRASES = new Set([
|
|
12
|
+
// greetings (id + en)
|
|
13
|
+
'hi', 'hii', 'hai', 'haii', 'hei', 'hey', 'heyy', 'hello', 'hallo', 'helo',
|
|
14
|
+
'halo', 'halow', 'hola', 'yo', 'yoo', 'yow', 'sup', 'wassup', 'whats up',
|
|
15
|
+
"what's up", 'hiya', 'howdy', 'selamat pagi', 'selamat siang', 'selamat sore',
|
|
16
|
+
'selamat malam', 'good morning', 'good afternoon', 'good evening',
|
|
17
|
+
'apa kabar', 'apakabar', 'kabar', 'kabar baik', 'how are you',
|
|
18
|
+
'how are you doing', 'hows it going', "how's it going",
|
|
19
|
+
// thanks / short acknowledgements
|
|
20
|
+
'thanks', 'thank you', 'thankyou', 'thx', 'ty', 'makasih', 'makasi',
|
|
21
|
+
'mkasih', 'mksh', 'terima kasih', 'trims', 'sip', 'siap', 'mantap',
|
|
22
|
+
'bagus', 'nice', 'oke', 'ok', 'okay', 'okey', 'okee',
|
|
23
|
+
// questions about the agent itself (no project needed)
|
|
24
|
+
'kamu siapa', 'siapa kamu', 'kamu sapa', 'who are you',
|
|
25
|
+
'kamu model apa', 'model apa kamu', 'model kamu apa', 'kamu pakai model apa',
|
|
26
|
+
'kamu provider apa', 'provider apa kamu', 'provider kamu apa', 'kamu pakai provider apa',
|
|
27
|
+
'kamu bisa apa', 'bisa apa', 'apa yang bisa kamu lakukan', 'apa yang kamu bisa',
|
|
28
|
+
'what can you do', 'can you help me', 'bisa bantu', 'boleh bantu',
|
|
29
|
+
'mau tanya', 'boleh tanya',
|
|
30
|
+
// skills / capabilities questions (agent, NOT the repository)
|
|
31
|
+
'apa skill mu', 'skill mu apa', 'skill kamu apa', 'apa skill kamu',
|
|
32
|
+
'apa aja skill mu', 'apa aja skill kamu', 'apa saja skill mu', 'apa saja skill kamu',
|
|
33
|
+
'skill apa aja', 'skill apa saja', 'skills apa', 'list skill', 'list skills',
|
|
34
|
+
'daftar skill', 'daftar skills', 'apa skills mu', 'skills kamu',
|
|
35
|
+
'keahlian mu apa', 'apa keahlian mu', 'apa aja keahlian mu', 'apa keahlian kamu',
|
|
36
|
+
'keahlian kamu apa', 'kemampuan mu apa', 'apa kemampuan mu', 'apa aja kemampuan mu',
|
|
37
|
+
'apa kemampuan kamu', 'kemampuan kamu apa', 'bisa apa aja', 'bisa apa saja',
|
|
38
|
+
'bisa ngapain', 'bisa ngapain aja', 'ngapain aja', 'kamu ngapain aja',
|
|
39
|
+
'bisa bantu apa', 'bisa membantu apa', 'bisa bantu apa saja',
|
|
40
|
+
'what skills do you have', 'list your skills', 'what are your skills',
|
|
41
|
+
'what are your capabilities', 'what are your abilities', 'your capabilities',
|
|
42
|
+
// what project are we on (identity of the workspace — brief answer, no
|
|
43
|
+
// full-project inspection / tool wall)
|
|
44
|
+
'apa project kita', 'project kita apa', 'project apa ini', 'apa project ini',
|
|
45
|
+
'ini project apa', 'project ini apa', 'apa proyek kita', 'proyek kita apa',
|
|
46
|
+
'proyek apa ini', 'apa proyek ini', 'ini proyek apa', 'proyek ini apa',
|
|
47
|
+
'project kamu apa', 'kamu project apa', 'apa project kamu', 'project apa ya ini',
|
|
48
|
+
]);
|
|
49
|
+
const MAX_LEN = 80;
|
|
50
|
+
/** True for short casual messages that deserve a direct answer, no tools. */
|
|
51
|
+
function isLightRequest(text) {
|
|
52
|
+
const t = text.trim();
|
|
53
|
+
if (!t || t.length > MAX_LEN)
|
|
54
|
+
return false;
|
|
55
|
+
// Normalize: lowercase, drop trailing punctuation/emoji/symbols.
|
|
56
|
+
const norm = t
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[\s!.,;:?]+$/, '')
|
|
59
|
+
.replace(/[^\x00-\x7F\s]/g, '')
|
|
60
|
+
.replace(/[\s!.,;:?]+$/, '')
|
|
61
|
+
.trim();
|
|
62
|
+
if (norm.length === 0)
|
|
63
|
+
return true; // emoji-only / punctuation-only message
|
|
64
|
+
return LIGHT_PHRASES.has(norm);
|
|
65
|
+
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -14,7 +14,10 @@ exports.estimateContextSize = estimateContextSize;
|
|
|
14
14
|
*/
|
|
15
15
|
const provider_1 = require("../providers/provider");
|
|
16
16
|
const context_1 = require("./context");
|
|
17
|
+
const light_1 = require("./light");
|
|
17
18
|
const prompts_1 = require("./prompts");
|
|
19
|
+
const rules_1 = require("./rules");
|
|
20
|
+
const skills_1 = require("./skills");
|
|
18
21
|
async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
19
22
|
const { provider, tools, permissions, config, cwd, signal } = opts;
|
|
20
23
|
const maxIterations = config.maxIterations;
|
|
@@ -24,11 +27,36 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
24
27
|
// the folder listing on every turn makes the agent re-explain/echo it.
|
|
25
28
|
const history = (opts.history ?? []).filter((m) => m.role !== 'system');
|
|
26
29
|
const isContinuation = history.length > 0;
|
|
27
|
-
|
|
30
|
+
// Casual small talk ("hallo", "hi", "apa kabar", "kamu siapa", …) gets a
|
|
31
|
+
// plain, direct answer: no project context, no folder listing, no skills
|
|
32
|
+
// catalog. The model must not read the whole directory just to greet back.
|
|
33
|
+
const isLight = (0, light_1.isLightRequest)(userRequest);
|
|
34
|
+
const context = isContinuation || isLight
|
|
28
35
|
? ''
|
|
29
36
|
: `${(0, context_1.buildProjectContext)(cwd)}\n\n${(0, context_1.listRootEntries)(cwd)}\n\n---\n\n`;
|
|
37
|
+
// Hermes-style agent rules: SOUL.md (identity) first, then AGENTS.md
|
|
38
|
+
// project rules, then bounded MEMORY.md / USER.md / SKILL.md. Empty
|
|
39
|
+
// sections are skipped, so nothing is added until the user/agent fills it.
|
|
40
|
+
const rulesText = (0, rules_1.rulesToPrompt)((0, rules_1.loadRules)(cwd));
|
|
41
|
+
// Level 0 of the skills catalog (names + one-line descriptions). Cheap
|
|
42
|
+
// enough to always include; full instructions load on demand via the
|
|
43
|
+
// skill/read_skill tools. Scans ALL roots — including the project's
|
|
44
|
+
// .agents/skills (`npx skills add` layout) — Freebuff parity.
|
|
45
|
+
const skillsCatalog = (0, skills_1.skillsCatalogText)((0, skills_1.listSkills)(undefined, cwd));
|
|
30
46
|
const messages = [
|
|
31
|
-
|
|
47
|
+
// Slot #1: identity (SOUL.md or the built-in VectorHead persona).
|
|
48
|
+
{ role: 'system', content: rulesText || prompts_1.SYSTEM_PROMPT },
|
|
49
|
+
// Core behavior rules (only when SOUL.md is active we still need the
|
|
50
|
+
// operation rules; when rulesText is empty SYSTEM_PROMPT already covers it).
|
|
51
|
+
...(rulesText ? [{ role: 'system', content: prompts_1.SYSTEM_PROMPT }] : []),
|
|
52
|
+
// Identity: the model must know its own name/provider to answer
|
|
53
|
+
// "kamu model apa / provider mana" truthfully instead of guessing.
|
|
54
|
+
{ role: 'system', content: (0, prompts_1.identityPrompt)(config.model, config.provider) },
|
|
55
|
+
// Skills catalog (Level 0) injected on the first turn; on continuation
|
|
56
|
+
// the model already saw it and can still list_skills at any time.
|
|
57
|
+
...(!isContinuation && !isLight && skillsCatalog
|
|
58
|
+
? [{ role: 'system', content: skillsCatalog }]
|
|
59
|
+
: []),
|
|
32
60
|
...history,
|
|
33
61
|
{ role: 'user', content: `${context}TASK:\n${userRequest}` },
|
|
34
62
|
];
|
|
@@ -37,12 +65,81 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
37
65
|
let aborted = false;
|
|
38
66
|
let stopped = false;
|
|
39
67
|
let finalContent = '';
|
|
68
|
+
let lastReasoning = '';
|
|
40
69
|
const checkAbort = () => {
|
|
41
70
|
if (signal?.aborted) {
|
|
42
71
|
aborted = true;
|
|
43
72
|
throw new Error('aborted');
|
|
44
73
|
}
|
|
45
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* One model call. Streams when the provider supports it so tokens appear
|
|
77
|
+
* live in the TUI (Freebuff-style) instead of a silent freeze while the
|
|
78
|
+
* model thinks (BitDeer cold starts can be 30–120s). Falls back to chat
|
|
79
|
+
* for providers that cannot stream.
|
|
80
|
+
*/
|
|
81
|
+
async function doCall() {
|
|
82
|
+
const params = { messages, model: config.model, tools: tools.definitions(), signal };
|
|
83
|
+
if (!provider.capabilities().streaming) {
|
|
84
|
+
const r = await provider.chat(params);
|
|
85
|
+
// Non-streaming providers deliver reasoning only in the result —
|
|
86
|
+
// normalize it into onThinking (once) so the TUI behaves the same.
|
|
87
|
+
if (r.reasoning) {
|
|
88
|
+
lastReasoning = r.reasoning;
|
|
89
|
+
callbacks.onThinking?.(r.reasoning);
|
|
90
|
+
}
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
let content = '';
|
|
94
|
+
const toolStream = [];
|
|
95
|
+
let final = null;
|
|
96
|
+
for await (const ev of provider.stream(params)) {
|
|
97
|
+
if (ev.type === 'delta' && ev.delta) {
|
|
98
|
+
content += ev.delta;
|
|
99
|
+
callbacks.onDelta?.(ev.delta);
|
|
100
|
+
}
|
|
101
|
+
else if (ev.type === 'delta' && ev.reasoning) {
|
|
102
|
+
// Model thinking (reasoning_content) — surfaced live to the TUI's
|
|
103
|
+
// thinking card, kept OUT of the visible answer text.
|
|
104
|
+
lastReasoning += ev.reasoning;
|
|
105
|
+
callbacks.onThinking?.(ev.reasoning);
|
|
106
|
+
}
|
|
107
|
+
else if (ev.type === 'tool_call' && ev.toolCall) {
|
|
108
|
+
// The same tool call id can be yielded repeatedly (partial chunks);
|
|
109
|
+
// keep the latest complete copy.
|
|
110
|
+
const call = ev.toolCall;
|
|
111
|
+
const idx = toolStream.findIndex((t) => t.id === call.id);
|
|
112
|
+
if (idx >= 0)
|
|
113
|
+
toolStream[idx] = call;
|
|
114
|
+
else
|
|
115
|
+
toolStream.push(call);
|
|
116
|
+
}
|
|
117
|
+
else if (ev.type === 'error') {
|
|
118
|
+
throw new provider_1.ProviderError(ev.error || 'Streaming error from provider.', 0, false);
|
|
119
|
+
}
|
|
120
|
+
else if (ev.type === 'done' && ev.result) {
|
|
121
|
+
final = ev.result;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (final) {
|
|
125
|
+
if (final.reasoning && !lastReasoning) {
|
|
126
|
+
// Reasoning appeared ONLY in the final result (no deltas were
|
|
127
|
+
// streamed): emit once so the TUI still gets the thinking card.
|
|
128
|
+
lastReasoning = final.reasoning;
|
|
129
|
+
callbacks.onThinking?.(final.reasoning);
|
|
130
|
+
}
|
|
131
|
+
if (lastReasoning && !final.reasoning)
|
|
132
|
+
final.reasoning = lastReasoning;
|
|
133
|
+
lastReasoning = '';
|
|
134
|
+
return final;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
content,
|
|
138
|
+
reasoning: lastReasoning || undefined,
|
|
139
|
+
toolCalls: toolStream,
|
|
140
|
+
stopReason: toolStream.length > 0 ? 'tool_calls' : 'stop',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
46
143
|
while (iterations < maxIterations) {
|
|
47
144
|
try {
|
|
48
145
|
checkAbort();
|
|
@@ -57,7 +154,7 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
57
154
|
callbacks.onStatus?.(iterations === 1 ? 'Thinking' : 'Thinking…');
|
|
58
155
|
let result;
|
|
59
156
|
try {
|
|
60
|
-
result = await (0, provider_1.withRetry)(
|
|
157
|
+
result = await (0, provider_1.withRetry)(doCall, { retries: config.maxRetries, signal });
|
|
61
158
|
}
|
|
62
159
|
catch (err) {
|
|
63
160
|
if (err.name === 'AbortError' || err.message === 'aborted') {
|
|
@@ -105,7 +202,7 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
105
202
|
onActivity: callbacks.onActivity,
|
|
106
203
|
maxOutput: 30_000,
|
|
107
204
|
});
|
|
108
|
-
callbacks.onToolResult?.(tc.name, toolResult.output);
|
|
205
|
+
callbacks.onToolResult?.(tc.name, toolResult.output, toolResult.data);
|
|
109
206
|
messages.push({
|
|
110
207
|
role: 'tool',
|
|
111
208
|
toolCallId: tc.id,
|
|
@@ -124,7 +221,8 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
124
221
|
// No tool calls → final answer
|
|
125
222
|
finalContent = result.content;
|
|
126
223
|
callbacks.onStatus?.('Completed');
|
|
127
|
-
callbacks.onFinal?.(result);
|
|
224
|
+
callbacks.onFinal?.({ ...result, reasoning: lastReasoning || result.reasoning });
|
|
225
|
+
lastReasoning = '';
|
|
128
226
|
return { content: finalContent, iterations, toolCalls, stopped, aborted, messages };
|
|
129
227
|
}
|
|
130
228
|
if (iterations >= maxIterations) {
|
|
@@ -136,9 +234,12 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
136
234
|
return { content: finalContent, iterations, toolCalls, stopped, aborted, messages };
|
|
137
235
|
}
|
|
138
236
|
/** Compact conversation history into a summary to reduce token usage. */
|
|
139
|
-
function compactMessages(messages, summarize) {
|
|
237
|
+
function compactMessages(messages, summarize, config) {
|
|
140
238
|
return summarize(messages).then((summary) => [
|
|
141
239
|
{ role: 'system', content: prompts_1.SYSTEM_PROMPT },
|
|
240
|
+
...(config
|
|
241
|
+
? [{ role: 'system', content: (0, prompts_1.identityPrompt)(config.model, config.provider) }]
|
|
242
|
+
: []),
|
|
142
243
|
{
|
|
143
244
|
role: 'user',
|
|
144
245
|
content: `[Previous conversation summarized]\n${summary}`,
|
package/dist/agent/planner.js
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generatePlan = generatePlan;
|
|
4
4
|
const context_1 = require("./context");
|
|
5
|
+
const light_1 = require("./light");
|
|
5
6
|
const PLAN_PROMPT = `You are a planning engine. Given the user's task and the project context, produce a concise numbered plan of the steps the agent should take. Output ONLY the numbered steps, one per line. No preamble, no markdown fences.`;
|
|
6
7
|
async function generatePlan(provider, userRequest, cwd, model, signal) {
|
|
8
|
+
// Casual small talk: answer directly — don't spend an LLM call (or the
|
|
9
|
+
// project context) on a greeting.
|
|
10
|
+
if ((0, light_1.isLightRequest)(userRequest)) {
|
|
11
|
+
return { steps: [userRequest], raw: '' };
|
|
12
|
+
}
|
|
7
13
|
const projectCtx = (0, context_1.buildProjectContext)(cwd);
|
|
8
14
|
const entries = (0, context_1.listRootEntries)(cwd);
|
|
9
15
|
const messages = [
|
package/dist/agent/prompts.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.SYSTEM_PROMPT = void 0;
|
|
7
|
+
exports.identityPrompt = identityPrompt;
|
|
7
8
|
exports.taskPrompt = taskPrompt;
|
|
8
9
|
exports.continuationPrompt = continuationPrompt;
|
|
9
10
|
exports.SYSTEM_PROMPT = `You are VectorHead, an AI coding agent operating inside a terminal.
|
|
@@ -24,7 +25,44 @@ Rules:
|
|
|
24
25
|
6. Never print API keys, tokens, or secrets. If you find a secret, say so without revealing it.
|
|
25
26
|
7. When you need more information than you have, use tools to get it instead of asking the user.
|
|
26
27
|
8. Answer in the same language the user used.
|
|
27
|
-
9. Keep responses concise but complete: state what you did, why, and how it was verified
|
|
28
|
+
9. Keep responses concise but complete: state what you did, why, and how it was verified.
|
|
29
|
+
10. Reusable procedures are saved as skills. Run list_skills to see what's installed
|
|
30
|
+
(names + descriptions), then read_skill with the skill name to load the full
|
|
31
|
+
instructions the moment you actually follow one. Use a matching skill when a
|
|
32
|
+
task clearly fits one instead of improvising from scratch. NEVER print the
|
|
33
|
+
installed-skills list or list_skills output back into your reply — it is only
|
|
34
|
+
an internal index; use it to pick and load a skill, nothing more.
|
|
35
|
+
11. When the user only greets you or asks about yourself (e.g. "hallo", "hi",
|
|
36
|
+
"apa kabar", "terima kasih", "kamu siapa?", "kamu model apa", "kamu bisa
|
|
37
|
+
apa?"), reply briefly and directly. Do NOT inspect the project, list files,
|
|
38
|
+
or call any tool for such messages — a greeting is not a task.
|
|
39
|
+
12. Whenever you receive a task that needs work (inspect, search, edit, run
|
|
40
|
+
anything), FIRST open your reply with one very short acknowledgment line
|
|
41
|
+
about what you are about to do — e.g. "Oke, saya akan cari dulu.",
|
|
42
|
+
"Baik, saya periksa dulu.", "Siap, saya kerjakan.", "Ok, let me look
|
|
43
|
+
first." — and only then start using tools. This way the user sees you
|
|
44
|
+
respond immediately even before the first tool runs. Keep it to one line;
|
|
45
|
+
do not repeat or pad it. For pure greetings/identity questions (rule 11)
|
|
46
|
+
skip the acknowledgment and answer directly.
|
|
47
|
+
13. Live task checklist (write_todos tool): the user watches a live ☐/☑
|
|
48
|
+
checklist while you work, so show it whenever a request needs 2+ tool
|
|
49
|
+
calls. Hard rule: after your one-line acknowledgment, your FIRST tool
|
|
50
|
+
call MUST be write_todos with the FULL step list as
|
|
51
|
+
{ task, completed: false } items; then after each step finishes, call
|
|
52
|
+
write_todos again with the full updated list (finished steps
|
|
53
|
+
completed: true). Example: for "buat file lalu baca isinya" first call
|
|
54
|
+
write_todos with ["Buat file", "Baca isinya"] before calling write_file.
|
|
55
|
+
The ONLY exceptions — greetings, plain questions, small talk, and
|
|
56
|
+
requests needing just ONE tool call: answer those directly with no
|
|
57
|
+
write_todos.`;
|
|
58
|
+
/**
|
|
59
|
+
* Identity line injected into the system prompt so the model can answer
|
|
60
|
+
* "which model/provider are you?" truthfully (it has no other way to know).
|
|
61
|
+
*/
|
|
62
|
+
function identityPrompt(model, provider) {
|
|
63
|
+
return `You are currently running as the model "${model}" via the "${provider}" provider (OpenAI-compatible API unless stated otherwise).
|
|
64
|
+
If the user asks which model you are or which provider backs you, answer with exactly this — e.g. "Saya dijalankan oleh model deepseek-ai/DeepSeek-V4-Flash lewat provider custom". Do not invent a different model name.`;
|
|
65
|
+
}
|
|
28
66
|
function taskPrompt(userRequest, cwd) {
|
|
29
67
|
return `Working directory: ${cwd}
|
|
30
68
|
|