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 +2 -8
- package/dist/agent/agent.js +4 -0
- package/dist/agent/loop.js +59 -16
- package/dist/agent/prompts.js +5 -0
- package/dist/cli/index.js +5 -1
- package/dist/config/providers.js +17 -0
- package/dist/providers/custom.js +1 -0
- package/dist/providers/factory.js +1 -0
- package/dist/providers/openai-compat.js +9 -0
- package/dist/providers/openrouter.js +1 -1
- package/dist/tools/apply-patch.js +1 -1
- package/dist/tools/background.js +217 -0
- package/dist/tools/extras.js +2 -2
- package/dist/tui/app.js +132 -21
- package/dist/tui/chat.js +219 -35
- package/dist/tui/statusbar.js +35 -15
- package/dist/tui/theme.js +5 -1
- package/dist/tui/themes.js +2 -0
- package/package.json +1 -9
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
|
-
|
|
60
|
-
|
|
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
|
|
package/dist/agent/agent.js
CHANGED
|
@@ -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) {
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 {
|
package/dist/agent/prompts.js
CHANGED
|
@@ -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
|
-
|
|
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}`);
|
package/dist/config/providers.js
CHANGED
|
@@ -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
|
package/dist/providers/custom.js
CHANGED
|
@@ -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://
|
|
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
|
|
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
|
+
}
|
package/dist/tools/extras.js
CHANGED
|
@@ -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
|
|
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
|
|
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: {
|