agen-vektor 0.3.9 → 0.3.11

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 CHANGED
@@ -13,7 +13,6 @@
13
13
 
14
14
  ```
15
15
  VectorHead ● Ready deepseek-ai/DeepSeek-V4-Flash
16
- ───────────────────────────────────────────────────────────────────────────────
17
16
  [09:41]
18
17
  Perbaiki error authentication pada project ini
19
18
 
@@ -56,13 +55,8 @@ Requirements: **Node.js 20+** and **npm** (Termux: `pkg install nodejs`).
56
55
 
57
56
  ### From source
58
57
 
59
- ```bash
60
- git clone https://github.com/clickmamaheti-prog/vector-agent.git
61
- cd vector-agent
62
- npm install
63
- npm run build
64
- npm link # makes `vector` available on PATH
65
- ```
58
+ Dari source: clone repo ini, lalu `npm install && npm run build && npm link`
59
+ (membuat `vector` tersedia di PATH).
66
60
 
67
61
  Then:
68
62
 
@@ -16,6 +16,7 @@ const loop_1 = require("./loop");
16
16
  const rules_1 = require("./rules");
17
17
  const skills_1 = require("./skills");
18
18
  const skills_tool_1 = require("../tools/skills-tool");
19
+ const background_1 = require("../tools/background");
19
20
  class Agent {
20
21
  config;
21
22
  provider;
@@ -49,6 +50,9 @@ class Agent {
49
50
  (0, apply_patch_1.createApplyPatchTool)(),
50
51
  // Hermes-style progressive-disclosure skills (list_skills / read_skill).
51
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)(),
52
56
  ]);
53
57
  }
54
58
  async run(request, signal) {
@@ -190,25 +190,68 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
190
190
  toolCalls: result.toolCalls,
191
191
  });
192
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
+ };
193
217
  try {
218
+ const batches = [];
219
+ let current = [];
194
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) {
195
234
  checkAbort();
196
- toolCalls++;
197
- callbacks.onToolCall?.(tc.name, tc.arguments);
198
- callbacks.onStatus?.(`Running ${tc.name}`);
199
- const toolResult = await tools.execute(tc.name, tc.arguments, {
200
- cwd,
201
- permissions,
202
- onActivity: callbacks.onActivity,
203
- maxOutput: 30_000,
204
- });
205
- callbacks.onToolResult?.(tc.name, toolResult.output, toolResult.data);
206
- messages.push({
207
- role: 'tool',
208
- toolCallId: tc.id,
209
- name: tc.name,
210
- content: toolResult.output.slice(0, 40_000),
211
- });
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
+ }
212
255
  }
213
256
  }
214
257
  catch {
@@ -8,6 +8,11 @@ exports.identityPrompt = identityPrompt;
8
8
  exports.taskPrompt = taskPrompt;
9
9
  exports.continuationPrompt = continuationPrompt;
10
10
  exports.SYSTEM_PROMPT = `You are VectorHead, an AI coding agent operating inside a terminal.
11
+ VectorHead is an independent product (npm package "agen-vektor", binary
12
+ "vector", config "~/.vector/") — it is not a fork, port, or rebrand of any
13
+ other agent product. When asked about your identity, version, or origin,
14
+ answer as VectorHead only; never attribute yourself to other agent tools or
15
+ npm packages.
11
16
 
12
17
  You work autonomously inside the user's project directory. You can:
13
18
  - Inspect the project structure and files
