minimal-agent 0.18.13 → 0.18.14
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/package.json +1 -3
- package/plugins/workflow-runner/src/runtime/context.js +1 -1
- package/plugins/workflow-runner/src/runtime/context.ts +1 -1
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
- package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
- package/skills/ssh-remote-dev/SKILL.md +61 -0
- package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
- package/src/advisor/README.md +62 -24
- package/src/advisor/advisorNudge.js +199 -0
- package/src/advisor/advisorNudge.ts +232 -0
- package/src/advisor/deliveryReview.js +35 -12
- package/src/advisor/deliveryReview.ts +35 -11
- package/src/advisor/presentation.js +7 -10
- package/src/advisor/presentation.ts +10 -13
- package/src/advisor/reviewContext.js +22 -58
- package/src/advisor/reviewContext.ts +24 -67
- package/src/cli/configMenu.js +17 -128
- package/src/cli/configMenu.tsx +17 -193
- package/src/cli/print.js +2 -0
- package/src/cli/print.ts +2 -0
- package/src/cli/streamJson.js +2 -0
- package/src/cli/streamJson.ts +2 -0
- package/src/cli/wizards/advisorWizard.js +9 -7
- package/src/cli/wizards/advisorWizard.tsx +9 -7
- package/src/cli/wizards/contextAdvisorWizard.js +9 -7
- package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
- package/src/cli/wizards/providerPresets.js +27 -1
- package/src/cli/wizards/providerPresets.ts +44 -2
- package/src/cli/wizards/providerWizard.js +51 -10
- package/src/cli/wizards/providerWizard.tsx +59 -9
- package/src/config/configDefaults.js +1 -1
- package/src/config/configDefaults.ts +1 -1
- package/src/config/configDiagnostics.js +1 -21
- package/src/config/configDiagnostics.ts +3 -23
- package/src/config/configFile.js +11 -4
- package/src/config/configFile.ts +27 -7
- package/src/config/configManagement.js +1 -0
- package/src/config/configManagement.ts +2 -0
- package/src/config/configService.js +12 -8
- package/src/config/configService.ts +16 -11
- package/src/config/numericLimits.js +0 -4
- package/src/config/numericLimits.ts +0 -4
- package/src/config/reasoningEffort.js +16 -0
- package/src/config/reasoningEffort.ts +18 -0
- package/src/context/budgetPlan.js +1 -1
- package/src/context/budgetPlan.ts +1 -1
- package/src/context/optimizeConversation.js +1 -1
- package/src/context/optimizeConversation.ts +1 -1
- package/src/harness/stallGuard.js +8 -13
- package/src/harness/stallGuard.ts +8 -12
- package/src/llm/client.js +64 -17
- package/src/llm/client.ts +62 -7
- package/src/llm/providers/openaiFormat.js +14 -3
- package/src/llm/providers/openaiFormat.ts +17 -3
- package/src/llm/providers/responsesFormat.js +275 -0
- package/src/llm/providers/responsesFormat.ts +306 -0
- package/src/loop/README.md +18 -3
- package/src/loop/liveDiagramGate.js +63 -0
- package/src/loop/liveDiagramGate.ts +61 -0
- package/src/loop/messages.js +12 -0
- package/src/loop/messages.ts +13 -0
- package/src/loop/options.ts +7 -2
- package/src/loop/toolBatch.js +0 -1
- package/src/loop/toolBatch.ts +0 -1
- package/src/loop/toolExecutor.js +11 -46
- package/src/loop/toolExecutor.ts +12 -55
- package/src/loop/toolPresentation.js +2 -13
- package/src/loop/toolPresentation.ts +2 -13
- package/src/loop/toolSemantics.js +3 -4
- package/src/loop/toolSemantics.ts +4 -6
- package/src/loop.js +222 -214
- package/src/loop.ts +226 -232
- package/src/memory/dependencies.js +11 -3
- package/src/memory/dependencies.ts +11 -3
- package/src/memory/episodicMemory.js +59 -18
- package/src/memory/episodicMemory.ts +58 -18
- package/src/memory/experienceStore.js +4 -3
- package/src/memory/experienceStore.ts +4 -3
- package/src/memory/factCandidates.js +53 -1
- package/src/memory/factCandidates.ts +54 -1
- package/src/memory/factLearning.js +2 -2
- package/src/memory/factLearning.ts +2 -2
- package/src/memory/jobPolicy.js +24 -5
- package/src/memory/jobPolicy.ts +25 -5
- package/src/memory/learning.js +2 -2
- package/src/memory/learning.ts +2 -2
- package/src/memory/learningModel.js +7 -0
- package/src/memory/learningModel.ts +7 -0
- package/src/memory/managementService.js +8 -7
- package/src/memory/managementService.ts +16 -8
- package/src/memory/memoryService.js +104 -33
- package/src/memory/memoryService.ts +132 -28
- package/src/memory/projectKnowledge.js +39 -3
- package/src/memory/projectKnowledge.ts +45 -3
- package/src/memory/recallQuery.js +56 -0
- package/src/memory/recallQuery.ts +58 -0
- package/src/memory/retrieval.js +39 -10
- package/src/memory/retrieval.ts +46 -11
- package/src/memory/turnJournal.js +75 -10
- package/src/memory/turnJournal.ts +66 -10
- package/src/memory/types.ts +7 -1
- package/src/prompts/delegation.js +2 -1
- package/src/prompts/delegation.ts +3 -1
- package/src/prompts/policyRegistry.js +9 -12
- package/src/prompts/policyRegistry.ts +9 -12
- package/src/prompts/promptAssembly.js +6 -5
- package/src/prompts/promptAssembly.ts +8 -6
- package/src/prompts/visualPresentation.js +39 -10
- package/src/prompts/visualPresentation.ts +38 -9
- package/src/runtime/answerPreparation.js +1 -1
- package/src/runtime/answerPreparation.ts +1 -1
- package/src/runtime/fileLock.js +19 -3
- package/src/runtime/fileLock.ts +19 -3
- package/src/runtime/invocationPolicy.js +1 -1
- package/src/runtime/invocationPolicy.ts +1 -1
- package/src/runtime/outputContract.js +0 -4
- package/src/runtime/outputContract.ts +0 -7
- package/src/session/runAgentTurn.js +3 -0
- package/src/session/runAgentTurn.ts +3 -0
- package/src/team/onlineToolContract.js +0 -1
- package/src/team/onlineToolContract.ts +0 -1
- package/src/tools/advisor/advisor.js +207 -365
- package/src/tools/advisor/advisor.ts +211 -383
- package/src/tools/agent/agentTypes.js +0 -1
- package/src/tools/agent/agentTypes.ts +0 -1
- package/src/tools/agent/coordinator.js +4 -1
- package/src/tools/agent/coordinator.ts +4 -1
- package/src/tools/bash/semantics.js +4 -5
- package/src/tools/bash/semantics.ts +4 -5
- package/src/tools/capabilities.js +2 -3
- package/src/tools/capabilities.ts +2 -4
- package/src/tools/index.js +0 -16
- package/src/tools/index.ts +0 -16
- package/src/tools/memory/maintenance.js +8 -9
- package/src/tools/memory/maintenance.ts +13 -11
- package/src/tools/memory/memory.js +5 -1
- package/src/tools/memory/memory.ts +5 -0
- package/src/tools/optimizeConversation/README.md +4 -6
- package/src/tools/toolPromptGuides.js +2 -9
- package/src/tools/toolPromptGuides.ts +2 -9
- package/src/types.ts +28 -105
- package/src/ui/App.js +2 -0
- package/src/ui/App.tsx +1 -0
- package/src/ui/MessageList.js +8 -44
- package/src/ui/MessageList.tsx +3 -81
- package/src/ui/hooks/useChat.js +2 -0
- package/src/ui/hooks/useChat.ts +2 -0
- package/src/ui/toolProgress.js +1 -1
- package/src/ui/toolProgress.ts +1 -1
- package/src/web/protocol.ts +10 -3
- package/src/advisor/advisorCoordinator.js +0 -734
- package/src/advisor/advisorCoordinator.ts +0 -825
- package/src/cli/wizards/sshHostWizard.js +0 -156
- package/src/cli/wizards/sshHostWizard.tsx +0 -214
- package/src/context/sshHosts.js +0 -290
- package/src/context/sshHosts.ts +0 -312
- package/src/loop/publication.js +0 -94
- package/src/loop/publication.ts +0 -124
- package/src/runtime/finalAnswer.js +0 -9
- package/src/runtime/finalAnswer.ts +0 -13
- package/src/tools/advisor/advisorDecision.js +0 -79
- package/src/tools/advisor/advisorDecision.ts +0 -92
- package/src/tools/ssh/ssh.js +0 -152
- package/src/tools/ssh/ssh.ts +0 -167
- package/src/tools/ssh/sshAvailability.js +0 -35
- package/src/tools/ssh/sshAvailability.ts +0 -39
- package/src/tools/ssh/sshClient.js +0 -177
- package/src/tools/ssh/sshClient.ts +0 -189
- package/src/tools/ssh/sshDownload.js +0 -77
- package/src/tools/ssh/sshDownload.ts +0 -92
- package/src/tools/ssh/sshHostsTool.js +0 -36
- package/src/tools/ssh/sshHostsTool.ts +0 -48
- package/src/tools/ssh/sshUpload.js +0 -72
- package/src/tools/ssh/sshUpload.ts +0 -85
package/src/context/sshHosts.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================
|
|
3
|
-
* src/context/sshHosts.ts —— 工作区级 SSH 主机配置存储
|
|
4
|
-
* ------------------------------------------------------------
|
|
5
|
-
* 每个工作区(workspaceId,见 workspaceIndex.ts)独立一份 SSH 主机列表,
|
|
6
|
-
* **不进全局 config.json**——绿色版的 config.json 是卖家预置件(U 盘出厂
|
|
7
|
-
* 即预配置好 API),不能承载终端用户自己录入的远程主机凭据(尤其是明文
|
|
8
|
-
* 密码:Jetson 类设备常常只支持密码认证,见 SshHostConfig.authMethod)。
|
|
9
|
-
*
|
|
10
|
-
* 落盘路径:agentStateDir()/workspaces/<workspaceId>/ssh-hosts.json
|
|
11
|
-
* 锁 + 原子写模式镜像 configFile.ts::acquireConfigLock(mkdir 锁目录 +
|
|
12
|
-
* owner.json + 陈旧锁检测),但这是独立文件独立锁,不与全局 config 锁
|
|
13
|
-
* 互相阻塞。
|
|
14
|
-
*
|
|
15
|
-
* workspaceId 缺失(临时/e2e/cron 工作目录,见 workspaceIndex.ts 的
|
|
16
|
-
* isTemporaryWorkspace)时,本模块的调用方(SSH/SSHUpload/SSHDownload
|
|
17
|
-
* 三个工具)必须 fail-closed 报错,不得静默回落到某个共享位置——这与
|
|
18
|
-
* memoryAccess 默认 denied 是同一个哲学:没有工作区身份就没有可信的
|
|
19
|
-
* 隔离边界。
|
|
20
|
-
* ============================================================
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { randomUUID } from 'node:crypto';
|
|
24
|
-
import { chmod, mkdir, readFile, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
25
|
-
import { dirname, join } from 'node:path';
|
|
26
|
-
|
|
27
|
-
import type { ShellKind } from '../tools/shared/permanentDeleteGuard.js';
|
|
28
|
-
import { agentStateDir } from './agentHome.js';
|
|
29
|
-
import { CONFIG_LIMITS, withinLimit } from '../config/numericLimits.js';
|
|
30
|
-
|
|
31
|
-
export type SshAuthMethod = 'password' | 'privateKey';
|
|
32
|
-
|
|
33
|
-
export interface SshHostConfig {
|
|
34
|
-
/** 模型和 CLI 都用这个引用主机;真实 host/密码永不进模型上下文。 */
|
|
35
|
-
alias: string;
|
|
36
|
-
host: string;
|
|
37
|
-
port: number;
|
|
38
|
-
username: string;
|
|
39
|
-
authMethod: SshAuthMethod;
|
|
40
|
-
password?: string;
|
|
41
|
-
privateKeyPath?: string;
|
|
42
|
-
passphrase?: string;
|
|
43
|
-
/**
|
|
44
|
-
* 远程执行命令时的 shell 语义——喂给 permanentDeleteReason() 挡永久删除。
|
|
45
|
-
* 必须显式声明:本地猜不出远程机器跑的是哪种 shell,历史上硬编码错过一次
|
|
46
|
-
* 真删过文件(见 permanentDeleteGuard.ts 头注),这里不能重蹈。
|
|
47
|
-
*/
|
|
48
|
-
remoteShell: ShellKind;
|
|
49
|
-
/** 单次操作(连接+执行/传输)超时毫秒;缺省时调用方使用固定默认值。 */
|
|
50
|
-
timeoutMs?: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SshHostsData {
|
|
54
|
-
hosts: SshHostConfig[];
|
|
55
|
-
defaultHostAlias: string | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface SshHostsFileV1 {
|
|
59
|
-
version: 1;
|
|
60
|
-
hosts: SshHostConfig[];
|
|
61
|
-
defaultHostAlias: string | null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const EMPTY_DATA: SshHostsData = { hosts: [], defaultHostAlias: null };
|
|
65
|
-
const LOCK_STALE_MS = 120_000;
|
|
66
|
-
const LOCK_WAIT_MS = 15_000;
|
|
67
|
-
const ALIAS_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
68
|
-
|
|
69
|
-
/** 标准 SSH 端口——唯一真值,向导默认值与 normalizeHost() 的兜底都从这里取。 */
|
|
70
|
-
export const DEFAULT_SSH_PORT = 22;
|
|
71
|
-
|
|
72
|
-
export function sshHostsFilePath(workspaceId: string): string {
|
|
73
|
-
return join(agentStateDir(), 'workspaces', workspaceId, 'ssh-hosts.json');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function processIsAlive(pid: number): boolean {
|
|
77
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
78
|
-
try {
|
|
79
|
-
process.kill(pid, 0);
|
|
80
|
-
return true;
|
|
81
|
-
} catch (error) {
|
|
82
|
-
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function acquireLock(file: string): Promise<() => Promise<void>> {
|
|
87
|
-
const lockDir = `${file}.lock`;
|
|
88
|
-
const ownerFile = join(lockDir, 'owner.json');
|
|
89
|
-
const startedAt = Date.now();
|
|
90
|
-
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
91
|
-
while (true) {
|
|
92
|
-
try {
|
|
93
|
-
await mkdir(lockDir, { mode: 0o700 });
|
|
94
|
-
await writeFile(ownerFile, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), { mode: 0o600 });
|
|
95
|
-
return async () => { await rm(lockDir, { recursive: true, force: true }); };
|
|
96
|
-
} catch (error) {
|
|
97
|
-
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
98
|
-
let stale = false;
|
|
99
|
-
try {
|
|
100
|
-
const [ownerRaw, info] = await Promise.all([readFile(ownerFile, 'utf8'), stat(lockDir)]);
|
|
101
|
-
const owner = JSON.parse(ownerRaw) as { pid?: number };
|
|
102
|
-
stale = typeof owner.pid === 'number' ? !processIsAlive(owner.pid) : Date.now() - info.mtimeMs > LOCK_STALE_MS;
|
|
103
|
-
} catch {
|
|
104
|
-
try { stale = Date.now() - (await stat(lockDir)).mtimeMs > LOCK_STALE_MS; } catch { stale = false; }
|
|
105
|
-
}
|
|
106
|
-
if (stale) { await rm(lockDir, { recursive: true, force: true }); continue; }
|
|
107
|
-
if (Date.now() - startedAt > LOCK_WAIT_MS) throw new Error('等待 SSH 主机配置锁超时');
|
|
108
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 40));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function normalizeHost(raw: unknown): SshHostConfig | null {
|
|
114
|
-
if (!raw || typeof raw !== 'object') return null;
|
|
115
|
-
const value = raw as Record<string, unknown>;
|
|
116
|
-
if (typeof value.alias !== 'string' || !ALIAS_RE.test(value.alias)) return null;
|
|
117
|
-
if (typeof value.host !== 'string' || !value.host.trim()) return null;
|
|
118
|
-
if (typeof value.username !== 'string' || !value.username.trim()) return null;
|
|
119
|
-
if (value.authMethod !== 'password' && value.authMethod !== 'privateKey') return null;
|
|
120
|
-
if (value.remoteShell !== 'bash' && value.remoteShell !== 'cmd' && value.remoteShell !== 'powershell') return null;
|
|
121
|
-
const port = typeof value.port === 'number' ? value.port : DEFAULT_SSH_PORT;
|
|
122
|
-
if (!withinLimit(port, CONFIG_LIMITS.sshPort)) return null;
|
|
123
|
-
const timeoutMs = typeof value.timeoutMs === 'number' ? value.timeoutMs : undefined;
|
|
124
|
-
if (timeoutMs !== undefined && !withinLimit(timeoutMs, CONFIG_LIMITS.sshTimeoutMs)) return null;
|
|
125
|
-
return {
|
|
126
|
-
alias: value.alias,
|
|
127
|
-
host: value.host,
|
|
128
|
-
port,
|
|
129
|
-
username: value.username,
|
|
130
|
-
authMethod: value.authMethod,
|
|
131
|
-
...(typeof value.password === 'string' ? { password: value.password } : {}),
|
|
132
|
-
...(typeof value.privateKeyPath === 'string' ? { privateKeyPath: value.privateKeyPath } : {}),
|
|
133
|
-
...(typeof value.passphrase === 'string' ? { passphrase: value.passphrase } : {}),
|
|
134
|
-
remoteShell: value.remoteShell,
|
|
135
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function normalizeFile(parsed: unknown): SshHostsData | null {
|
|
140
|
-
if (!parsed || typeof parsed !== 'object') return null;
|
|
141
|
-
const raw = parsed as Record<string, unknown>;
|
|
142
|
-
if (raw.version !== 1 || !Array.isArray(raw.hosts)) return null;
|
|
143
|
-
const hosts = raw.hosts.flatMap((item) => {
|
|
144
|
-
const host = normalizeHost(item);
|
|
145
|
-
return host ? [host] : [];
|
|
146
|
-
});
|
|
147
|
-
const defaultHostAlias = typeof raw.defaultHostAlias === 'string' && hosts.some((h) => h.alias === raw.defaultHostAlias)
|
|
148
|
-
? raw.defaultHostAlias
|
|
149
|
-
: null;
|
|
150
|
-
return { hosts, defaultHostAlias };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** 读路径 fail-open:坏文件被隔离到旁路而不是让整个工作区永远读不出主机列表。 */
|
|
154
|
-
async function quarantineCorruptFile(file: string, reason: string): Promise<void> {
|
|
155
|
-
const target = `${file}.corrupt-${Date.now()}`;
|
|
156
|
-
try {
|
|
157
|
-
await rename(file, target);
|
|
158
|
-
await writeFile(`${target}.reason.txt`, reason, { mode: 0o600 });
|
|
159
|
-
} catch { /* 文件在读和隔离之间消失也无妨,视同不存在 */ }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function readUnlocked(workspaceId: string): Promise<SshHostsData> {
|
|
163
|
-
const file = sshHostsFilePath(workspaceId);
|
|
164
|
-
let raw: string;
|
|
165
|
-
try {
|
|
166
|
-
raw = await readFile(file, 'utf8');
|
|
167
|
-
} catch (error) {
|
|
168
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return EMPTY_DATA;
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
let parsed: unknown;
|
|
172
|
-
try {
|
|
173
|
-
parsed = JSON.parse(raw);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
await quarantineCorruptFile(file, `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
176
|
-
return EMPTY_DATA;
|
|
177
|
-
}
|
|
178
|
-
const normalized = normalizeFile(parsed);
|
|
179
|
-
if (!normalized) {
|
|
180
|
-
await quarantineCorruptFile(file, 'SSH 主机配置文件结构不合法');
|
|
181
|
-
return EMPTY_DATA;
|
|
182
|
-
}
|
|
183
|
-
return normalized;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function writeUnlocked(workspaceId: string, data: SshHostsData): Promise<void> {
|
|
187
|
-
const file = sshHostsFilePath(workspaceId);
|
|
188
|
-
const payload: SshHostsFileV1 = { version: 1, hosts: data.hosts, defaultHostAlias: data.defaultHostAlias };
|
|
189
|
-
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
190
|
-
try {
|
|
191
|
-
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
192
|
-
await writeFile(tmp, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
193
|
-
await rename(tmp, file);
|
|
194
|
-
await chmod(file, 0o600).catch(() => {});
|
|
195
|
-
} finally {
|
|
196
|
-
await unlink(tmp).catch(() => {});
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** 写入前的形状校验——别名唯一、必填字段非空、数字边界,defaultHostAlias 必须指向存在的主机或为 null。 */
|
|
201
|
-
function assertValid(data: SshHostsData): void {
|
|
202
|
-
const seen = new Set<string>();
|
|
203
|
-
for (const host of data.hosts) {
|
|
204
|
-
if (!ALIAS_RE.test(host.alias)) throw new Error(`主机别名不合法:${host.alias}`);
|
|
205
|
-
if (seen.has(host.alias)) throw new Error(`主机别名重复:${host.alias}`);
|
|
206
|
-
seen.add(host.alias);
|
|
207
|
-
if (!host.host.trim()) throw new Error(`主机地址不能为空(alias=${host.alias})`);
|
|
208
|
-
if (!host.username.trim()) throw new Error(`用户名不能为空(alias=${host.alias})`);
|
|
209
|
-
if (!withinLimit(host.port, CONFIG_LIMITS.sshPort)) throw new Error(`端口超出范围(alias=${host.alias})`);
|
|
210
|
-
if (host.timeoutMs !== undefined && !withinLimit(host.timeoutMs, CONFIG_LIMITS.sshTimeoutMs)) {
|
|
211
|
-
throw new Error(`超时时间超出范围(alias=${host.alias})`);
|
|
212
|
-
}
|
|
213
|
-
if (host.authMethod === 'password' && !host.password) throw new Error(`密码认证缺少密码(alias=${host.alias})`);
|
|
214
|
-
if (host.authMethod === 'privateKey' && !host.privateKeyPath) throw new Error(`密钥认证缺少私钥路径(alias=${host.alias})`);
|
|
215
|
-
}
|
|
216
|
-
if (data.defaultHostAlias !== null && !seen.has(data.defaultHostAlias)) {
|
|
217
|
-
throw new Error(`默认主机 "${data.defaultHostAlias}" 不在主机列表中`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** 读-改-写的唯一入口:整个过程持锁,避免并发编辑丢更新。 */
|
|
222
|
-
async function mutate(workspaceId: string, mutator: (current: SshHostsData) => SshHostsData): Promise<SshHostsData> {
|
|
223
|
-
const file = sshHostsFilePath(workspaceId);
|
|
224
|
-
const release = await acquireLock(file);
|
|
225
|
-
try {
|
|
226
|
-
const current = await readUnlocked(workspaceId);
|
|
227
|
-
const next = mutator(current);
|
|
228
|
-
assertValid(next);
|
|
229
|
-
await writeUnlocked(workspaceId, next);
|
|
230
|
-
return next;
|
|
231
|
-
} finally {
|
|
232
|
-
await release();
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export async function listSshHosts(workspaceId: string): Promise<SshHostsData> {
|
|
237
|
-
return readUnlocked(workspaceId);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export async function addSshHost(workspaceId: string, host: SshHostConfig): Promise<SshHostsData> {
|
|
241
|
-
return mutate(workspaceId, (current) => {
|
|
242
|
-
if (current.hosts.some((h) => h.alias === host.alias)) throw new Error(`主机别名已存在:${host.alias}`);
|
|
243
|
-
const hosts = [...current.hosts, host];
|
|
244
|
-
return { hosts, defaultHostAlias: current.defaultHostAlias ?? (hosts.length === 1 ? host.alias : null) };
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export async function updateSshHost(
|
|
249
|
-
workspaceId: string,
|
|
250
|
-
alias: string,
|
|
251
|
-
patch: Partial<Omit<SshHostConfig, 'alias'>>,
|
|
252
|
-
): Promise<SshHostsData> {
|
|
253
|
-
return mutate(workspaceId, (current) => {
|
|
254
|
-
const index = current.hosts.findIndex((h) => h.alias === alias);
|
|
255
|
-
if (index < 0) throw new Error(`主机不存在:${alias}`);
|
|
256
|
-
const hosts = [...current.hosts];
|
|
257
|
-
hosts[index] = { ...hosts[index], ...patch };
|
|
258
|
-
return { hosts, defaultHostAlias: current.defaultHostAlias };
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function removeSshHost(workspaceId: string, alias: string): Promise<SshHostsData> {
|
|
263
|
-
return mutate(workspaceId, (current) => {
|
|
264
|
-
const hosts = current.hosts.filter((h) => h.alias !== alias);
|
|
265
|
-
return {
|
|
266
|
-
hosts,
|
|
267
|
-
defaultHostAlias: current.defaultHostAlias === alias ? null : current.defaultHostAlias,
|
|
268
|
-
};
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export async function setDefaultSshHost(workspaceId: string, alias: string | null): Promise<SshHostsData> {
|
|
273
|
-
return mutate(workspaceId, (current) => ({ ...current, defaultHostAlias: alias }));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export type ResolveSshHostResult =
|
|
277
|
-
| { ok: true; host: SshHostConfig }
|
|
278
|
-
| { ok: false; error: string };
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 供 SSH/SSHUpload/SSHDownload 三个工具调用的解析入口:
|
|
282
|
-
* 显式给了 alias 就按 alias 找;没给就退回该工作区的默认主机;
|
|
283
|
-
* 两者都没有就明确报错,不静默选第一个(选错主机执行远程命令后果比报错严重)。
|
|
284
|
-
*/
|
|
285
|
-
export async function resolveSshHost(workspaceId: string, aliasInput?: string): Promise<ResolveSshHostResult> {
|
|
286
|
-
const data = await listSshHosts(workspaceId);
|
|
287
|
-
if (aliasInput) {
|
|
288
|
-
const host = data.hosts.find((h) => h.alias === aliasInput);
|
|
289
|
-
if (!host) {
|
|
290
|
-
const known = data.hosts.map((h) => h.alias).join(', ') || '(无)';
|
|
291
|
-
return { ok: false, error: `未找到主机 "${aliasInput}"。已配置:${known}` };
|
|
292
|
-
}
|
|
293
|
-
return { ok: true, host };
|
|
294
|
-
}
|
|
295
|
-
if (data.defaultHostAlias) {
|
|
296
|
-
const host = data.hosts.find((h) => h.alias === data.defaultHostAlias);
|
|
297
|
-
if (host) return { ok: true, host };
|
|
298
|
-
}
|
|
299
|
-
if (data.hosts.length === 0) {
|
|
300
|
-
return { ok: false, error: '当前工作区尚未配置任何 SSH 主机,请先在 /config → 远程主机 (SSH) 中添加。' };
|
|
301
|
-
}
|
|
302
|
-
return { ok: false, error: `未指定主机且未设置默认主机。已配置:${data.hosts.map((h) => h.alias).join(', ')}` };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** 给工具 description 用的轻量列表——只暴露 alias/host/username,绝不带密码。 */
|
|
306
|
-
export async function describeSshHosts(workspaceId: string): Promise<string> {
|
|
307
|
-
const data = await listSshHosts(workspaceId);
|
|
308
|
-
if (data.hosts.length === 0) return '(当前工作区尚未配置任何 SSH 主机)';
|
|
309
|
-
return data.hosts
|
|
310
|
-
.map((h) => `${h.alias}(${h.username}@${h.host}:${h.port})${h.alias === data.defaultHostAlias ? ' [默认]' : ''}`)
|
|
311
|
-
.join(', ');
|
|
312
|
-
}
|
package/src/loop/publication.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { buildContextBudgetPlan } from '../context/budgetPlan.js';
|
|
2
|
-
import { finalAnswerMessages } from '../runtime/finalAnswer.js';
|
|
3
|
-
/** A one-way visibility boundary: never retract or regenerate visible text. */
|
|
4
|
-
export async function* publishFinalAnswer(input) {
|
|
5
|
-
const { provider, messages, candidate, candidateFinishReason, output, chatFn, options, signal, conversationMode, turnUsage, } = input;
|
|
6
|
-
let lastFinishReason = candidateFinishReason;
|
|
7
|
-
let publicationError;
|
|
8
|
-
let publicationInterrupted = false;
|
|
9
|
-
let streamed = false;
|
|
10
|
-
let publishedText = '';
|
|
11
|
-
let publicationDone = false;
|
|
12
|
-
yield { type: 'stage_change', stage: 'streaming' };
|
|
13
|
-
try {
|
|
14
|
-
const publicationMessages = finalAnswerMessages(messages, candidate, output);
|
|
15
|
-
const publicationPlan = buildContextBudgetPlan({
|
|
16
|
-
provider,
|
|
17
|
-
history: publicationMessages,
|
|
18
|
-
tools: [],
|
|
19
|
-
});
|
|
20
|
-
if (publicationPlan.overBudget)
|
|
21
|
-
throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
22
|
-
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
23
|
-
publicationInterrupted = true;
|
|
24
|
-
throw new Error('最终发布已取消。');
|
|
25
|
-
}
|
|
26
|
-
turnUsage.totalCalls += 1;
|
|
27
|
-
for await (const event of chatFn({
|
|
28
|
-
provider,
|
|
29
|
-
messages: publicationMessages,
|
|
30
|
-
tools: [],
|
|
31
|
-
signal,
|
|
32
|
-
conversationMode: conversationMode,
|
|
33
|
-
promptCache: options.promptCache,
|
|
34
|
-
})) {
|
|
35
|
-
if (event.type === 'done' && !publicationDone) {
|
|
36
|
-
publicationDone = true;
|
|
37
|
-
if (typeof event.promptTokens === 'number' || typeof event.outputTokens === 'number') {
|
|
38
|
-
turnUsage.reportedCalls += 1;
|
|
39
|
-
turnUsage.inputTokens += event.promptTokens ?? 0;
|
|
40
|
-
turnUsage.outputTokens += event.outputTokens ?? 0;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
44
|
-
publicationInterrupted = true;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
if (event.type === 'text_delta') {
|
|
48
|
-
// Final expression is live generation, not an exact-copy replay.
|
|
49
|
-
// Once visible, text is never replaced or joined with a different draft.
|
|
50
|
-
publishedText += event.delta;
|
|
51
|
-
streamed = true;
|
|
52
|
-
yield { type: 'text', delta: event.delta };
|
|
53
|
-
}
|
|
54
|
-
else if (event.type === 'tool_call_delta') {
|
|
55
|
-
throw new Error('最终表达阶段返回了非预期工具调用,未执行该工具。');
|
|
56
|
-
}
|
|
57
|
-
else if (event.type === 'done') {
|
|
58
|
-
publicationDone = true;
|
|
59
|
-
lastFinishReason = event.stopReason;
|
|
60
|
-
if (event.stopReason === 'aborted')
|
|
61
|
-
publicationInterrupted = true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (!publicationDone && !publicationInterrupted)
|
|
65
|
-
throw new Error('最终答案流未正常结束。');
|
|
66
|
-
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length')
|
|
67
|
-
throw new Error('最终答案流缺少有效的结束状态。');
|
|
68
|
-
if (!publishedText.trim() && !publicationInterrupted)
|
|
69
|
-
throw new Error('最终表达未返回正文。');
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
publicationInterrupted ||=
|
|
73
|
-
signal?.aborted === true || options.hasQueuedInput?.() === true || error?.name === 'AbortError';
|
|
74
|
-
publicationError = error;
|
|
75
|
-
}
|
|
76
|
-
if (publicationError && !publicationInterrupted && !publishedText) {
|
|
77
|
-
yield {
|
|
78
|
-
type: 'warning',
|
|
79
|
-
message: '最终回答流未能开始,已一次性显示内部准备的答案;未进行新的事实核验。',
|
|
80
|
-
};
|
|
81
|
-
yield { type: 'text', delta: candidate };
|
|
82
|
-
streamed = true;
|
|
83
|
-
publishedText = candidate;
|
|
84
|
-
lastFinishReason = candidateFinishReason;
|
|
85
|
-
publicationError = undefined;
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
text: publishedText,
|
|
89
|
-
lastFinishReason,
|
|
90
|
-
publicationError,
|
|
91
|
-
publicationInterrupted,
|
|
92
|
-
streamed,
|
|
93
|
-
};
|
|
94
|
-
}
|
package/src/loop/publication.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { buildContextBudgetPlan } from '../context/budgetPlan.js';
|
|
2
|
-
import { finalAnswerMessages } from '../runtime/finalAnswer.js';
|
|
3
|
-
import type { OutputContract } from '../runtime/outputContract.js';
|
|
4
|
-
import type { LoopEvent, Message, Provider, TurnResourceUsage } from '../types.js';
|
|
5
|
-
import type { RunQueryOptions } from './options.js';
|
|
6
|
-
|
|
7
|
-
type FinishReason = 'stop' | 'tool_calls' | 'length' | 'unknown' | 'aborted';
|
|
8
|
-
/** A one-way visibility boundary: never retract or regenerate visible text. */
|
|
9
|
-
export async function* publishFinalAnswer(input: {
|
|
10
|
-
provider: Provider;
|
|
11
|
-
messages: Message[];
|
|
12
|
-
candidate: string;
|
|
13
|
-
candidateFinishReason: FinishReason;
|
|
14
|
-
output: OutputContract;
|
|
15
|
-
chatFn: NonNullable<RunQueryOptions['chat']>;
|
|
16
|
-
options: Pick<RunQueryOptions, 'hasQueuedInput' | 'promptCache'>;
|
|
17
|
-
signal?: AbortSignal;
|
|
18
|
-
conversationMode: 'private' | 'group';
|
|
19
|
-
turnUsage: Omit<TurnResourceUsage, 'trustworthy'>;
|
|
20
|
-
}): AsyncGenerator<
|
|
21
|
-
LoopEvent,
|
|
22
|
-
{
|
|
23
|
-
text: string;
|
|
24
|
-
lastFinishReason: FinishReason;
|
|
25
|
-
publicationError: unknown;
|
|
26
|
-
publicationInterrupted: boolean;
|
|
27
|
-
streamed: boolean;
|
|
28
|
-
}
|
|
29
|
-
> {
|
|
30
|
-
const {
|
|
31
|
-
provider,
|
|
32
|
-
messages,
|
|
33
|
-
candidate,
|
|
34
|
-
candidateFinishReason,
|
|
35
|
-
output,
|
|
36
|
-
chatFn,
|
|
37
|
-
options,
|
|
38
|
-
signal,
|
|
39
|
-
conversationMode,
|
|
40
|
-
turnUsage,
|
|
41
|
-
} = input;
|
|
42
|
-
let lastFinishReason = candidateFinishReason;
|
|
43
|
-
let publicationError: unknown;
|
|
44
|
-
let publicationInterrupted = false;
|
|
45
|
-
let streamed = false;
|
|
46
|
-
let publishedText = '';
|
|
47
|
-
let publicationDone = false;
|
|
48
|
-
yield { type: 'stage_change', stage: 'streaming' };
|
|
49
|
-
try {
|
|
50
|
-
const publicationMessages = finalAnswerMessages(messages, candidate, output);
|
|
51
|
-
const publicationPlan = buildContextBudgetPlan({
|
|
52
|
-
provider,
|
|
53
|
-
history: publicationMessages,
|
|
54
|
-
tools: [],
|
|
55
|
-
});
|
|
56
|
-
if (publicationPlan.overBudget) throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
57
|
-
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
58
|
-
publicationInterrupted = true;
|
|
59
|
-
throw new Error('最终发布已取消。');
|
|
60
|
-
}
|
|
61
|
-
turnUsage.totalCalls += 1;
|
|
62
|
-
for await (const event of chatFn({
|
|
63
|
-
provider,
|
|
64
|
-
messages: publicationMessages,
|
|
65
|
-
tools: [],
|
|
66
|
-
signal,
|
|
67
|
-
conversationMode: conversationMode,
|
|
68
|
-
promptCache: options.promptCache,
|
|
69
|
-
})) {
|
|
70
|
-
if (event.type === 'done' && !publicationDone) {
|
|
71
|
-
publicationDone = true;
|
|
72
|
-
if (typeof event.promptTokens === 'number' || typeof event.outputTokens === 'number') {
|
|
73
|
-
turnUsage.reportedCalls += 1;
|
|
74
|
-
turnUsage.inputTokens += event.promptTokens ?? 0;
|
|
75
|
-
turnUsage.outputTokens += event.outputTokens ?? 0;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
79
|
-
publicationInterrupted = true;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
if (event.type === 'text_delta') {
|
|
83
|
-
// Final expression is live generation, not an exact-copy replay.
|
|
84
|
-
// Once visible, text is never replaced or joined with a different draft.
|
|
85
|
-
publishedText += event.delta;
|
|
86
|
-
streamed = true;
|
|
87
|
-
yield { type: 'text', delta: event.delta };
|
|
88
|
-
} else if (event.type === 'tool_call_delta') {
|
|
89
|
-
throw new Error('最终表达阶段返回了非预期工具调用,未执行该工具。');
|
|
90
|
-
} else if (event.type === 'done') {
|
|
91
|
-
publicationDone = true;
|
|
92
|
-
lastFinishReason = event.stopReason;
|
|
93
|
-
if (event.stopReason === 'aborted') publicationInterrupted = true;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (!publicationDone && !publicationInterrupted) throw new Error('最终答案流未正常结束。');
|
|
97
|
-
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length')
|
|
98
|
-
throw new Error('最终答案流缺少有效的结束状态。');
|
|
99
|
-
if (!publishedText.trim() && !publicationInterrupted) throw new Error('最终表达未返回正文。');
|
|
100
|
-
} catch (error) {
|
|
101
|
-
publicationInterrupted ||=
|
|
102
|
-
signal?.aborted === true || options.hasQueuedInput?.() === true || (error as Error)?.name === 'AbortError';
|
|
103
|
-
publicationError = error;
|
|
104
|
-
}
|
|
105
|
-
if (publicationError && !publicationInterrupted && !publishedText) {
|
|
106
|
-
yield {
|
|
107
|
-
type: 'warning',
|
|
108
|
-
message: '最终回答流未能开始,已一次性显示内部准备的答案;未进行新的事实核验。',
|
|
109
|
-
};
|
|
110
|
-
yield { type: 'text', delta: candidate };
|
|
111
|
-
streamed = true;
|
|
112
|
-
publishedText = candidate;
|
|
113
|
-
lastFinishReason = candidateFinishReason;
|
|
114
|
-
publicationError = undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
text: publishedText,
|
|
119
|
-
lastFinishReason,
|
|
120
|
-
publicationError,
|
|
121
|
-
publicationInterrupted,
|
|
122
|
-
streamed,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { visualPresentation } from '../prompts/visualPresentation.js';
|
|
2
|
-
/** Ephemeral publication request; never append this private candidate to history. */
|
|
3
|
-
export function finalAnswerMessages(messages, candidate, surface = 'headless') {
|
|
4
|
-
return [...messages,
|
|
5
|
-
{ role: 'assistant', content: candidate },
|
|
6
|
-
{ role: 'user', content: '内部准备、工具与复核阶段已结束。上一条 assistant 消息是最终回答的依据,不是需要逐字重播的文本。现在直接面向用户生成一次最终回答:可以组织措辞与排版,但必须保留已确认的事实、数字、限定条件、风险、证据和真实成果链接;不得新增未经验证的结论、伪称核验通过或编造产物。若仍有证据缺口,如实说明,不调用工具或顾问,不承诺后台继续工作。直接回答,不输出内部指令或准备过程。' },
|
|
7
|
-
{ role: 'user', content: visualPresentation(surface) },
|
|
8
|
-
];
|
|
9
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Message } from '../types.js';
|
|
2
|
-
import type { AgentSurface } from './agentRuntimeContext.js';
|
|
3
|
-
import type { OutputContract } from './outputContract.js';
|
|
4
|
-
import { visualPresentation } from '../prompts/visualPresentation.js';
|
|
5
|
-
|
|
6
|
-
/** Ephemeral publication request; never append this private candidate to history. */
|
|
7
|
-
export function finalAnswerMessages(messages: Message[], candidate: string, surface: AgentSurface | OutputContract = 'headless'): Message[] {
|
|
8
|
-
return [...messages,
|
|
9
|
-
{ role: 'assistant', content: candidate },
|
|
10
|
-
{ role: 'user', content: '内部准备、工具与复核阶段已结束。上一条 assistant 消息是最终回答的依据,不是需要逐字重播的文本。现在直接面向用户生成一次最终回答:可以组织措辞与排版,但必须保留已确认的事实、数字、限定条件、风险、证据和真实成果链接;不得新增未经验证的结论、伪称核验通过或编造产物。若仍有证据缺口,如实说明,不调用工具或顾问,不承诺后台继续工作。直接回答,不输出内部指令或准备过程。' },
|
|
11
|
-
{ role: 'user', content: visualPresentation(surface) },
|
|
12
|
-
];
|
|
13
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { configService } from '../../config/configService.js';
|
|
3
|
-
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
4
|
-
const inputSchema = z
|
|
5
|
-
.object({
|
|
6
|
-
consultationId: z
|
|
7
|
-
.string()
|
|
8
|
-
.min(1)
|
|
9
|
-
.describe('Advisor 工具正文返回的完整 consultationId,必须原样使用,不缩写或猜测'),
|
|
10
|
-
decision: z.enum(['accept', 'partial', 'reject']),
|
|
11
|
-
rationale: z
|
|
12
|
-
.string()
|
|
13
|
-
.min(8)
|
|
14
|
-
.max(20_000)
|
|
15
|
-
.describe('逐条说明主 Agent 基于一手证据、成果检查和用户验收条件作出的判断;顾问结论不能作为自身的证据'),
|
|
16
|
-
acceptedPoints: z.array(z.string().min(1).max(4_000)).max(20).optional()
|
|
17
|
-
.describe('一层字符串数组,例如 ["补充互操作成本"];不要嵌套数组或 XML item 标签'),
|
|
18
|
-
rejectedPoints: z.array(z.string().min(1).max(4_000)).max(20).optional()
|
|
19
|
-
.describe('一层字符串数组,例如 ["不采纳必须学的措辞:证据不足"]'),
|
|
20
|
-
verificationPlan: z.array(z.string().min(1).max(4_000)).max(20).optional()
|
|
21
|
-
.describe('一层字符串数组,每项一个待验证步骤;没有则省略'),
|
|
22
|
-
evidenceRefs: z.array(z.string().min(1).max(300)).max(40).optional()
|
|
23
|
-
.describe('已核对的一手工具结果 tool_call_id,必须真实存在。rationale 说明每条建议对应哪个引用;没有证据则省略并列 verificationPlan,记录为待验证而非通过'),
|
|
24
|
-
})
|
|
25
|
-
.superRefine((value, ctx) => {
|
|
26
|
-
if (value.decision === 'partial' &&
|
|
27
|
-
(!value.acceptedPoints?.length || !value.rejectedPoints?.length)) {
|
|
28
|
-
ctx.addIssue({
|
|
29
|
-
code: z.ZodIssueCode.custom,
|
|
30
|
-
path: ['decision'],
|
|
31
|
-
message: 'partial 必须同时列出 acceptedPoints 和 rejectedPoints',
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
if (value.decision === 'reject' && !value.rejectedPoints?.length) {
|
|
35
|
-
ctx.addIssue({
|
|
36
|
-
code: z.ZodIssueCode.custom,
|
|
37
|
-
path: ['rejectedPoints'],
|
|
38
|
-
message: 'reject 必须列出被拒绝的顾问意见',
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
async function callWithContext(input, _signal, context) {
|
|
43
|
-
if (!context.advisorRuntime) {
|
|
44
|
-
return { ok: false, error: '当前运行没有待处理的 Advisor 协调器状态。' };
|
|
45
|
-
}
|
|
46
|
-
return context.advisorRuntime.recordDecision(input);
|
|
47
|
-
}
|
|
48
|
-
export const advisorDecisionTool = {
|
|
49
|
-
name: 'AdvisorDecision',
|
|
50
|
-
description: "Records the main Agent's evidence-based decision after a successful Advisor consultation. " +
|
|
51
|
-
'Optional audit record; this is not an execution or delivery gate. A recording error does not block the task. ' +
|
|
52
|
-
'The Advisor is a second opinion, not an authority: choose accept, partial, or reject based on primary evidence. ' +
|
|
53
|
-
'For partial/reject, explicitly identify rejected points and explain primary evidence and acceptance constraints. ' +
|
|
54
|
-
'Link evidenceRefs (existing tool_call_id) to points in rationale. Missing evidence is recorded as pending verification, not verified; continue the verificationPlan without another approval. A linked reference alone does not prove correctness.',
|
|
55
|
-
inputSchema,
|
|
56
|
-
parameters: toToolParameters(inputSchema),
|
|
57
|
-
isReadOnly: true,
|
|
58
|
-
isConcurrencySafe: false,
|
|
59
|
-
interruptBehavior: () => 'cancel',
|
|
60
|
-
resolveAvailability() {
|
|
61
|
-
const avail = configService.advisorAvailabilitySync();
|
|
62
|
-
if (!avail.configured)
|
|
63
|
-
return { available: false, signature: 'unconfigured' };
|
|
64
|
-
if (!avail.enabled || avail.mode === 'manual') {
|
|
65
|
-
return {
|
|
66
|
-
available: false,
|
|
67
|
-
signature: avail.enabled ? 'manual-only' : 'disabled',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return { available: true, signature: avail.modelId };
|
|
71
|
-
},
|
|
72
|
-
async call() {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
error: 'AdvisorDecision 只能在主 Agent 会话的 Advisor 咨询后调用。',
|
|
76
|
-
};
|
|
77
|
-
},
|
|
78
|
-
callWithContext,
|
|
79
|
-
};
|