agy-mcp-bridge 0.1.8 → 0.1.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 +6 -2
- package/dist/src/config.js +3 -1
- package/dist/src/effort.js +32 -0
- package/dist/src/models.js +19 -8
- package/dist/src/runtime.js +1 -1
- package/dist/src/security/input.js +4 -3
- package/dist/src/tasks/manager.js +7 -3
- package/dist/src/tools/register.js +3 -3
- package/dist/src/ui/routes.js +12 -0
- package/package.json +1 -1
- package/skills/antigravity-bridge/SKILL.md +1 -1
- package/skills/known-versions.json +4 -0
- package/ui/app.js +24 -2
package/README.md
CHANGED
|
@@ -226,7 +226,7 @@ Luồng sử dụng trong Codex hoặc Claude Code:
|
|
|
226
226
|
|
|
227
227
|
1. Đọc code và Git status trước task; chia task nhỏ, rõ file được sửa và tiêu chí nghiệm thu.
|
|
228
228
|
2. Gọi `antigravity_delegate` với arguments mẫu ở dưới.
|
|
229
|
-
3. Lưu `task_id`; gọi `antigravity_task_wait` (tối đa
|
|
229
|
+
3. Lưu `task_id`; gọi `antigravity_task_wait` (mỗi lần chờ tối đa `max_wait_seconds`, mặc định 55 giây, trả sớm khi task xong) hoặc `antigravity_task_status`, rồi lấy `antigravity_task_output`.
|
|
230
230
|
4. Nếu client bị đóng, bật lại client, gọi environment để xem `recent_tasks`, rồi query task ID cũ. Không giao trùng task khi chưa kiểm tra trạng thái.
|
|
231
231
|
5. Tech Lead review diff thực tế, gồm file untracked, chạy test và đánh giá từng tiêu chí. Nếu chưa đạt, giao task sửa tiếp.
|
|
232
232
|
|
|
@@ -244,7 +244,7 @@ Luồng sử dụng trong Codex hoặc Claude Code:
|
|
|
244
244
|
| `antigravity_environment` | CLI/version/allowlist/giới hạn và recent_tasks để tìm lại task ID |
|
|
245
245
|
| `antigravity_delegate` | Nhận task, workspace, acceptance_criteria, context?, timeout_seconds?; trả `running` (hoặc `queued` nếu bridge đang bận và hàng đợi còn chỗ) cùng task_id |
|
|
246
246
|
| `antigravity_task_status` | Trạng thái/process/thời gian/lỗi/denied_actions/số byte theo task_id, không kèm log (rẻ để poll) |
|
|
247
|
-
| `antigravity_task_wait` | Chờ tối đa `wait_seconds` (1
|
|
247
|
+
| `antigravity_task_wait` | Chờ tối đa `wait_seconds` (1 đến `max_wait_seconds`, mặc định là mức tối đa, 55 giây) tới khi task kết thúc, trả cùng nội dung như status; còn chạy thì gọi lại |
|
|
248
248
|
| `antigravity_task_output` | Phản hồi cuối, stdout/stderr, lỗi, denied_actions, truncation theo task_id |
|
|
249
249
|
| `antigravity_cancel_task` | Hủy cây tiến trình thuộc task; không rollback thay đổi file |
|
|
250
250
|
|
|
@@ -376,6 +376,10 @@ Lưu ý bảo mật: nội dung yêu cầu và phản hồi được hiển th
|
|
|
376
376
|
|
|
377
377
|
**Ghi chú phát hành:** mỗi lần workflow phát hành, nó tạo một GitHub Release cho tag `vX.Y.Z` với ghi chú tự sinh gồm các pull request đã merge kể từ tag trước (tên PR là nội dung ghi chú, nên hãy đặt tên PR nêu rõ thay đổi). Xem tại trang Releases của repo. Các tag cũ chưa có release cũng được tạo bù, theo thứ tự từ cũ đến mới.
|
|
378
378
|
|
|
379
|
+
**Mức suy luận (effort):** mục Cài đặt → Mức suy luận chọn `--effort` truyền cho Antigravity: thấp (nhanh nhất), vừa, cao, rất cao, tối đa, hoặc mặc định (không truyền cờ). Mức thấp giúp task nhanh hơn nhưng suy luận ít hơn. Có hiệu lực ngay cho task mới, mỗi task ghi lại mức đã dùng, lưu ở `.bridge-state/effort.json`. Chưa kiểm chứng với `agy` thật ở chế độ `--print`. Kết hợp chọn model nhỏ hơn (ví dụ `gemini-3.8-flash-low`) để tăng tốc.
|
|
380
|
+
|
|
381
|
+
**Giảm số lần Codex/Claude phải hỏi lại (tiết kiệm token):** mỗi lần gọi `antigravity_task_wait` tốn token, nên nên chờ lâu trong một lần gọi thay vì nhiều lần ngắn. Server không thể tự "đánh thức" Codex/Claude khi có kết quả: thông báo MCP từ server không khiến mô hình chạy, mô hình chỉ hành động khi được nhắn hoặc khi nó gọi tool. Vì vậy bridge cho chờ dài: mặc định một lần chờ tối đa 55 giây (nhỏ hơn thời hạn tool 60 giây mặc định của Codex). Muốn chờ lâu hơn nữa, nâng đồng thời hai giá trị: `max_wait_seconds` trong file cấu hình của bridge (5 đến 3600) và thời hạn tool của client (Codex: `tool_timeout_sec` trong `config.toml`; Claude Code: biến môi trường `MCP_TOOL_TIMEOUT` tính bằng mili giây, theo tài liệu của Claude Code, chưa kiểm chứng ở đây). Ví dụ `max_wait_seconds: 540` cùng `tool_timeout_sec = 600` thì một lần gọi chờ tới 9 phút. Nếu thời hạn tool của client ngắn hơn `max_wait_seconds`, lần chờ dài sẽ bị client cắt và báo lỗi timeout. File skill dặn Codex/Claude gọi `wait` không kèm `wait_seconds` và không báo tiến độ giữa các lần chờ.
|
|
382
|
+
|
|
379
383
|
**Quyền của Antigravity:** mục Cài đặt → Quyền của Antigravity chọn Antigravity được tự làm đến đâu khi chạy task. Ở chế độ không tương tác, Antigravity tự từ chối việc cần phê duyệt (thấy ở `denied_actions`), nên task sửa file hay chạy lệnh có thể không làm được nếu cài đặt Antigravity chưa cấp quyền. Năm mức, từ thấp đến cao:
|
|
380
384
|
|
|
381
385
|
| Mức | Cờ truyền cho `agy` | Ý nghĩa |
|
package/dist/src/config.js
CHANGED
|
@@ -11,7 +11,9 @@ const schema = z.object({
|
|
|
11
11
|
max_retained_tasks: z.number().int().min(1).max(200).default(50),
|
|
12
12
|
max_queued_tasks: z.number().int().min(0).max(20).default(3),
|
|
13
13
|
extra_env_passthrough: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).max(100).default([]),
|
|
14
|
-
prompt_transport: z.enum(['stdin', 'argv']).default('argv')
|
|
14
|
+
prompt_transport: z.enum(['stdin', 'argv']).default('argv'),
|
|
15
|
+
/** Longest single antigravity_task_wait call (default 55). Keep it below the MCP client's tool timeout. */
|
|
16
|
+
max_wait_seconds: z.number().int().min(5).max(3600).optional()
|
|
15
17
|
}).strict();
|
|
16
18
|
const DEFAULT_LIMITS = { max_timeout_seconds: 900, default_timeout_seconds: 300, max_output_bytes: 262144, max_retained_tasks: 50, max_queued_tasks: 3, extra_env_passthrough: [], prompt_transport: 'argv' };
|
|
17
19
|
// Directories allowed for this run only (agy-mcp-bridge --workspace <dir>); never written to the config file.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Reasoning effort passed to Antigravity as `--effort`. Lower is faster and thinks less. `default` passes no flag, so Antigravity's own
|
|
6
|
+
* setting applies. Chosen by the bridge owner in the web UI; read on every task, so a change needs no restart.
|
|
7
|
+
*/
|
|
8
|
+
export const EFFORT_LEVELS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
9
|
+
export const EFFORT_FILE = 'effort.json';
|
|
10
|
+
export const isEffortLevel = (value) => typeof value === 'string' && EFFORT_LEVELS.includes(value);
|
|
11
|
+
export const effortFlags = (level) => level === 'default' ? [] : ['--effort', level];
|
|
12
|
+
/** Anything missing, unreadable or unknown means `default` (no flag). */
|
|
13
|
+
export function readEffort(stateDirectory) {
|
|
14
|
+
try {
|
|
15
|
+
const level = JSON.parse(readFileSync(path.join(stateDirectory, EFFORT_FILE), 'utf8')).effort;
|
|
16
|
+
return isEffortLevel(level) ? level : 'default';
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 'default';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function writeEffort(stateDirectory, level) {
|
|
23
|
+
const file = path.join(stateDirectory, EFFORT_FILE);
|
|
24
|
+
if (level === 'default') {
|
|
25
|
+
await rm(file, { force: true });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
29
|
+
await writeFile(`${file}.tmp`, JSON.stringify({ effort: level }) + '\n');
|
|
30
|
+
await rename(`${file}.tmp`, file);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=effort.js.map
|
package/dist/src/models.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { startProcess, terminateOwned } from './antigravity/process.js';
|
|
4
|
+
import { redact } from './security/redact.js';
|
|
4
5
|
export const MODEL_FILE = 'model.json';
|
|
5
6
|
// Ids go straight into the agy command line, so only plain identifier characters are ever accepted (and never a leading dash).
|
|
6
7
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/;
|
|
7
8
|
export const validModelId = (value) => typeof value === 'string' && MODEL_ID.test(value);
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Parses `agy models`: one model per line, the id first, then the display name. In a terminal the columns are padded with spaces; when
|
|
11
|
+
* the output is piped (as the bridge reads it) they are separated by a single tab, and a progress line ("Fetching available models...") comes first.
|
|
12
|
+
*/
|
|
9
13
|
export function parseModels(text) {
|
|
10
14
|
const models = [];
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
// eslint-disable-next-line no-control-regex
|
|
16
|
+
for (const line of text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
|
|
17
|
+
const match = line.match(/^(\S+)(?:[ ]*\t[ \t]*|[ ]{2,})(\S.*?)\s*$/);
|
|
13
18
|
if (match && validModelId(match[1]) && !models.some(model => model.id === match[1]))
|
|
14
19
|
models.push({ id: match[1], label: match[2].slice(0, 100) });
|
|
15
20
|
}
|
|
@@ -20,19 +25,25 @@ export function listModels(executable, cwd, extraEnvPassthrough = []) {
|
|
|
20
25
|
return new Promise((resolve, reject) => {
|
|
21
26
|
const child = startProcess(executable, ['models'], cwd, { extraEnvPassthrough });
|
|
22
27
|
let text = '';
|
|
28
|
+
let errors = '';
|
|
23
29
|
let timedOut = false;
|
|
24
30
|
const timer = setTimeout(() => { timedOut = true; void terminateOwned(child).catch(() => { }); }, 10000);
|
|
25
|
-
|
|
26
|
-
text += chunk.toString().slice(0, 32768 - text.length); };
|
|
27
|
-
child.
|
|
31
|
+
child.stdout.on('data', (chunk) => { if (text.length < 32768)
|
|
32
|
+
text += chunk.toString().slice(0, 32768 - text.length); });
|
|
33
|
+
child.stderr.on('data', (chunk) => { if (errors.length < 4096)
|
|
34
|
+
errors += chunk.toString().slice(0, 4096 - errors.length); });
|
|
28
35
|
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
29
36
|
child.once('close', code => {
|
|
30
37
|
clearTimeout(timer);
|
|
31
38
|
const models = parseModels(text);
|
|
32
39
|
if (timedOut)
|
|
33
40
|
reject(new Error('`agy models` timed out'));
|
|
34
|
-
else if (code !== 0 || !models.length)
|
|
35
|
-
|
|
41
|
+
else if (code !== 0 || !models.length) {
|
|
42
|
+
// Say what came back, so a changed output format is visible in the web UI instead of a bare "did not return a model list".
|
|
43
|
+
const firstLines = (value) => value.trim().split(/\r?\n/).slice(0, 2).join(' | ');
|
|
44
|
+
const seen = [firstLines(text), firstLines(errors)].filter(Boolean).join(' | ').replace(/\s+/g, ' ').slice(0, 240);
|
|
45
|
+
reject(new Error(`\`agy models\` did not return a model list (exit code ${code}${seen ? `; output: ${redact(seen)}` : '; no output'})`));
|
|
46
|
+
}
|
|
36
47
|
else
|
|
37
48
|
resolve(models);
|
|
38
49
|
});
|
package/dist/src/runtime.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function createRuntime() {
|
|
|
30
30
|
supported: { delegate: cli.supported, status: true, wait: true, output: true, cancel_owned_process: true, detached_resume: false },
|
|
31
31
|
allowed_workspaces: config.allowed_workspaces, session_workspaces: sessionOnlyWorkspaces(),
|
|
32
32
|
authentication: 'Cached CLI authentication required; only a live task verifies it.',
|
|
33
|
-
limits: { max_timeout_seconds: config.max_timeout_seconds, max_output_bytes_per_stream: config.max_output_bytes, max_retained_tasks: config.max_retained_tasks, concurrent_tasks: 1, max_queued_tasks: config.max_queued_tasks },
|
|
33
|
+
limits: { max_timeout_seconds: config.max_timeout_seconds, max_output_bytes_per_stream: config.max_output_bytes, max_retained_tasks: config.max_retained_tasks, concurrent_tasks: 1, max_queued_tasks: config.max_queued_tasks, max_wait_seconds: manager.maxWaitSeconds },
|
|
34
34
|
limitations: [
|
|
35
35
|
'Only agy 1.4.x with all required flags is enabled; real agent execution was verified on 1.4.3.',
|
|
36
36
|
...cli.warnings,
|
|
@@ -7,9 +7,10 @@ export const delegateSchema = z.object({
|
|
|
7
7
|
timeout_seconds: z.number().int().min(1).max(900).optional()
|
|
8
8
|
}).strict().refine(input => JSON.stringify(input).length <= 24000 && !JSON.stringify(input).includes('\\u0000'), 'Combined task data must fit 24000 characters and contain no NUL characters');
|
|
9
9
|
export const taskIdSchema = z.object({ task_id: z.uuid() }).strict();
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
export const
|
|
10
|
+
// A wait must end before the client's tool timeout (60s by default in Codex), so the default cap sits just below it. Raise `max_wait_seconds`
|
|
11
|
+
// in the config together with the client's tool timeout to let one call cover a long task (fewer calls, fewer tokens).
|
|
12
|
+
export const DEFAULT_MAX_WAIT_SECONDS = 55;
|
|
13
|
+
export const MAX_WAIT_SECONDS = 3600;
|
|
13
14
|
export const waitSchema = z.object({ task_id: z.uuid(), wait_seconds: z.number().int().min(1).max(MAX_WAIT_SECONDS).optional() }).strict();
|
|
14
15
|
export function promptFor(input, client) {
|
|
15
16
|
return [
|
|
@@ -2,13 +2,14 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { delegateSchema, promptFor } from '../security/input.js';
|
|
5
|
+
import { DEFAULT_MAX_WAIT_SECONDS, delegateSchema, promptFor } from '../security/input.js';
|
|
6
6
|
import { validateWorkspace } from '../security/workspace.js';
|
|
7
7
|
import { redact } from '../security/redact.js';
|
|
8
8
|
import { delegationState } from '../delegation.js';
|
|
9
9
|
import { startProcess, terminateOwned } from '../antigravity/process.js';
|
|
10
10
|
import { invocationFor, finalEnvelope } from '../antigravity/invocation.js';
|
|
11
11
|
import { helperDirectory } from '../paths.js';
|
|
12
|
+
import { effortFlags, readEffort } from '../effort.js';
|
|
12
13
|
import { lowerLevel, permissionFlags, permissionState } from '../permissions.js';
|
|
13
14
|
import { listModels, readModel } from '../models.js';
|
|
14
15
|
import { pidAlive, recoverExecutionLock } from './execution-lock.js';
|
|
@@ -74,7 +75,8 @@ export class TaskManager {
|
|
|
74
75
|
context: input.context === undefined ? null : redact(input.context), timeout_seconds: timeout,
|
|
75
76
|
client: meta.client ? redact(meta.client).slice(0, 100) : null,
|
|
76
77
|
model: await readModel(this.runtime.stateDirectory),
|
|
77
|
-
permission: permissionState(this.runtime.stateDirectory).level
|
|
78
|
+
permission: permissionState(this.runtime.stateDirectory).level,
|
|
79
|
+
effort: readEffort(this.runtime.stateDirectory)
|
|
78
80
|
}
|
|
79
81
|
};
|
|
80
82
|
this.entries.set(task.task_id, entry);
|
|
@@ -149,7 +151,7 @@ export class TaskManager {
|
|
|
149
151
|
const permission = lowerLevel(entry.request?.permission ?? 'default', permissionState(this.runtime.stateDirectory).level);
|
|
150
152
|
if (entry.request)
|
|
151
153
|
entry.request.permission = permission;
|
|
152
|
-
const invocation = invocationFor(promptFor({ ...input, workspace }, entry.request?.client), timeout, this.config.prompt_transport, entry.request?.model ?? null, permissionFlags(permission));
|
|
154
|
+
const invocation = invocationFor(promptFor({ ...input, workspace }, entry.request?.client), timeout, this.config.prompt_transport, entry.request?.model ?? null, [...permissionFlags(permission), ...effortFlags(entry.request?.effort ?? 'default')]);
|
|
153
155
|
const options = { extraEnvPassthrough: this.config.extra_env_passthrough, stdin: invocation.stdin };
|
|
154
156
|
const child = this.runtime.launch ? this.runtime.launch(invocation.args, workspace, options) : startProcess(this.runtime.executable, invocation.args, workspace, options);
|
|
155
157
|
entry.child = child;
|
|
@@ -340,6 +342,8 @@ export class TaskManager {
|
|
|
340
342
|
const lastOutputAt = this.entries.get(id)?.lastOutputAt;
|
|
341
343
|
return { task, request, prompt, activity: { last_output_at: lastOutputAt ? new Date(lastOutputAt).toISOString() : null } };
|
|
342
344
|
}
|
|
345
|
+
/** Longest single wait a client may ask for. */
|
|
346
|
+
get maxWaitSeconds() { return this.config.max_wait_seconds ?? DEFAULT_MAX_WAIT_SECONDS; }
|
|
343
347
|
/** Bounded long-poll: returns as soon as the task ends, or after `seconds` if it is still running. */
|
|
344
348
|
async waitFor(id, seconds) {
|
|
345
349
|
const entry = this.entries.get(id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLIENT_INFO_META_KEY, McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { requestContext } from '../request-context.js';
|
|
3
3
|
import * as z from 'zod/v4';
|
|
4
|
-
import {
|
|
4
|
+
import { delegateSchema, taskIdSchema, waitSchema } from '../security/input.js';
|
|
5
5
|
import { redact } from '../security/redact.js';
|
|
6
6
|
import { readPackageInfo } from '../update.js';
|
|
7
7
|
/** Best-effort label for who delegated: the client's declared name/version when the request carries it, else the HTTP User-Agent. */
|
|
@@ -36,10 +36,10 @@ export function createServer(manager, environment) {
|
|
|
36
36
|
inputSchema: taskIdSchema, annotations: { readOnlyHint: true, openWorldHint: false }
|
|
37
37
|
}, async ({ task_id }) => guard(() => manager.summary(task_id)));
|
|
38
38
|
server.registerTool('antigravity_task_wait', {
|
|
39
|
-
description: `Wait up to wait_seconds (1-${
|
|
39
|
+
description: `Wait up to wait_seconds (1-${manager.maxWaitSeconds}, default ${manager.maxWaitSeconds}) for the task to finish, then return its status without logs. Returns early when the task ends; if still running, call again. Does not cancel the task.`,
|
|
40
40
|
inputSchema: waitSchema, annotations: { readOnlyHint: true, openWorldHint: false }
|
|
41
41
|
}, async ({ task_id, wait_seconds }) => guard(async () => {
|
|
42
|
-
await manager.waitFor(task_id, wait_seconds ??
|
|
42
|
+
await manager.waitFor(task_id, Math.min(wait_seconds ?? manager.maxWaitSeconds, manager.maxWaitSeconds));
|
|
43
43
|
return manager.summary(task_id);
|
|
44
44
|
}));
|
|
45
45
|
server.registerTool('antigravity_task_output', {
|
package/dist/src/ui/routes.js
CHANGED
|
@@ -7,6 +7,7 @@ import { paths } from '../paths.js';
|
|
|
7
7
|
import { knownUpdate, readPackageInfo } from '../update.js';
|
|
8
8
|
import { workspaceChanges } from '../changes.js';
|
|
9
9
|
import { validModelId, writeModel } from '../models.js';
|
|
10
|
+
import { EFFORT_LEVELS, isEffortLevel, readEffort, writeEffort } from '../effort.js';
|
|
10
11
|
import { PERMISSION_LEVELS, isPermissionLevel, needsAcknowledgement, permissionState, writePermission } from '../permissions.js';
|
|
11
12
|
import { inside } from '../security/workspace.js';
|
|
12
13
|
const assets = {
|
|
@@ -55,6 +56,7 @@ async function overview(ctx) {
|
|
|
55
56
|
delegation: { ...state, controllable: delegationControllable() },
|
|
56
57
|
model: await ctx.manager.currentModel(),
|
|
57
58
|
permission: permissionState(ctx.manager.stateDirectory),
|
|
59
|
+
effort: readEffort(ctx.manager.stateDirectory),
|
|
58
60
|
queue: ctx.manager.overview().queue,
|
|
59
61
|
warnings: [await ctx.manager.lockDiagnostic()].filter(Boolean),
|
|
60
62
|
update: knownUpdate() && { ...knownUpdate(), package: readPackageInfo().name, command: 'agy-mcp-bridge update' },
|
|
@@ -166,6 +168,16 @@ export async function handleUi(req, res, ctx) {
|
|
|
166
168
|
json(res, 200, { ...delegationState(ctx.manager.stateDirectory), controllable: true });
|
|
167
169
|
return true;
|
|
168
170
|
}
|
|
171
|
+
if (pathname === '/ui/api/effort' && method === 'POST') {
|
|
172
|
+
const body = await readJson(req);
|
|
173
|
+
if (!isEffortLevel(body.effort)) {
|
|
174
|
+
json(res, 400, { error: `effort must be one of: ${EFFORT_LEVELS.join(', ')}` });
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
await writeEffort(ctx.manager.stateDirectory, body.effort);
|
|
178
|
+
json(res, 200, { effort: body.effort });
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
169
181
|
if (pathname === '/ui/api/permission' && method === 'POST') {
|
|
170
182
|
const body = await readJson(req);
|
|
171
183
|
if (!isPermissionLevel(body.level)) {
|
package/package.json
CHANGED
|
@@ -58,7 +58,7 @@ Replace example paths and symbols with the user's actual authorized project. `co
|
|
|
58
58
|
|
|
59
59
|
## Observe and reconnect
|
|
60
60
|
|
|
61
|
-
Prefer `antigravity_task_wait` with `{"task_id":"the-returned-UUID",
|
|
61
|
+
Prefer `antigravity_task_wait` with `{"task_id":"the-returned-UUID"}` and no `wait_seconds` (or the largest value the tool description allows, also `limits.max_wait_seconds` in the environment): it returns as soon as the task ends, or at that limit with `running`, so call it again while the task is within its deadline. Every call costs tokens, so use few long waits and do not narrate or call `antigravity_task_status` between them; the user can watch progress in the web UI. If a wait fails with a client timeout, the client's tool timeout is shorter than the bridge's wait limit: tell the user once (raise the client's tool timeout or lower `max_wait_seconds`) and continue with shorter `wait_seconds`. `antigravity_task_status` is an instant, log-free snapshot for quick checks. Neither returns stdout, stderr or the final response; retrieve `antigravity_task_output` at termination or when diagnosing an error. Output/status calls return snapshots, not a completion wait.
|
|
62
62
|
|
|
63
63
|
Client disconnect does not cancel an accepted task. Reconnect to the same running server and query the saved ID; `recent_tasks` in environment can recover IDs if the delegate reply was lost. If several tasks could match, inspect their status/output before deciding which is yours. Do not resubmit just because a response was lost: submission is not idempotent. Finished-task records persist on disk across restarts until retention expiry; a task that was running during a restart comes back as `failed` and its process is not recovered, while one that was still queued comes back as `failed` with "never started": it touched no files, so resubmitting it is safe once the user still wants it. Unknown/expired ID does not prove the task never ran; inspect workspace changes and reconcile before another submission.
|
|
64
64
|
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
{
|
|
33
33
|
"sha256": "65c0a72bb016ee8d891bfea17ea5d972dc79dc7c12c57edb17c1e6e8bbd37078",
|
|
34
34
|
"note": "Antigravity permission level is the owner's setting (agy_permissions)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"sha256": "066f499ccd7341c7f77c2a409b3632e4cefe16e45ed2c77e75c8154028e951f9",
|
|
38
|
+
"note": "long waits (limits.max_wait_seconds), fewer polling calls"
|
|
35
39
|
}
|
|
36
40
|
]
|
|
37
41
|
}
|
package/ui/app.js
CHANGED
|
@@ -175,6 +175,18 @@ async function setPermission(level) {
|
|
|
175
175
|
try { await api('permission', {method: 'POST', body: {level, acknowledge_risk: risky}}); notify('Đã đổi quyền cho các task mới'); await refresh(); }
|
|
176
176
|
catch (error) { notify(error.message); await refresh(); }
|
|
177
177
|
}
|
|
178
|
+
const EFFORT_LEVELS = [
|
|
179
|
+
{id: 'default', title: 'Mặc định của Antigravity', text: 'Không truyền --effort; Antigravity dùng mức đã cấu hình.'},
|
|
180
|
+
{id: 'low', title: 'Thấp (nhanh nhất)', text: 'Suy luận ít nhất, phản hồi nhanh. Hợp việc nhỏ, rõ ràng.'},
|
|
181
|
+
{id: 'medium', title: 'Vừa', text: 'Cân bằng giữa tốc độ và độ kỹ.'},
|
|
182
|
+
{id: 'high', title: 'Cao', text: 'Suy luận kỹ hơn, chậm hơn.'},
|
|
183
|
+
{id: 'xhigh', title: 'Rất cao', text: 'Rất kỹ, khá chậm.'},
|
|
184
|
+
{id: 'max', title: 'Tối đa (chậm nhất)', text: 'Suy luận sâu nhất, chậm nhất.'}
|
|
185
|
+
];
|
|
186
|
+
async function setEffort(effort) {
|
|
187
|
+
try { await api('effort', {method: 'POST', body: {effort}}); notify('Đã đổi mức suy luận cho các task mới'); await refresh(); }
|
|
188
|
+
catch (error) { notify(error.message); await refresh(); }
|
|
189
|
+
}
|
|
178
190
|
async function copy(text) {
|
|
179
191
|
try { await navigator.clipboard.writeText(text); notify('Đã sao chép'); } catch { notify('Không thể sao chép tự động'); }
|
|
180
192
|
}
|
|
@@ -293,7 +305,7 @@ function detailView() {
|
|
|
293
305
|
const facts = h('dl', {class: 'kv'},
|
|
294
306
|
...[['Gửi lúc', clock(task.queued_at || task.started_at)], ['Bắt đầu chạy', queued ? 'Chưa chạy' : clock(task.started_at)], ['Kết thúc', clock(task.ended_at)],
|
|
295
307
|
['Thời gian chạy', running ? elapsedEl(task.started_at) : queued ? '—' : duration(task.duration_ms)],
|
|
296
|
-
['Giới hạn', request ? `${request.timeout_seconds}s` : '—'], ['Model', request && 'model' in request ? request.model || 'mặc định' : '—'], ['Quyền', request && request.permission ? (PERMISSION_LEVELS.find(l => l.id === request.permission)?.title || request.permission) : '—'], ['Exit code', task.exit_code ?? '—'], ['PID', task.pid ?? '—'],
|
|
308
|
+
['Giới hạn', request ? `${request.timeout_seconds}s` : '—'], ['Model', request && 'model' in request ? request.model || 'mặc định' : '—'], ['Quyền', request && request.permission ? (PERMISSION_LEVELS.find(l => l.id === request.permission)?.title || request.permission) : '—'], ['Effort', request && request.effort ? request.effort : '—'], ['Exit code', task.exit_code ?? '—'], ['PID', task.pid ?? '—'],
|
|
297
309
|
['CLI status', task.cli_status ?? '—']].map(([k, v]) => h('div', {}, h('dt', {text: k}), h('dd', {}, typeof v === 'string' || typeof v === 'number' ? String(v) : v))));
|
|
298
310
|
|
|
299
311
|
const asker = request?.client ? `Tech Lead · ${request.client}` : 'Tech Lead (Codex / Claude)';
|
|
@@ -360,6 +372,15 @@ function modelCard(o) {
|
|
|
360
372
|
loaded?.error ? h('div', {class: 'callout warn', text: `Không lấy được danh sách model: ${loaded.error}`}) : null,
|
|
361
373
|
h('p', {text: 'Áp dụng cho task mới, không cần khởi động lại. Task đang chạy hoặc đang chờ giữ model đã chốt lúc nhận. Danh sách lấy từ `agy models`; model được ghi lại trong từng task.'}));
|
|
362
374
|
}
|
|
375
|
+
function effortCard(o) {
|
|
376
|
+
const current = EFFORT_LEVELS.find(level => level.id === (o.effort || 'default')) || EFFORT_LEVELS[0];
|
|
377
|
+
const select = h('select', {class: 'select', 'aria-label': 'Mức suy luận cho task mới', onchange: event => setEffort(event.target.value)},
|
|
378
|
+
EFFORT_LEVELS.map(level => h('option', {value: level.id, text: level.title})));
|
|
379
|
+
select.value = current.id;
|
|
380
|
+
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Mức suy luận (effort)'}),
|
|
381
|
+
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: current.title}), h('span', {class: 'muted', text: current.text})), select),
|
|
382
|
+
h('p', {text: 'Truyền --effort cho Antigravity. Mức thấp giúp task nhanh hơn nhưng suy luận ít hơn. Áp dụng cho task mới, không cần khởi động lại; mỗi task ghi lại mức đã dùng. Chưa kiểm chứng với agy thật ở chế độ --print.'}));
|
|
383
|
+
}
|
|
363
384
|
function permissionCard(o) {
|
|
364
385
|
const p = o.permission || {level: 'default', controllable: true};
|
|
365
386
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Quyền của Antigravity'}),
|
|
@@ -382,6 +403,7 @@ function settingsView(o) {
|
|
|
382
403
|
delegation.controllable ? h('p', {text: 'Tắt để buộc Codex / Claude tự làm. Task đang chạy hoặc đang chờ không bị dừng, nhưng task đang chờ sẽ bị hủy khi đến lượt. Có hiệu lực ngay, không cần khởi động lại.'})
|
|
383
404
|
: h('div', {class: 'callout warn', text: 'Đang bị khóa bởi biến môi trường BRIDGE_DELEGATION. Đổi biến rồi khởi động lại server để mở.'})),
|
|
384
405
|
modelCard(o),
|
|
406
|
+
effortCard(o),
|
|
385
407
|
permissionCard(o),
|
|
386
408
|
h('section', {class: 'card panel'}, h('h2', {text: 'Kết nối'}),
|
|
387
409
|
h('div', {class: 'copy-row'}, h('code', {text: o.server.mcp_endpoint}), h('button', {class: 'btn small', onclick: () => copy(o.server.mcp_endpoint), text: 'Sao chép'})),
|
|
@@ -462,7 +484,7 @@ function patch() {
|
|
|
462
484
|
if (!rebuilt) patchLogs();
|
|
463
485
|
} else {
|
|
464
486
|
if (!state.models && !state.modelsLoading) loadModels();
|
|
465
|
-
setRegion('settings', JSON.stringify([state.models, o.model, o.permission, o.cli, o.delegation, o.limits, o.allowed_workspaces, o.session_workspaces, o.server.mcp_endpoint]), () => settingsView(o));
|
|
487
|
+
setRegion('settings', JSON.stringify([state.models, o.model, o.permission, o.effort, o.cli, o.delegation, o.limits, o.allowed_workspaces, o.session_workspaces, o.server.mcp_endpoint]), () => settingsView(o));
|
|
466
488
|
}
|
|
467
489
|
tick();
|
|
468
490
|
}
|