agen-vektor 0.3.8 → 0.3.10
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 +151 -22
- package/dist/agent/agent.js +23 -1
- package/dist/agent/light.js +65 -0
- package/dist/agent/loop.js +165 -21
- 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 +57 -21
- 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 +166 -0
- package/dist/providers/custom.js +5 -2
- package/dist/providers/factory.js +45 -14
- package/dist/providers/openai-compat.js +85 -9
- package/dist/providers/openrouter.js +1 -1
- package/dist/tools/apply-patch.js +264 -0
- package/dist/tools/background.js +217 -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 +1827 -164
- package/dist/tui/chat.js +1255 -46
- package/dist/tui/commands.js +97 -0
- package/dist/tui/components.js +13 -5
- package/dist/tui/statusbar.js +59 -23
- package/dist/tui/suggest.js +63 -0
- package/dist/tui/theme.js +71 -24
- package/dist/tui/themes.js +430 -0
- package/dist/utils/paths.js +42 -0
- package/package.json +1 -9
package/README.md
CHANGED
|
@@ -12,33 +12,41 @@
|
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
VectorHead ● Ready
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
│ Perbaiki error authentication pada project ini
|
|
19
|
-
VectorHead akan memeriksa file auth, memperbaiki, lalu menjalankan test.
|
|
15
|
+
VectorHead ● Ready deepseek-ai/DeepSeek-V4-Flash
|
|
16
|
+
[09:41]
|
|
17
|
+
Perbaiki error authentication pada project ini
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
# Cek Auth ← kuning (heading H1–H6, Freebuff palette)
|
|
20
|
+
VectorHead akan memeriksa file auth, memperbaiki, lalu menjalankan test.
|
|
21
|
+
[09:42]
|
|
22
|
+
$ npm test
|
|
23
|
+
... (2 more lines above)
|
|
24
|
+
3 tests passed
|
|
24
25
|
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
26
|
+
│ │
|
|
25
27
|
│ ▍ Enter a coding task or / for commands │
|
|
28
|
+
│ │
|
|
26
29
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
27
30
|
● Ready ● ask
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
A Freebuff-style terminal UI
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
A Freebuff-style terminal UI with the EXACT Freebuff dark palette (`#9EFC62`
|
|
34
|
+
green accents, markdown headings H1–H6 all yellow `#facc15`, inline code
|
|
35
|
+
BOLD orange `#FF8534` on a `#374151` chip, links blue `#3B82F6`, fenced
|
|
36
|
+
code `// lang` header `#5b647a` on a `#374151` slab, plain prose — quoted
|
|
37
|
+
from [CodebuffAI/freebuff](https://github.com/CodebuffAI/freebuff)
|
|
38
|
+
theme-system.ts + markdown-renderer.tsx), a compact agent-activity strip
|
|
39
|
+
(⚙ tool calls), and a rounded input box with a block cursor — all rendered
|
|
40
|
+
incrementally with no flicker.
|
|
33
41
|
|
|
34
42
|
## Features
|
|
35
43
|
|
|
36
|
-
- **Interactive TUI** — chat, agent status, tool execution, diff viewer, input. Keyboard driven, mouse-aware, works on 80×24 terminals and survives resize.
|
|
44
|
+
- **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
45
|
- **Agent loop** — plan → inspect → tool call → result → review → fix → retry until done (with an iteration cap).
|
|
38
46
|
- **Tools** — read/write/edit files, list directories, search files, run shell commands, git, fetch web pages.
|
|
39
47
|
- **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
48
|
- **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint.
|
|
41
|
-
- **Sessions & memory** — conversations persist to `~/.vector/sessions/`; `/continue` resumes
|
|
49
|
+
- **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
50
|
- **Context management** — only relevant context is sent to the model, with compaction (`/compact`).
|
|
43
51
|
|
|
44
52
|
## Installation
|
|
@@ -47,13 +55,8 @@ Requirements: **Node.js 20+** and **npm** (Termux: `pkg install nodejs`).
|
|
|
47
55
|
|
|
48
56
|
### From source
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
cd vector-agent
|
|
53
|
-
npm install
|
|
54
|
-
npm run build
|
|
55
|
-
npm link # makes `vector` available on PATH
|
|
56
|
-
```
|
|
58
|
+
Dari source: clone repo ini, lalu `npm install && npm run build && npm link`
|
|
59
|
+
(membuat `vector` tersedia di PATH).
|
|
57
60
|
|
|
58
61
|
Then:
|
|
59
62
|
|
|
@@ -100,6 +103,7 @@ vector
|
|
|
100
103
|
| `vector "task"` | Start TUI and run the task immediately |
|
|
101
104
|
| `vector --model MODEL` | Set the model |
|
|
102
105
|
| `vector --provider P` | Set provider: `openai`, `anthropic`, `gemini`, `openrouter`, `ollama`, `custom` |
|
|
106
|
+
| `vector --theme NAME` | Set theme: built-in or `~/.vector/themes/<name>.json` |
|
|
103
107
|
| `vector --continue` | Continue the most recent session |
|
|
104
108
|
| `vector --session NAME` | Use/resume session `NAME` |
|
|
105
109
|
| `vector --plan` | Show a plan before executing |
|
|
@@ -113,11 +117,14 @@ Type `/` inside VectorHead:
|
|
|
113
117
|
| Command | Description |
|
|
114
118
|
|---|---|
|
|
115
119
|
| `/model` | Select a model |
|
|
120
|
+
| `/theme` | Select a theme (OpenCode-style) |
|
|
116
121
|
| `/provider` | Select a provider |
|
|
122
|
+
| `/provider-remove` | Remove the stored provider key (and custom URL/name) so you can re-enter fresh values |
|
|
117
123
|
| `/session` | Browse saved sessions |
|
|
118
124
|
| `/settings` | Show configuration |
|
|
119
125
|
| `/status` | Show agent status |
|
|
120
|
-
| `/clear` | Clear the conversation |
|
|
126
|
+
| `/clear` | Clear the conversation (keeps the session) |
|
|
127
|
+
| `/new` | Start a fresh conversation — rotates to a brand-new session (the old one is kept on disk) |
|
|
121
128
|
| `/continue` | Continue the current/last session |
|
|
122
129
|
| `/diff` | Show diff viewer |
|
|
123
130
|
| `/git` | Run `git status` |
|
|
@@ -138,7 +145,7 @@ export OPENROUTER_API_KEY=...
|
|
|
138
145
|
|
|
139
146
|
Or inside VectorHead: `/provider` → pick provider → paste key (stored masked in `~/.vector/credentials.json`, permissions `0600`).
|
|
140
147
|
|
|
141
|
-
Generic variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_HOME
|
|
148
|
+
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
149
|
|
|
143
150
|
### Custom API (OpenAI-compatible)
|
|
144
151
|
|
|
@@ -150,6 +157,128 @@ vector --provider custom --model deepseek-v3
|
|
|
150
157
|
|
|
151
158
|
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
159
|
|
|
160
|
+
### Multiple custom providers (OpenCode-style)
|
|
161
|
+
|
|
162
|
+
Any number of OpenAI-compatible providers can be defined side by side in
|
|
163
|
+
`~/.vector/config.json` under `providers` — the same shape OpenCode uses in
|
|
164
|
+
its `provider` section (minus the `npm` field, since every custom endpoint
|
|
165
|
+
here is OpenAI-compatible):
|
|
166
|
+
|
|
167
|
+
```jsonc
|
|
168
|
+
{
|
|
169
|
+
"provider": "bitdeer", // ACTIVE provider id
|
|
170
|
+
"model": "deepseek-ai/DeepSeek-V4-Flash",
|
|
171
|
+
"providers": {
|
|
172
|
+
"bitdeer": {
|
|
173
|
+
"name": "BitDeer",
|
|
174
|
+
"options": {
|
|
175
|
+
"baseURL": "https://api-inference.bitdeer.ai/v1",
|
|
176
|
+
"apiKey": "{env:BITDEER_API_KEY}" // or a plain key, or omit and store via /provider
|
|
177
|
+
},
|
|
178
|
+
"models": {
|
|
179
|
+
"deepseek-ai/DeepSeek-V4-Flash": { "name": "DeepSeek V4 Flash" }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"groq2": {
|
|
183
|
+
"name": "Groq 2",
|
|
184
|
+
"options": { "baseURL": "https://groq2.example.com/v1" }
|
|
185
|
+
},
|
|
186
|
+
"agentrouter": {
|
|
187
|
+
"name": "AgentRouter",
|
|
188
|
+
"model": "deepseek-v4-flash", // default model when this provider is active
|
|
189
|
+
"options": {
|
|
190
|
+
"baseURL": "https://agentrouter.org/v1",
|
|
191
|
+
// Gateways that filter by client (AgentRouter only accepts official
|
|
192
|
+
// AI clients) need a spoofed User-Agent on every request:
|
|
193
|
+
"headers": {
|
|
194
|
+
"User-Agent": "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464",
|
|
195
|
+
"Originator": "codex_cli_rs"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- `provider` is the active id — a built-in, the legacy `custom` slot, or any
|
|
204
|
+
key of `providers`. Keys are **per provider id**: store each under its own
|
|
205
|
+
id in `~/.vector/credentials.json` (0600) via `/provider`, inline them in
|
|
206
|
+
`options.apiKey`, or reference an env var with `"{env:NAME}"`.
|
|
207
|
+
- `options.headers` adds extra HTTP headers to every request of that provider
|
|
208
|
+
(e.g. a `User-Agent` required by a client-filtering gateway like
|
|
209
|
+
AgentRouter). Values may be plain strings or `"{env:NAME}"` templates;
|
|
210
|
+
headers referencing a missing env var are dropped.
|
|
211
|
+
- `model` is the **default model** for that provider: when the provider
|
|
212
|
+
becomes active (`/provider`, `--provider`, or `VECTOR_PROVIDER`), the active
|
|
213
|
+
model automatically follows it unless an explicit `--model` / `VECTOR_MODEL`
|
|
214
|
+
was given. Handy when each provider speaks its own model ids (e.g.
|
|
215
|
+
AgentRouter uses `deepseek-v4-flash` while BitDeer uses
|
|
216
|
+
`deepseek-ai/DeepSeek-V4-Flash`).
|
|
217
|
+
- `/provider` lists built-ins + every configured custom and offers
|
|
218
|
+
**➕ Add custom provider** (id → name → base URL → key). `/model` then
|
|
219
|
+
auto-discovers models from the endpoint's `/models` route and falls back
|
|
220
|
+
to the `models` map above.
|
|
221
|
+
- The old single-custom setup (`provider: "custom"` + `apiUrl`/`customName`
|
|
222
|
+
+ key under `custom`) keeps working unchanged.
|
|
223
|
+
|
|
224
|
+
Switch at runtime with `/provider`, or per-run with
|
|
225
|
+
`VECTOR_PROVIDER=bitdeer vector …` / `--provider bitdeer`.
|
|
226
|
+
|
|
227
|
+
## Themes
|
|
228
|
+
|
|
229
|
+
VectorHead adopts [OpenCode's theme system](https://opencode.ai/docs/themes/):
|
|
230
|
+
select a built-in theme, or drop your own JSON files into
|
|
231
|
+
`~/.vector/themes/` (same files OpenCode uses work verbatim).
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
vector --theme tokyonight # one-off
|
|
235
|
+
# or persist it:
|
|
236
|
+
echo '{ "theme": "nord" }' >> ~/.vector/config.json # (merge manually)
|
|
237
|
+
# or inside the TUI: /theme
|
|
238
|
+
# or env: export VECTOR_THEME=matrix
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Built-ins: `vectorhead` (default, the Exact Freebuff dark palette),
|
|
242
|
+
`nord`, `tokyonight`, `gruvbox`, `catppuccin`, `one-dark`, `matrix`.
|
|
243
|
+
|
|
244
|
+
### Custom themes
|
|
245
|
+
|
|
246
|
+
A theme is a JSON file with an optional `defs` block of reusable colors
|
|
247
|
+
plus a semantic `theme` block. Color values may be hex (`#88C0D0`), ANSI
|
|
248
|
+
palette indexes (`3` for 0–255), references (`"nord8"` or `"primary"`),
|
|
249
|
+
dark/light variants (`{ "dark": "#000", "light": "#fff" }`), or
|
|
250
|
+
`"none"` (terminal default).
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"defs": { "brand": "#9EFC62" },
|
|
255
|
+
"theme": {
|
|
256
|
+
"primary": "brand",
|
|
257
|
+
"text": "#f1f5f9",
|
|
258
|
+
"textMuted": "#acb3bf",
|
|
259
|
+
"background": "#0b0f14",
|
|
260
|
+
"backgroundPanel": "#141a21",
|
|
261
|
+
"border": "#2b3542",
|
|
262
|
+
"error": "#ff6b6b",
|
|
263
|
+
"warning": "#ffa500",
|
|
264
|
+
"success": "#9EFC62"
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Save it as `~/.vector/themes/my-theme.json`, then `/theme` (or
|
|
270
|
+
`vector --theme my-theme`) to use it. Semantic keys adopted from OpenCode
|
|
271
|
+
and their effect here: `primary` (brand accent), `accent`/`markdownHeading`
|
|
272
|
+
(headings, prompt), `text`, `textMuted`, `textFaint`, `markdownCode`,
|
|
273
|
+
`border`/`borderActive`, `error`/`warning`/`success`/`info`, `diffAdded`/
|
|
274
|
+
`diffRemoved`/`diffContext`, `backgroundPanel`/`backgroundElement`.
|
|
275
|
+
Unknown keys and unresolvable colors are ignored, so partial themes keep
|
|
276
|
+
the default palette for anything you don't override.
|
|
277
|
+
|
|
278
|
+
> Hex colors are emitted as truecolor (24-bit) escapes — terminals need
|
|
279
|
+
> `COLORTERM=truecolor` for full fidelity (most modern terminals). ANSI
|
|
280
|
+
> palette indexes and `vectorhead` work everywhere.
|
|
281
|
+
|
|
153
282
|
## Security model
|
|
154
283
|
|
|
155
284
|
Every shell command is classified:
|
package/dist/agent/agent.js
CHANGED
|
@@ -8,9 +8,15 @@ 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");
|
|
19
|
+
const background_1 = require("../tools/background");
|
|
14
20
|
class Agent {
|
|
15
21
|
config;
|
|
16
22
|
provider;
|
|
@@ -20,6 +26,11 @@ class Agent {
|
|
|
20
26
|
constructor(opts) {
|
|
21
27
|
this.config = (0, config_1.effectiveConfig)(opts.config);
|
|
22
28
|
this.cwd = opts.cwd || process.cwd();
|
|
29
|
+
// Seed Hermes-style SOUL.md/MEMORY.md/USER.md/SKILL.md on first run
|
|
30
|
+
// (never overwrites existing user files — see src/agent/rules.ts).
|
|
31
|
+
(0, rules_1.ensureRuleFiles)();
|
|
32
|
+
// Seed the bundled skill catalog into ~/.vector/skills/ (never overwrites).
|
|
33
|
+
(0, skills_1.seedBundledSkills)();
|
|
23
34
|
this.provider = (0, factory_1.createProvider)(this.config);
|
|
24
35
|
this.permissions = new permissions_1.PermissionManager({
|
|
25
36
|
mode: this.config.permissionMode,
|
|
@@ -31,7 +42,17 @@ class Agent {
|
|
|
31
42
|
...(0, search_1.createSearchTools)(),
|
|
32
43
|
(0, shell_1.createShellTool)(),
|
|
33
44
|
(0, git_1.createGitTool)(),
|
|
34
|
-
(0, web_1.
|
|
45
|
+
...(0, web_1.createWebTools)(),
|
|
46
|
+
// Freebuff-style extras: glob, read_subtree, task_completed,
|
|
47
|
+
// suggest_followups, render_ui.
|
|
48
|
+
...(0, extras_1.createExtrasTools)(),
|
|
49
|
+
// Unified-diff editing (Freebuff/Codebuff apply_patch format).
|
|
50
|
+
(0, apply_patch_1.createApplyPatchTool)(),
|
|
51
|
+
// Hermes-style progressive-disclosure skills (list_skills / read_skill).
|
|
52
|
+
...(0, skills_tool_1.createSkillTools)(),
|
|
53
|
+
// Parallelism: non-blocking background shell jobs (run_in_background /
|
|
54
|
+
// output_from_background / stop_background_job).
|
|
55
|
+
...(0, background_1.createBackgroundTools)(),
|
|
35
56
|
]);
|
|
36
57
|
}
|
|
37
58
|
async run(request, signal) {
|
|
@@ -79,6 +100,7 @@ class Agent {
|
|
|
79
100
|
this.callbacks = {
|
|
80
101
|
onStatus: cb.onStatus,
|
|
81
102
|
onDelta: cb.onDelta,
|
|
103
|
+
onThinking: cb.onThinking,
|
|
82
104
|
onToolCall: cb.onToolCall,
|
|
83
105
|
onToolResult: cb.onToolResult,
|
|
84
106
|
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') {
|
|
@@ -93,25 +190,68 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
93
190
|
toolCalls: result.toolCalls,
|
|
94
191
|
});
|
|
95
192
|
if (hasToolCalls) {
|
|
193
|
+
// Parallel execution for READ-ONLY tools (Freebuff-style: when the
|
|
194
|
+
// model requests several independent reads at once — read_file,
|
|
195
|
+
// search, glob — run them concurrently instead of one-by-one).
|
|
196
|
+
// Anything that mutates, prompts for permission, or starts background
|
|
197
|
+
// work stays sequential so side effects keep their order.
|
|
198
|
+
const PARALLEL_TOOLS = new Set([
|
|
199
|
+
'read_file',
|
|
200
|
+
'list_directory',
|
|
201
|
+
'search_files',
|
|
202
|
+
'glob',
|
|
203
|
+
'read_subtree',
|
|
204
|
+
'list_skills',
|
|
205
|
+
'output_from_background',
|
|
206
|
+
]);
|
|
207
|
+
const runOne = async (tc) => {
|
|
208
|
+
callbacks.onToolCall?.(tc.name, tc.arguments, tc.id);
|
|
209
|
+
const toolResult = await tools.execute(tc.name, tc.arguments, {
|
|
210
|
+
cwd,
|
|
211
|
+
permissions,
|
|
212
|
+
onActivity: callbacks.onActivity,
|
|
213
|
+
maxOutput: 30_000,
|
|
214
|
+
});
|
|
215
|
+
return { tc, toolResult };
|
|
216
|
+
};
|
|
96
217
|
try {
|
|
218
|
+
const batches = [];
|
|
219
|
+
let current = [];
|
|
97
220
|
for (const tc of result.toolCalls) {
|
|
221
|
+
if (current.length > 0 && !PARALLEL_TOOLS.has(tc.name)) {
|
|
222
|
+
batches.push(current);
|
|
223
|
+
current = [];
|
|
224
|
+
}
|
|
225
|
+
current.push(tc);
|
|
226
|
+
if (!PARALLEL_TOOLS.has(tc.name)) {
|
|
227
|
+
batches.push(current);
|
|
228
|
+
current = [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (current.length > 0)
|
|
232
|
+
batches.push(current);
|
|
233
|
+
for (const batch of batches) {
|
|
98
234
|
checkAbort();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
235
|
+
const parallel = batch.length > 1 && batch.every((tc) => PARALLEL_TOOLS.has(tc.name));
|
|
236
|
+
toolCalls += batch.length;
|
|
237
|
+
if (!parallel) {
|
|
238
|
+
callbacks.onStatus?.(`Running ${batch[0].name}`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
callbacks.onStatus?.(`Running ${batch.length}× ${batch[0].name} (parallel)`);
|
|
242
|
+
}
|
|
243
|
+
const results = parallel
|
|
244
|
+
? await Promise.all(batch.map(runOne))
|
|
245
|
+
: [await runOne(batch[0])];
|
|
246
|
+
for (const { tc, toolResult } of results) {
|
|
247
|
+
callbacks.onToolResult?.(tc.name, toolResult.output, toolResult.data, tc.id);
|
|
248
|
+
messages.push({
|
|
249
|
+
role: 'tool',
|
|
250
|
+
toolCallId: tc.id,
|
|
251
|
+
name: tc.name,
|
|
252
|
+
content: toolResult.output.slice(0, 40_000),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
115
255
|
}
|
|
116
256
|
}
|
|
117
257
|
catch {
|
|
@@ -124,7 +264,8 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
124
264
|
// No tool calls → final answer
|
|
125
265
|
finalContent = result.content;
|
|
126
266
|
callbacks.onStatus?.('Completed');
|
|
127
|
-
callbacks.onFinal?.(result);
|
|
267
|
+
callbacks.onFinal?.({ ...result, reasoning: lastReasoning || result.reasoning });
|
|
268
|
+
lastReasoning = '';
|
|
128
269
|
return { content: finalContent, iterations, toolCalls, stopped, aborted, messages };
|
|
129
270
|
}
|
|
130
271
|
if (iterations >= maxIterations) {
|
|
@@ -136,9 +277,12 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
136
277
|
return { content: finalContent, iterations, toolCalls, stopped, aborted, messages };
|
|
137
278
|
}
|
|
138
279
|
/** Compact conversation history into a summary to reduce token usage. */
|
|
139
|
-
function compactMessages(messages, summarize) {
|
|
280
|
+
function compactMessages(messages, summarize, config) {
|
|
140
281
|
return summarize(messages).then((summary) => [
|
|
141
282
|
{ role: 'system', content: prompts_1.SYSTEM_PROMPT },
|
|
283
|
+
...(config
|
|
284
|
+
? [{ role: 'system', content: (0, prompts_1.identityPrompt)(config.model, config.provider) }]
|
|
285
|
+
: []),
|
|
142
286
|
{
|
|
143
287
|
role: 'user',
|
|
144
288
|
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 = [
|