@luchenglong/cli-bridge 0.0.0-stage → 0.2.2
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/CHANGELOG.md +148 -0
- package/LICENSE +21 -0
- package/MCP-POLICY.md +53 -0
- package/PROVIDER-CONTRACT.md +235 -0
- package/README.en.md +98 -0
- package/README.md +150 -2
- package/assets/indicator-states.svg +62 -0
- package/bin/agy-quota.mjs +223 -0
- package/core/bridge-kit.mjs +549 -0
- package/core/scheduling.mjs +149 -0
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/FALLBACK-AND-INDICATOR.md +93 -0
- package/docs/INSTALL.md +118 -0
- package/docs/MCP-POLICY.zh.md +49 -0
- package/docs/MIGRATION.md +43 -0
- package/docs/RELEASE-SOP.md +77 -0
- package/docs/history/agy/CHANGELOG.md +326 -0
- package/docs/history/codebuddy/CHANGELOG.md +668 -0
- package/docs/history/mimo/CHANGELOG.md +346 -0
- package/dynamic/agy.host.js +1237 -0
- package/dynamic/agy.host.template.mjs +140 -0
- package/dynamic/codebuddy.host.js +1787 -0
- package/dynamic/codebuddy.host.template.mjs +162 -0
- package/dynamic/mimo.host.js +1732 -0
- package/dynamic/mimo.host.template.mjs +169 -0
- package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
- package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
- package/mcp/agy-mcp-server.mjs +6 -0
- package/mcp/cli-bridge-mcp.mjs +184 -0
- package/mcp/codebuddy-mcp-server.mjs +6 -0
- package/mcp/mimo-mcp-server.mjs +6 -0
- package/mcp/providers/agy.mjs +254 -0
- package/mcp/providers/codebuddy.mjs +397 -0
- package/mcp/providers/mimo.mjs +290 -0
- package/package.json +87 -4
- package/preset/agy-first/agent.cordis.yml +141 -0
- package/preset/agy-first/cli-bridge.mjs +169 -0
- package/preset/agy-first/plugin-list.mjs +26 -0
- package/preset/agy-first/preset.yml +3 -0
- package/preset/codebuddy-first/agent.cordis.yml +141 -0
- package/preset/codebuddy-first/cli-bridge.mjs +188 -0
- package/preset/codebuddy-first/plugin-list.mjs +20 -0
- package/preset/codebuddy-first/preset.yml +3 -0
- package/preset/mimo-first/agent.cordis.yml +143 -0
- package/preset/mimo-first/cli-bridge.mjs +236 -0
- package/preset/mimo-first/plugin-list.mjs +39 -0
- package/preset/mimo-first/preset.yml +3 -0
- package/preset/standard-base.mjs +102 -0
- package/providers/agy.mjs +398 -0
- package/providers/codebuddy.mjs +926 -0
- package/providers/mimo.mjs +864 -0
- package/providers/registry.mjs +34 -0
- package/scripts/build.mjs +235 -0
- package/scripts/deploy.mjs +61 -0
- package/scripts/verify.mjs +148 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// providers/agy.mjs — agy provider descriptor(真移植,替换 Lead 冒烟占位)。
|
|
2
|
+
//
|
|
3
|
+
// 移植基线:agy-first-bridge v1.7.1 preset/agy-first/agy-first-bridge.mjs 的内联逻辑
|
|
4
|
+
// (buildArgv / parseAgyJson / buildResult / foldStepUpdate / 额度门禁 / 三工具 schema),
|
|
5
|
+
// 语义逐字保持;与 provider 无关的骨架(状态引擎/行流/回退弹窗/展示层)在
|
|
6
|
+
// core/bridge-kit.mjs(PROVIDER-CONTRACT.md §1)。
|
|
7
|
+
//
|
|
8
|
+
// 红线(PROVIDER-CONTRACT.md §8 + Lead 定案):
|
|
9
|
+
// - 本文件会被 build 内联进 dynamic 沙箱(new Function):绝不出现 import.meta 字面量;
|
|
10
|
+
// process 只经 deps 注入或 typeof 守卫读取;宿主能力全部走 deps/attach(hostOpts)。
|
|
11
|
+
// - 只允许 import '../core/bridge-kit.mjs'。
|
|
12
|
+
import { createStatusEngine, createRunner, clampInt, isLimited } from '../core/bridge-kit.mjs'
|
|
13
|
+
|
|
14
|
+
// subprocess stdio 预算(与 bridge-kit createRunner 内部 stdio 一致;quota 脚本用)。
|
|
15
|
+
const STDIO = {
|
|
16
|
+
stdin: 'ignore',
|
|
17
|
+
stdout: { maxBytes: 4000000, spill: { maxBytes: 40000000 } },
|
|
18
|
+
stderr: { maxBytes: 1000000, spill: { maxBytes: 8000000 } }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nowIso() { return new Date().toISOString() }
|
|
22
|
+
|
|
23
|
+
// ── 三工具 schema(旧 preset verbatim;契约 §8:不许顺手改 schema)──────────────
|
|
24
|
+
const RUN_PARAMS = {
|
|
25
|
+
type: 'object',
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
required: ['prompt'],
|
|
28
|
+
properties: {
|
|
29
|
+
prompt: { type: 'string', description: 'The task instruction for agy. On first dispatch for a topic, include a compact CONTEXT preamble (goal, repo root, key paths, decisions/constraints; point at paths, do not paste file contents — agy reads the repo itself). For follow-ups on the same topic, use agy_continue with latest=true or conversationId (agy keeps conversation context; state only what is new).' },
|
|
30
|
+
mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows DSH plan state; plan = no writes; accept-edits = allow edits. Default auto.' },
|
|
31
|
+
model: { type: 'string', description: 'Optional agy model id — pick from the Gemini pool or utility models per the model-selection policy (use agy_quota to see the recommended:true list). Do NOT pass a Claude/GPT (3p) model.' },
|
|
32
|
+
effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
|
|
33
|
+
cwd: { type: 'string', description: 'Working directory for agy. Defaults to the DSH workspace root.' },
|
|
34
|
+
addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to add to agy workspace.' },
|
|
35
|
+
timeoutSec: { type: 'integer', description: 'Print timeout seconds (10-3600, default 600).' },
|
|
36
|
+
background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output, watch progress with agy_status. Set false to block for the final answer.' }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const CONTINUE_PARAMS = {
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
required: ['prompt'],
|
|
43
|
+
properties: {
|
|
44
|
+
prompt: { type: 'string', description: 'Follow-up instruction for the ongoing agy conversation. State only what is new; agy preserves its conversation context.' },
|
|
45
|
+
conversationId: { type: 'string', description: 'agy conversation id to resume (from a prior agy_run result).' },
|
|
46
|
+
latest: { type: 'boolean', description: 'Continue the most recent agy conversation instead of a specific id.' },
|
|
47
|
+
mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto.' },
|
|
48
|
+
model: { type: 'string', description: 'Optional agy model id.' },
|
|
49
|
+
effort: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Optional reasoning effort.' },
|
|
50
|
+
cwd: { type: 'string', description: 'Working directory for agy.' },
|
|
51
|
+
timeoutSec: { type: 'integer', description: 'Print timeout seconds (10-3600, default 600).' },
|
|
52
|
+
background: { type: 'boolean', description: 'Defaults to true: dispatch returns a jobId immediately; collect with job_output, watch progress with agy_status. Set false to block for the final answer.' }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
|
|
56
|
+
|
|
57
|
+
// agy_quota(旧 preset quotaTool verbatim):查 Google AI 套餐(Antigravity OAuth)池子额度。
|
|
58
|
+
const QUOTA_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { summary: { type: 'boolean', description: 'Return only a compact summary line (weekly buckets + top models).' } } }
|
|
59
|
+
const QUOTA_DESCRIPTION = 'Query the Google AI plan (Antigravity OAuth consumer) quota pool behind the local agy CLI. Reads the Windows credential manager entry (gemini:antigravity) that agy wrote at login, refreshes the access token, then calls Google Cloud Code quota APIs: fetchAvailableModels (per-model remaining pool percentage) and retrieveUserQuotaSummary (grouped plan buckets: weekly + 5h windows). Returns { ok, models: [{name, percentage, resetTime, displayName}], groups: [{displayName, buckets: [{bucketId, window, remainingFraction, resetTime}]}], tier }. GATING RULE: only the Gemini 5h window decides whether agy can run this task — agy_run/agy_continue already refuse silently below 10% of the 5h pool, so you normally do not need to pre-check. The weekly window is NOT a per-task gate: a drained weekly bucket only means agy is a poor choice for the rest of the week (pick another subagent or your own tools), never a reason to abort or shrink the current task. If the credential is missing the tool returns { ok:false, error } and agy_run still works normally.'
|
|
60
|
+
|
|
61
|
+
function renderQuota(args, value) {
|
|
62
|
+
const v = value || {}
|
|
63
|
+
if (!v.ok) return [{ type: 'text', text: 'agy_quota failed: ' + String(v.error || 'unknown') }]
|
|
64
|
+
const lines = []
|
|
65
|
+
lines.push('agy quota [tier=' + (v.tier || '?') + ']')
|
|
66
|
+
if (Array.isArray(v.groups)) {
|
|
67
|
+
for (const g of v.groups) {
|
|
68
|
+
for (const b of (g.buckets || [])) {
|
|
69
|
+
const pct = typeof b.remainingFraction === 'number' ? Math.round(b.remainingFraction * 100) : '?'
|
|
70
|
+
lines.push('· ' + (b.bucketId || g.displayName || '') + ' [' + (b.window || '') + '] ' + pct + '%' + (b.resetTime ? ' reset=' + b.resetTime : ''))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(v.models) && v.models.length) {
|
|
75
|
+
lines.push('models (pool %):')
|
|
76
|
+
for (const m of v.models.slice(0, 10)) { lines.push(' ' + (m.displayName || m.name) + ' : ' + m.percentage + '%' + (m.recommended === false ? ' [3p: Claude/GPT 不推荐]' : '') + (m.resetTime ? ' (reset ' + m.resetTime + ')' : '')) }
|
|
77
|
+
if (v.models.length > 10) lines.push(' … and ' + (v.models.length - 10) + ' more')
|
|
78
|
+
}
|
|
79
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function defineProvider(deps = {}) {
|
|
83
|
+
const env = deps.env || {}
|
|
84
|
+
let host = null // attach 后保存 hostOpts(preset/dynamic 形态提供 subprocess)
|
|
85
|
+
let quotaCache = { at: 0, data: null } // 30 分钟缓存(旧 cachedQuotaCheck 语义)
|
|
86
|
+
|
|
87
|
+
// ── 额度脚本执行(bin/agy-quota.mjs)─────────────────────────────────────────
|
|
88
|
+
// 脚本定位(契约 §8 + Lead 定案 4):CLI_BRIDGE_AGY_QUOTA > AGY_QUOTA_SCRIPT(旧兼容)
|
|
89
|
+
// > deps.quotaScript(preset/MCP 宿主注入)。定位不到或无 spawn 能力 = 静默跳过门禁
|
|
90
|
+
// (旧语义的宽容降级)。
|
|
91
|
+
function quotaScriptPath() {
|
|
92
|
+
return env.CLI_BRIDGE_AGY_QUOTA || env.AGY_QUOTA_SCRIPT || deps.quotaScript || null
|
|
93
|
+
}
|
|
94
|
+
function quotaCapable() {
|
|
95
|
+
return !!quotaScriptPath() && !!(deps.nodeChild || (host && host.subprocess))
|
|
96
|
+
}
|
|
97
|
+
function execQuotaScript() {
|
|
98
|
+
const scriptPath = quotaScriptPath()
|
|
99
|
+
if (!scriptPath) return Promise.resolve({ ok: false, error: 'quota script not found (set CLI_BRIDGE_AGY_QUOTA or install the package bin/agy-quota.mjs)' })
|
|
100
|
+
const node = (deps.process && deps.process.execPath) || 'node'
|
|
101
|
+
if (deps.nodeChild) {
|
|
102
|
+
// MCP/测试形态:真 child_process(旧 agy-mcp-server execQuota verbatim 语义)。
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
let child
|
|
105
|
+
try { child = deps.nodeChild.spawn(node, [scriptPath], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) }
|
|
106
|
+
catch (e) { resolve({ ok: false, error: String(e && e.message || e) }); return }
|
|
107
|
+
let out = ''
|
|
108
|
+
let err = ''
|
|
109
|
+
const timer = setTimeout(() => { try { child.kill() } catch (e) {} }, 60000)
|
|
110
|
+
child.stdout.on('data', (d) => { out += d })
|
|
111
|
+
child.stderr.on('data', (d) => { err += d })
|
|
112
|
+
child.on('error', (e) => { clearTimeout(timer); resolve({ ok: false, error: 'spawn: ' + String(e && e.message || e) }) })
|
|
113
|
+
child.on('close', (code) => {
|
|
114
|
+
clearTimeout(timer)
|
|
115
|
+
const m = out.match(/\{[\s\S]*\}/)
|
|
116
|
+
if (!m) { resolve({ ok: false, error: 'no JSON output (exit=' + code + '): ' + (out + ' ' + err).slice(0, 300) }); return }
|
|
117
|
+
try { resolve(JSON.parse(m[0])) } catch (e) { resolve({ ok: false, error: 'parse: ' + e.message }) }
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
if (host && host.subprocess) {
|
|
122
|
+
// preset 形态:DSH subprocess(旧 preset execQuotaScript verbatim 语义)。
|
|
123
|
+
return (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const spec = { argv: [node, scriptPath], stdio: STDIO, graceMs: 5000 }
|
|
126
|
+
if (deps.process && typeof deps.process.cwd === 'function') spec.cwd = deps.process.cwd()
|
|
127
|
+
const handle = host.subprocess.spawn(spec)
|
|
128
|
+
const outcome = await handle.done
|
|
129
|
+
const stdoutText = handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : ''
|
|
130
|
+
const stderrText = handle.collected.stderr ? handle.collected.stderr.readFrom(0).text : ''
|
|
131
|
+
const jsonMatch = String(stdoutText || '').match(/\{[\s\S]*\}/)
|
|
132
|
+
if (!jsonMatch) return { ok: false, error: 'no JSON output (exit=' + (outcome && outcome.exitCode) + '): ' + (String(stdoutText).slice(0, 200) || String(stderrText).slice(0, 200)) }
|
|
133
|
+
try { return JSON.parse(jsonMatch[0]) } catch (e) { return { ok: false, error: 'parse error: ' + e.message } }
|
|
134
|
+
} catch (e) { return { ok: false, error: String(e && e.message || e) } }
|
|
135
|
+
})()
|
|
136
|
+
}
|
|
137
|
+
return Promise.resolve({ ok: false, error: 'no spawn capability in this host form' })
|
|
138
|
+
}
|
|
139
|
+
async function cachedQuotaCheck() {
|
|
140
|
+
const now = Date.now()
|
|
141
|
+
if (quotaCache.data && now - quotaCache.at < 30 * 60 * 1000) return quotaCache.data
|
|
142
|
+
let res = null
|
|
143
|
+
try { res = await execQuotaScript() } catch (e) { res = { ok: false, error: String(e && e.message || e) } }
|
|
144
|
+
quotaCache = { at: now, data: res }
|
|
145
|
+
return res
|
|
146
|
+
}
|
|
147
|
+
// 5h 硬阻断(旧 v1.5.13 门禁 verbatim):Gemini 5h 桶 <10% → QUOTA_BLOCKED(静默,
|
|
148
|
+
// 不弹窗);周用量不参与单次任务判断。预检失败不阻塞调用。
|
|
149
|
+
async function quotaGate5h(args, exec) {
|
|
150
|
+
if (!quotaCapable()) return null
|
|
151
|
+
let q = null
|
|
152
|
+
try { q = await cachedQuotaCheck() } catch (e) { return null }
|
|
153
|
+
if (!q || !q.ok) return null
|
|
154
|
+
const fiveHBuckets = (q.groups || []).flatMap((g) =>
|
|
155
|
+
(g.buckets || [])
|
|
156
|
+
.filter((b) => b.window === '5h' && (String(b.bucketId || '').toLowerCase().includes('gemini') || String(g.displayName || '').toLowerCase().includes('gemini')))
|
|
157
|
+
.map((b) => ({ bucketId: b.bucketId, remainingFraction: b.remainingFraction, resetTime: b.resetTime }))
|
|
158
|
+
)
|
|
159
|
+
const low5h = fiveHBuckets.find((b) => typeof b.remainingFraction === 'number' && b.remainingFraction < 0.10)
|
|
160
|
+
if (!low5h) return null
|
|
161
|
+
let mode = (args && args.mode) || 'auto'
|
|
162
|
+
if (mode === 'auto') {
|
|
163
|
+
let planActive = false
|
|
164
|
+
try { if (host && typeof host.planActiveFor === 'function') planActive = !!host.planActiveFor(exec) } catch (e) {}
|
|
165
|
+
mode = planActive ? 'plan' : 'accept-edits'
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: false, status: 'QUOTA_BLOCKED', response: '', conversationId: null, sessionId: null,
|
|
169
|
+
durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode, backend: null,
|
|
170
|
+
stderr: 'agy blocked: Gemini 5h pool quota < 10% (' + Math.round(low5h.remainingFraction * 100) + '%, reset ' + (low5h.resetTime || '?') + '). Use native tools; do not call agy.'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const descriptor = {
|
|
175
|
+
id: 'agy', label: 'AGY', presetId: 'cordis-agy', defaultBackend: null, defaultMode: 'auto',
|
|
176
|
+
exeName: 'agy', binEnvVars: ['AGY_BIN', 'AGY_CLI'], unavailableStatus: 'AGY_UNAVAILABLE',
|
|
177
|
+
|
|
178
|
+
// 单后端:backends() 恒 null;'agy'/'antigravity' 是历史别名,归一为 null;
|
|
179
|
+
// 非法值同样返回 null(契约 §1:非法输入返回 null)。会话路由由 engine.resolveTarget
|
|
180
|
+
// 完成(agy 的 backend 恒 null)。
|
|
181
|
+
backends() { return null },
|
|
182
|
+
normalizeBackend(v) {
|
|
183
|
+
if (v === undefined || v === null) return null
|
|
184
|
+
const s = String(v).trim().toLowerCase()
|
|
185
|
+
if (s === '' || s === 'agy' || s === 'antigravity') return null
|
|
186
|
+
return null
|
|
187
|
+
},
|
|
188
|
+
resolveBackend(args, target, preferred) { return null },
|
|
189
|
+
|
|
190
|
+
// 旧 preset buildArgv(exe, args, planActive) verbatim 语义(execPrefix 可为
|
|
191
|
+
// [nodeExe, binPath] 数组,runner 已归一)。
|
|
192
|
+
buildArgv(exePrefix, args, opts) {
|
|
193
|
+
const o = opts || {}
|
|
194
|
+
const argv = Array.isArray(exePrefix) ? exePrefix.slice() : [String(exePrefix)]
|
|
195
|
+
argv.push('-p', String(args.prompt), '--output-format', 'stream-json', '--dangerously-skip-permissions')
|
|
196
|
+
const timeoutSec = clampInt(args.timeoutSec, 600, 10, 3600)
|
|
197
|
+
argv.push('--print-timeout', timeoutSec + 's')
|
|
198
|
+
let mode = args.mode || 'auto'
|
|
199
|
+
if (mode === 'auto') mode = o.planActive ? 'plan' : 'accept-edits'
|
|
200
|
+
if (mode === 'plan' || mode === 'accept-edits') argv.push('--mode', mode)
|
|
201
|
+
if (args.model) argv.push('--model', String(args.model))
|
|
202
|
+
if (args.effort) argv.push('--effort', String(args.effort))
|
|
203
|
+
if (Array.isArray(args.addDirs)) for (const d of args.addDirs) { if (d) argv.push('--add-dir', String(d)) }
|
|
204
|
+
if (args.conversationId) argv.push('--conversation', String(args.conversationId))
|
|
205
|
+
else if (args.continueLatest) argv.push('--continue')
|
|
206
|
+
return { argv, mode, timeoutSec }
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// 旧 parseAgyJson verbatim:末 result 事件优先,容忍杂日志与整串 JSON。
|
|
210
|
+
parseJson(stdoutText) {
|
|
211
|
+
const trimmed = String(stdoutText || '').trim()
|
|
212
|
+
if (!trimmed) return null
|
|
213
|
+
const lines = trimmed.split(/\r?\n/).filter(Boolean)
|
|
214
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
215
|
+
const ln = lines[i].trim()
|
|
216
|
+
if (!ln.startsWith('{')) continue
|
|
217
|
+
try {
|
|
218
|
+
const obj = JSON.parse(ln)
|
|
219
|
+
if (obj && obj.event === 'result' && obj.result) return obj.result
|
|
220
|
+
if (obj && obj.status && obj.response !== undefined) return obj
|
|
221
|
+
} catch (e) {}
|
|
222
|
+
}
|
|
223
|
+
try { return JSON.parse(trimmed) } catch (e) {}
|
|
224
|
+
return null
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// 旧 buildResult verbatim + backend 字段 + sessionId/conversationId 双写
|
|
228
|
+
//(agy 的会话字段名旧包是 conversationId;新契约统一 sessionId,两键同值以兼容
|
|
229
|
+
// 旧 status 面板/测试与 keystone engine.end 的会话登记)。
|
|
230
|
+
buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
|
|
231
|
+
const exitCode = outcome ? outcome.exitCode : null
|
|
232
|
+
// agy 超时/错误时信息在 result.error 字段(如 "timeout waiting for response"),
|
|
233
|
+
// 必须并入 stderr 供 isLimited 归类(旧 v1.5.8 修复:网络挂起不再静默 FAILED)。
|
|
234
|
+
const errText = parsed && typeof parsed.error === 'string' && parsed.error ? parsed.error : ''
|
|
235
|
+
const stderr = (stderrText ? String(stderrText).slice(-2000) : '') + (errText ? (stderrText ? ' ' : '') + errText : '')
|
|
236
|
+
if (parsed) {
|
|
237
|
+
const conversationId = typeof parsed.conversation_id === 'string' ? parsed.conversation_id : null
|
|
238
|
+
return {
|
|
239
|
+
ok: exitCode === 0 && parsed.status === 'SUCCESS',
|
|
240
|
+
status: typeof parsed.status === 'string' ? parsed.status : (exitCode === 0 ? 'UNKNOWN' : 'ERROR'),
|
|
241
|
+
response: typeof parsed.response === 'string' ? parsed.response : '',
|
|
242
|
+
conversationId,
|
|
243
|
+
sessionId: conversationId,
|
|
244
|
+
durationSeconds: typeof parsed.duration_seconds === 'number' ? parsed.duration_seconds : null,
|
|
245
|
+
numTurns: typeof parsed.num_turns === 'number' ? parsed.num_turns : null,
|
|
246
|
+
totalTokens: parsed.usage && typeof parsed.usage.total_tokens === 'number' ? parsed.usage.total_tokens : null,
|
|
247
|
+
exitCode, mode, backend: backend || null, stderr
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ok: false, status: 'PARSE_ERROR', response: '',
|
|
252
|
+
conversationId: null, sessionId: null,
|
|
253
|
+
durationSeconds: null, numTurns: null, totalTokens: null,
|
|
254
|
+
exitCode, mode, backend: backend || null, stderr,
|
|
255
|
+
rawStdout: String(stdoutText || '').slice(-2000)
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// 旧 foldStepUpdate verbatim(step_update → project.current/trail)。
|
|
260
|
+
// bridge-kit foldEvent 对每行 JSON 都调本函数,故这里自判 event==='step_update'。
|
|
261
|
+
foldStreamEvent(ev, p, summarizeArgs, maxArgLen, maxTrail) {
|
|
262
|
+
if (!ev || ev.event !== 'step_update') return
|
|
263
|
+
const s = ev.step_update
|
|
264
|
+
if (!s) return
|
|
265
|
+
p.updatedAt = Date.now()
|
|
266
|
+
if (s.step_type === 'tool') {
|
|
267
|
+
const tool = s.tool_name || (s.tool_info && s.tool_info.name) || 'tool'
|
|
268
|
+
const args = summarizeArgs(s.tool_info && s.tool_info.parameters)
|
|
269
|
+
const entry = { stepIndex: s.step_index, state: s.state, tool: tool, args: args || null, at: nowIso() }
|
|
270
|
+
p.trail.push(entry)
|
|
271
|
+
const cap = maxTrail || 12
|
|
272
|
+
if (p.trail.length > cap) p.trail.shift()
|
|
273
|
+
if (s.state === 'ACTIVE') { p.current = { tool: tool, args: args || null, stepIndex: s.step_index, since: nowIso() } }
|
|
274
|
+
else if (p.current && p.current.stepIndex === s.step_index) { p.current = null }
|
|
275
|
+
} else if (s.step_type === 'agent_response' && s.state === 'ACTIVE' && s.text_delta) {
|
|
276
|
+
const capLen = maxArgLen || 120
|
|
277
|
+
p.current = { tool: 'agent_response', args: { text_delta: String(s.text_delta).slice(0, capLen) }, stepIndex: s.step_index, since: nowIso() }
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
// 旧 agy 仓无此概念(codebuddy v1.1.4 引入);保守版对齐 bridge-kit 语义:
|
|
282
|
+
// CLI 自报 error_during_execution 且非限流/网络类 → 静默重试一次。
|
|
283
|
+
isTransientCliError(res) {
|
|
284
|
+
if (!res || res.ok) return false
|
|
285
|
+
if (isLimited(res)) return false
|
|
286
|
+
return res.status === 'ERROR_DURING_EXECUTION'
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
// 失败指引(契约 §1;定案:401/OAuth 类指引 agy login,额度类指引 agy_quota)。
|
|
290
|
+
failureHint(res) {
|
|
291
|
+
if (!res || res.ok) return ''
|
|
292
|
+
const st = String(res.status || '')
|
|
293
|
+
if (st === 'QUOTA_BLOCKED') return '[诊断] agy 未调用(Gemini 5h 池子额度 <10%):请直接用原生工具完成本任务,本任务内不要再调 agy。'
|
|
294
|
+
if (st === 'AGY_UNAVAILABLE') return '[诊断] 未找到 agy 可执行文件:请安装 agy CLI,或设置 AGY_BIN 指向其绝对路径。'
|
|
295
|
+
const hay = String(res.stderr || '') + ' ' + String(res.response || '')
|
|
296
|
+
if (/invalid_grant|unauthorized|401|not logged|login|oauth/i.test(hay) && !/rate.?limit|quota|限流|额度/i.test(hay)) {
|
|
297
|
+
return '[诊断] agy 登录态/OAuth 凭据问题:请让用户在本机执行 `agy login` 重新登录后再试;可用 agy_quota 核对凭据与额度。这不是限流。'
|
|
298
|
+
}
|
|
299
|
+
if (/quota|额度|exhausted|insufficient|credit|balance/i.test(hay)) {
|
|
300
|
+
return '[诊断] agy 池子额度可能耗尽:用 agy_quota 查看各池子余量(Gemini 5h 是唯一任务级门禁);5h 低则改用原生工具完成本任务,不要循环调用 agy。'
|
|
301
|
+
}
|
|
302
|
+
return ''
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
isSuccessStatus(st) { return st === 'SUCCESS' },
|
|
306
|
+
|
|
307
|
+
tools: {
|
|
308
|
+
run: { name: 'agy_run', description: 'Dispatch a coding/build/debug/investigation task to the local agy agent CLI and return its final answer. Prefer this for implementation, edits, refactors, multi-file investigation and debugging in every mode. DSH fully controls agy: it always runs non-interactively with permissions auto-approved (agy never prompts). Use read-only native tools only for quick lookups and for final build/test verification. In mode=auto the DSH plan state decides agy plan vs accept-edits. Runs in the background by default (returns a jobId immediately; collect with job_output); set background=false to block for the final answer.', inputSchema: RUN_PARAMS },
|
|
309
|
+
continue: { name: 'agy_continue', description: 'Continue an existing agy conversation with a follow-up prompt, reusing agy context. Pass conversationId from a prior agy_run result, or set latest=true to continue the most recent agy conversation. Same DSH-controlled, no-prompt execution as agy_run.', inputSchema: CONTINUE_PARAMS },
|
|
310
|
+
status: { name: 'agy_status', description: 'Read a live snapshot of what the local agy agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), and the last completed run status + conversation id. Optional cwd filters to a single project. Call this to check on an in-flight agy_run/agy_continue without waiting for it to finish.', inputSchema: STATUS_PARAMS }
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// agy_quota 仅在宿主有 spawn 能力时注册(preset 注入 deps.process / MCP 注入
|
|
314
|
+
// deps.nodeChild;dynamic 沙箱形态省略 —— Lead 定案 4)。
|
|
315
|
+
extraTools: (deps.process || deps.nodeChild) ? [{
|
|
316
|
+
name: 'agy_quota',
|
|
317
|
+
description: QUOTA_DESCRIPTION,
|
|
318
|
+
inputSchema: QUOTA_PARAMS,
|
|
319
|
+
render: renderQuota,
|
|
320
|
+
async logic(args) {
|
|
321
|
+
const a = args || {}
|
|
322
|
+
const res = await execQuotaScript()
|
|
323
|
+
if (res && res.ok && a.summary) {
|
|
324
|
+
const weekly = (res.groups || []).flatMap((g) => (g.buckets || []).filter((b) => b.window === 'weekly').map((b) => ({ g: g.displayName, id: b.bucketId, pct: Math.round((b.remainingFraction || 0) * 100), reset: b.resetTime })))
|
|
325
|
+
const top = (res.models || []).slice(0, 4).map((m) => (m.displayName || m.name) + ' ' + m.percentage + '%').join(', ')
|
|
326
|
+
return { ok: true, summary: { weekly, topModels: top }, models: res.models, groups: res.groups, tier: res.tier }
|
|
327
|
+
}
|
|
328
|
+
return res
|
|
329
|
+
}
|
|
330
|
+
}] : [],
|
|
331
|
+
|
|
332
|
+
// agy 无设置分区字段(契约 §3)。
|
|
333
|
+
settings: {
|
|
334
|
+
keys: [],
|
|
335
|
+
defaults: () => ({}),
|
|
336
|
+
normalize: () => ({}),
|
|
337
|
+
meta: () => ({ fields: [] }),
|
|
338
|
+
diagnose: () => ''
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
mcp: { version: '0.2.2' }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 三形态共用的工具逻辑。签名保持不变(keystone 与队友按此接线)。
|
|
345
|
+
descriptor.runToolLogic = async function (args, exec) {
|
|
346
|
+
const a = args || {}
|
|
347
|
+
// 旧 agy preset/dynamic 语义:background 默认 true(仅显式 false 才前台阻塞)。
|
|
348
|
+
const normalized = a.background === undefined ? Object.assign({}, a, { background: true }) : a
|
|
349
|
+
const block = await quotaGate5h(normalized, exec)
|
|
350
|
+
if (block) return block
|
|
351
|
+
return this.runner.coreExecute(normalized, exec)
|
|
352
|
+
}
|
|
353
|
+
descriptor.continueToolLogic = async function (args, exec) {
|
|
354
|
+
const a = args || {}
|
|
355
|
+
const mapped = { prompt: a.prompt, mode: a.mode, model: a.model, effort: a.effort, cwd: a.cwd, timeoutSec: a.timeoutSec, background: a.background === undefined ? true : a.background }
|
|
356
|
+
const cid = a.conversationId || a.sessionId || null
|
|
357
|
+
if (cid) {
|
|
358
|
+
mapped.conversationId = cid
|
|
359
|
+
mapped.sessionId = cid // keystone engine.resolveTarget 按 sessionId 路由 cwd
|
|
360
|
+
} else if (a.latest) {
|
|
361
|
+
mapped.continueLatest = true
|
|
362
|
+
}
|
|
363
|
+
const block = await quotaGate5h(mapped, exec)
|
|
364
|
+
if (block) return block
|
|
365
|
+
return this.runner.coreExecute(mapped, exec)
|
|
366
|
+
}
|
|
367
|
+
// 旧 statusTool.execute verbatim:cwd 过滤到单项目。
|
|
368
|
+
descriptor.statusToolLogic = function (args) {
|
|
369
|
+
const a = args || {}
|
|
370
|
+
const snap = this.engine.statusSnapshot()
|
|
371
|
+
if (a.cwd) {
|
|
372
|
+
const key = String(a.cwd)
|
|
373
|
+
snap.projects = snap.projects.filter((p) => p.cwd === key)
|
|
374
|
+
const g = snap.projects[0]
|
|
375
|
+
if (g) {
|
|
376
|
+
Object.assign(snap, {
|
|
377
|
+
state: g.state, running: g.running, current: g.current, trail: g.trail,
|
|
378
|
+
lastStatus: g.lastStatus, lastAt: g.lastAt, lastSessionId: g.lastSessionId,
|
|
379
|
+
lastBackend: g.lastBackend, fallbackActive: g.fallbackActive, updatedAt: g.updatedAt
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return snap
|
|
384
|
+
}
|
|
385
|
+
// 供 MCP/测试形态直接复用门禁与额度执行。
|
|
386
|
+
descriptor.quotaGate5h = quotaGate5h
|
|
387
|
+
descriptor.execQuotaScript = execQuotaScript
|
|
388
|
+
descriptor.cachedQuotaCheck = cachedQuotaCheck
|
|
389
|
+
|
|
390
|
+
descriptor.attach = function (hostOpts) {
|
|
391
|
+
host = hostOpts || {}
|
|
392
|
+
this.engine = createStatusEngine({ provider: this, publish: host.publish })
|
|
393
|
+
const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
|
|
394
|
+
this.runner = runner
|
|
395
|
+
return runner
|
|
396
|
+
}
|
|
397
|
+
return descriptor
|
|
398
|
+
}
|