package/dist/cli/index.js CHANGED
@@ -185,7 +185,11 @@ async function runNonInteractive(opts) {
185
185
  // answer only arrives in result.content (never via onDelta).
186
186
  const reply = result.content.trim();
187
187
  if (reply) {
188
- process.stdout.write(reply + '\n');
188
+ // Freebuff appendInterruptionNotice: a user-interrupted run keeps its
189
+ // partial reply with the marker appended — non-interactive parity.
190
+ process.stdout.write(result.aborted && !reply.endsWith('[response interrupted]')
191
+ ? `${reply}\n\n[response interrupted]\n`
192
+ : reply + '\n');
189
193
  }
190
194
  else if (result.stopped || result.aborted) {
191
195
  console.log(`${terminal_1.ANSI.yellow}⏹ Stopped — no reply was produced.${terminal_1.ANSI.reset}`);
@@ -10,6 +10,7 @@ exports.customProviderPickerOptions = customProviderPickerOptions;
10
10
  exports.providerApiKey = providerApiKey;
11
11
  exports.isCustomProvider = isCustomProvider;
12
12
  exports.providerHeaders = providerHeaders;
13
+ exports.providerBody = providerBody;
13
14
  exports.providerConfigured = providerConfigured;
14
15
  const credentials_1 = require("./credentials");
15
16
  /** Sentinel provider id meaning "add a brand new custom provider". */
@@ -128,6 +129,22 @@ function providerHeaders(config, id) {
128
129
  }
129
130
  return Object.keys(out).length > 0 ? out : undefined;
130
131
  }
132
+ /**
133
+ * Resolve the extra request-body fields for a provider id (OpenCode-style
134
+ * per-provider options). Returns a shallow copy so callers cannot mutate the
135
+ * config; undefined when the definition carries no body overrides.
136
+ */
137
+ function providerBody(config, id) {
138
+ const raw = customProviderDef(config, id)?.options?.body;
139
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
140
+ return undefined;
141
+ const out = {};
142
+ for (const [k, v] of Object.entries(raw)) {
143
+ if (v !== undefined)
144
+ out[k] = v;
145
+ }
146
+ return Object.keys(out).length > 0 ? out : undefined;
147
+ }
131
148
  /**
132
149
  * Is a provider fully usable right now? Built-ins need a key (env or stored);
133
150
  * a legacy custom needs its apiUrl + key; a named custom needs a base URL in
@@ -19,6 +19,7 @@ class CustomProvider extends openai_compat_1.OpenAICompatProvider {
19
19
  defaultModel: cfg.model || 'default',
20
20
  contextWindow: 128000,
21
21
  headers: cfg.headers,
22
+ body: cfg.body,
22
23
  });
23
24
  this.protocol = cfg.protocol;
24
25
  }
@@ -72,6 +72,7 @@ function createProvider(config) {
72
72
  model: config.model,
73
73
  protocol: config.protocol || 'openai',
74
74
  headers: (0, providers_1.providerHeaders)(config, pid),
75
+ body: (0, providers_1.providerBody)(config, pid),
75
76
  });
76
77
  }
77
78
  }
@@ -83,6 +83,8 @@ class OpenAICompatProvider {
83
83
  defaultModel;
84
84
  contextWindow;
85
85
  extraHeaders;
86
+ /** Provider-level request-body defaults (shallow-copied at construction). */
87
+ extraBody;
86
88
  constructor(opts) {
87
89
  this.id = opts.id;
88
90
  this.name = opts.name;
@@ -91,6 +93,7 @@ class OpenAICompatProvider {
91
93
  this.defaultModel = opts.defaultModel || 'gpt-4o-mini';
92
94
  this.contextWindow = opts.contextWindow || 128000;
93
95
  this.extraHeaders = opts.headers || {};
96
+ this.extraBody = { ...(opts.body || {}) };
94
97
  }
95
98
  capabilities() {
96
99
  return {
@@ -136,6 +139,12 @@ class OpenAICompatProvider {
136
139
  if (params.tools && params.tools.length > 0) {
137
140
  body.tools = toToolDefs(params.tools);
138
141
  }
142
+ // Provider-level body defaults FIRST (options.body — e.g. BitDeer's
143
+ // reasoning_effort), then the call-level fields on top: an explicit
144
+ // per-call temperature/max_tokens overrides the provider default.
145
+ for (const [k, v] of Object.entries(this.extraBody)) {
146
+ body[k] = v;
147
+ }
139
148
  if (params.temperature !== undefined)
140
149
  body.temperature = params.temperature;
141
150
  if (params.maxTokens)
@@ -15,7 +15,7 @@ class OpenRouterProvider extends openai_compat_1.OpenAICompatProvider {
15
15
  }
16
16
  headers() {
17
17
  const h = super.headers();
18
- h['HTTP-Referer'] = 'https://github.com/clickmamaheti-prog/vector-agent';
18
+ h['HTTP-Referer'] = 'https://www.npmjs.com/package/agen-vektor';
19
19
  h['X-Title'] = 'VectorHead AI Coding Agent';
20
20
  return h;
21
21
  }
@@ -170,7 +170,7 @@ function createApplyPatchTool() {
170
170
  return {
171
171
  definition: {
172
172
  name: 'apply_patch',
173
- description: 'Apply a patch (unified diff, Freebuff/Codebuff format) to one or more files: "*** Update File: <path>" followed by "@@ -l,c +l,c @@" hunks with lines prefixed by " " (context), "-" (delete), "+" (add); or "*** Delete File: <path>". Creates the file if it does not exist. Safer than full rewrites for small changes. Requires explicit permission.',
173
+ description: 'Apply a patch (unified diff format) to one or more files: "*** Update File: <path>" followed by "@@ -l,c +l,c @@" hunks with lines prefixed by " " (context), "-" (delete), "+" (add); or "*** Delete File: <path>". Creates the file if it does not exist. Safer than full rewrites for small changes. Requires explicit permission.',
174
174
  parameters: {
175
175
  type: 'object',
176
176
  properties: {
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetBackgroundJobs = resetBackgroundJobs;
4
+ exports.createBackgroundTools = createBackgroundTools;
5
+ /**
6
+ * Background jobs — long-running shell commands that do NOT block the agent
7
+ * loop (Freebuff/Codebuff-style parallelism: start a dev server or a long
8
+ * test suite, keep working, poll the output, stop it when done).
9
+ *
10
+ * run_in_background → spawn now, return a job id immediately
11
+ * output_from_background → status + NEW output since the last poll
12
+ * stop_background_job → SIGKILL the job
13
+ *
14
+ * The command goes through the SAME permission policy as the shell tool
15
+ * (classifyCommand → decide). Jobs live for the lifetime of the process;
16
+ * output buffers are capped (tail is kept).
17
+ */
18
+ const node_child_process_1 = require("node:child_process");
19
+ const MAX_LOG = 1_000_000; // keep the TAIL of huge outputs
20
+ const jobs = new Map();
21
+ let nextId = 1;
22
+ /** Test helper: kill everything and reset ids. */
23
+ function resetBackgroundJobs() {
24
+ for (const job of jobs.values()) {
25
+ if (job.timeoutTimer)
26
+ clearTimeout(job.timeoutTimer);
27
+ killTree(job);
28
+ }
29
+ jobs.clear();
30
+ nextId = 1;
31
+ }
32
+ /** Kill the job's whole process tree — the shell AND its grandchildren. */
33
+ function killTree(job) {
34
+ const pid = job.child?.pid;
35
+ if (pid != null) {
36
+ try {
37
+ process.kill(-pid, 'SIGKILL'); // detached spawn ⇒ own process group
38
+ }
39
+ catch {
40
+ /* group already gone */
41
+ }
42
+ }
43
+ try {
44
+ job.child?.kill('SIGKILL');
45
+ }
46
+ catch {
47
+ /* already gone */
48
+ }
49
+ }
50
+ function trim(job) {
51
+ if (job.log.length > MAX_LOG) {
52
+ // keep the tail; shift the cursor so nothing unread is lost
53
+ const cut = job.log.length - MAX_LOG;
54
+ job.log = job.log.slice(cut);
55
+ job.readCursor = Math.max(0, job.readCursor - cut);
56
+ }
57
+ }
58
+ function describe(job) {
59
+ const secs = Math.round(((job.endedAt ?? Date.now()) - job.startedAt) / 1000);
60
+ const status = job.exitCode === null ? `RUNNING (${secs}s)` : job.killed ? `STOPPED after ${secs}s` : `EXITED ${job.exitCode} after ${secs}s`;
61
+ const fresh = job.log.slice(job.readCursor);
62
+ job.readCursor = job.log.length;
63
+ const tail = fresh.length > 20_000 ? `... [${fresh.length - 20_000} chars hidden] ${fresh.slice(-20_000)}` : fresh;
64
+ return `[${job.id}] ${status} — $ ${job.command}\n${tail || '(no output yet)'}`;
65
+ }
66
+ async function startJob(command, cwd, ctx, timeoutMs) {
67
+ const level = ctx.permissions.classifyCommand(command);
68
+ if (level === 'blocked') {
69
+ return {
70
+ output: `BLOCKED: command is not permitted by VectorHead security policy:\n${command}`,
71
+ summary: 'blocked by policy',
72
+ };
73
+ }
74
+ const allowed = await ctx.permissions.decide({ tool: 'shell', summary: `[background] ${command}`, level });
75
+ if (!allowed) {
76
+ return { output: `Permission denied by user:\n${command}`, summary: 'denied by user' };
77
+ }
78
+ const id = `bg${nextId++}`;
79
+ const child = (0, node_child_process_1.spawn)(command, {
80
+ cwd,
81
+ shell: '/bin/sh',
82
+ env: { ...process.env },
83
+ stdio: ['ignore', 'pipe', 'pipe'],
84
+ detached: true, // own process group → killTree can take down grandchildren
85
+ });
86
+ const job = {
87
+ id,
88
+ command,
89
+ child,
90
+ log: '',
91
+ readCursor: 0,
92
+ startedAt: Date.now(),
93
+ endedAt: null,
94
+ exitCode: null,
95
+ killed: false,
96
+ timeoutTimer: null,
97
+ };
98
+ jobs.set(id, job);
99
+ child.stdout?.on('data', (d) => {
100
+ job.log += d.toString();
101
+ trim(job);
102
+ });
103
+ child.stderr?.on('data', (d) => {
104
+ job.log += d.toString();
105
+ trim(job);
106
+ });
107
+ // 'exit' = the shell itself died; 'close' = shell AND stdio pipes closed.
108
+ // A grandchild (e.g. `sleep`) can hold the pipes open long after the shell
109
+ // is SIGKILLed, so the final status is recorded on 'exit' — not 'close'.
110
+ child.on('exit', (code) => {
111
+ if (job.exitCode === null)
112
+ job.exitCode = code ?? (job.killed ? -1 : 0);
113
+ if (job.endedAt === null)
114
+ job.endedAt = Date.now();
115
+ });
116
+ child.on('close', () => {
117
+ if (job.endedAt === null)
118
+ job.endedAt = Date.now();
119
+ job.child = null;
120
+ if (job.timeoutTimer)
121
+ clearTimeout(job.timeoutTimer);
122
+ });
123
+ child.on('error', (e) => {
124
+ job.log += `\n[spawn error] ${e.message}`;
125
+ trim(job);
126
+ job.exitCode = -1;
127
+ job.endedAt = Date.now();
128
+ job.child = null;
129
+ });
130
+ if (timeoutMs > 0) {
131
+ job.timeoutTimer = setTimeout(() => {
132
+ killTree(job);
133
+ }, timeoutMs);
134
+ }
135
+ return {
136
+ output: `Started background job ${id}: $ ${command}\nDo NOT wait for it — keep working. Check progress with output_from_background (id: "${id}", or omit for all jobs); stop it with stop_background_job.`,
137
+ summary: `[bg] started ${id}`,
138
+ data: { jobId: id },
139
+ };
140
+ }
141
+ function createBackgroundTools() {
142
+ return [
143
+ {
144
+ definition: {
145
+ name: 'run_in_background',
146
+ description: 'Start a shell command in the BACKGROUND and return immediately (non-blocking) — use for dev servers, watchers, long test suites, or anything that runs a while. You get a job id (e.g. bg1); keep working and poll progress with output_from_background, stop with stop_background_job. Do NOT use for quick commands — use shell instead.',
147
+ parameters: {
148
+ type: 'object',
149
+ properties: {
150
+ command: { type: 'string', description: 'The shell command to run in the background' },
151
+ timeout_ms: { type: 'number', description: 'Optional kill-after timeout in ms (0 or omitted = run until stopped)' },
152
+ },
153
+ required: ['command'],
154
+ },
155
+ },
156
+ async execute(args, ctx) {
157
+ const command = String(args.command || '').trim();
158
+ if (!command)
159
+ return { output: 'ERROR: empty command' };
160
+ ctx.onActivity?.('run_in_background', `[bg] $ ${command}`);
161
+ return startJob(command, ctx.cwd, ctx, Number(args.timeout_ms) || 0);
162
+ },
163
+ },
164
+ {
165
+ definition: {
166
+ name: 'output_from_background',
167
+ description: 'Read the status and NEW output of a background job started with run_in_background (everything since your last poll). Omit "id" to summarize ALL jobs. A RUNNING job may have partial output; EXITED <code> is finished.',
168
+ parameters: {
169
+ type: 'object',
170
+ properties: {
171
+ id: { type: 'string', description: 'Job id from run_in_background (e.g. "bg1"); omit for all jobs' },
172
+ },
173
+ required: [],
174
+ },
175
+ },
176
+ async execute(args, ctx) {
177
+ ctx.onActivity?.('output_from_background', 'polling background jobs');
178
+ if (jobs.size === 0)
179
+ return { output: 'No background jobs have been started.' };
180
+ const id = String(args.id || '').trim();
181
+ if (id) {
182
+ const job = jobs.get(id);
183
+ if (!job) {
184
+ return { output: `ERROR: unknown background job "${id}". Running/known jobs: ${[...jobs.keys()].join(', ') || '(none)'}` };
185
+ }
186
+ return { output: describe(job), summary: `[bg] poll ${id}`, data: { jobId: id, exitCode: job.exitCode } };
187
+ }
188
+ return { output: [...jobs.values()].map((j) => describe(j)).join('\n\n'), summary: `[bg] poll all (${jobs.size})` };
189
+ },
190
+ },
191
+ {
192
+ definition: {
193
+ name: 'stop_background_job',
194
+ description: 'Stop (SIGKILL) a background job started with run_in_background. Use when the server/test/watch is no longer needed or is stuck.',
195
+ parameters: {
196
+ type: 'object',
197
+ properties: {
198
+ id: { type: 'string', description: 'Job id from run_in_background (e.g. "bg1")' },
199
+ },
200
+ required: ['id'],
201
+ },
202
+ },
203
+ async execute(args, ctx) {
204
+ const id = String(args.id || '').trim();
205
+ const job = jobs.get(id);
206
+ if (!job)
207
+ return { output: `ERROR: unknown background job "${id}"` };
208
+ ctx.onActivity?.('stop_background_job', `stopping ${id}`);
209
+ job.killed = true;
210
+ if (job.timeoutTimer)
211
+ clearTimeout(job.timeoutTimer);
212
+ killTree(job);
213
+ return { output: `Stop signal sent to ${id} ($ ${job.command}). Poll output_from_background for its final output.`, summary: `[bg] stopped ${id}` };
214
+ },
215
+ },
216
+ ];
217
+ }
@@ -229,7 +229,7 @@ function createExtrasTools() {
229
229
  {
230
230
  definition: {
231
231
  name: 'task_completed',
232
- description: 'Signal that the task is fully done (Freebuff task_completed). Call ONLY after finishing and verifying all requested work. Takes an optional summary with verification results. The TUI shows a checkmark.',
232
+ description: 'Signal that the task is fully done. Call ONLY after finishing and verifying all requested work. Takes an optional summary with verification results. The TUI shows a checkmark.',
233
233
  parameters: {
234
234
  type: 'object',
235
235
  properties: {
@@ -327,7 +327,7 @@ function createExtrasTools() {
327
327
  {
328
328
  definition: {
329
329
  name: 'render_ui',
330
- description: 'Render an interactive terminal widget (Freebuff render_ui). Currently supports a "button" that opens a URL. Use when giving the user a direct link to click (docs, dashboard, generated report, deployment URL).',
330
+ description: 'Render an interactive terminal widget. Currently supports a "button" that opens a URL. Use when giving the user a direct link to click (docs, dashboard, generated report, deployment URL).',
331
331
  parameters: {
332
332
  type: 'object',
333
333
  properties: {