minimal-agent 0.18.12 → 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.
Files changed (174) hide show
  1. package/package.json +1 -3
  2. package/plugins/workflow-runner/src/runtime/context.js +1 -1
  3. package/plugins/workflow-runner/src/runtime/context.ts +1 -1
  4. package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
  5. package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
  6. package/skills/ssh-remote-dev/SKILL.md +61 -0
  7. package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
  8. package/src/advisor/README.md +62 -24
  9. package/src/advisor/advisorNudge.js +199 -0
  10. package/src/advisor/advisorNudge.ts +232 -0
  11. package/src/advisor/deliveryReview.js +35 -12
  12. package/src/advisor/deliveryReview.ts +35 -11
  13. package/src/advisor/presentation.js +7 -10
  14. package/src/advisor/presentation.ts +10 -13
  15. package/src/advisor/reviewContext.js +22 -58
  16. package/src/advisor/reviewContext.ts +24 -67
  17. package/src/cli/configMenu.js +17 -128
  18. package/src/cli/configMenu.tsx +17 -193
  19. package/src/cli/print.js +2 -0
  20. package/src/cli/print.ts +2 -0
  21. package/src/cli/streamJson.js +2 -0
  22. package/src/cli/streamJson.ts +2 -0
  23. package/src/cli/wizards/advisorWizard.js +9 -7
  24. package/src/cli/wizards/advisorWizard.tsx +9 -7
  25. package/src/cli/wizards/contextAdvisorWizard.js +9 -7
  26. package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
  27. package/src/cli/wizards/providerPresets.js +27 -1
  28. package/src/cli/wizards/providerPresets.ts +44 -2
  29. package/src/cli/wizards/providerWizard.js +51 -10
  30. package/src/cli/wizards/providerWizard.tsx +59 -9
  31. package/src/config/configDefaults.js +4 -8
  32. package/src/config/configDefaults.ts +4 -8
  33. package/src/config/configDiagnostics.js +1 -21
  34. package/src/config/configDiagnostics.ts +3 -23
  35. package/src/config/configFile.js +11 -4
  36. package/src/config/configFile.ts +27 -7
  37. package/src/config/configManagement.js +1 -0
  38. package/src/config/configManagement.ts +2 -0
  39. package/src/config/configService.js +12 -8
  40. package/src/config/configService.ts +16 -11
  41. package/src/config/numericLimits.js +0 -4
  42. package/src/config/numericLimits.ts +0 -4
  43. package/src/config/reasoningEffort.js +16 -0
  44. package/src/config/reasoningEffort.ts +18 -0
  45. package/src/context/budgetPlan.js +1 -1
  46. package/src/context/budgetPlan.ts +1 -1
  47. package/src/context/optimizeConversation.js +1 -1
  48. package/src/context/optimizeConversation.ts +1 -1
  49. package/src/harness/stallGuard.js +8 -13
  50. package/src/harness/stallGuard.ts +8 -12
  51. package/src/llm/client.js +64 -17
  52. package/src/llm/client.ts +62 -7
  53. package/src/llm/providers/openaiFormat.js +14 -3
  54. package/src/llm/providers/openaiFormat.ts +17 -3
  55. package/src/llm/providers/responsesFormat.js +275 -0
  56. package/src/llm/providers/responsesFormat.ts +306 -0
  57. package/src/loop/README.md +18 -3
  58. package/src/loop/liveDiagramGate.js +63 -0
  59. package/src/loop/liveDiagramGate.ts +61 -0
  60. package/src/loop/messages.js +12 -0
  61. package/src/loop/messages.ts +13 -0
  62. package/src/loop/options.ts +7 -2
  63. package/src/loop/toolBatch.js +0 -1
  64. package/src/loop/toolBatch.ts +0 -1
  65. package/src/loop/toolExecutor.js +11 -46
  66. package/src/loop/toolExecutor.ts +12 -55
  67. package/src/loop/toolPresentation.js +2 -13
  68. package/src/loop/toolPresentation.ts +2 -13
  69. package/src/loop/toolSemantics.js +3 -4
  70. package/src/loop/toolSemantics.ts +4 -6
  71. package/src/loop.js +222 -214
  72. package/src/loop.ts +226 -232
  73. package/src/memory/dependencies.js +11 -3
  74. package/src/memory/dependencies.ts +11 -3
  75. package/src/memory/episodicMemory.js +59 -18
  76. package/src/memory/episodicMemory.ts +58 -18
  77. package/src/memory/experienceStore.js +4 -3
  78. package/src/memory/experienceStore.ts +4 -3
  79. package/src/memory/factCandidates.js +53 -1
  80. package/src/memory/factCandidates.ts +54 -1
  81. package/src/memory/factLearning.js +2 -2
  82. package/src/memory/factLearning.ts +2 -2
  83. package/src/memory/jobPolicy.js +24 -5
  84. package/src/memory/jobPolicy.ts +25 -5
  85. package/src/memory/learning.js +2 -2
  86. package/src/memory/learning.ts +2 -2
  87. package/src/memory/learningModel.js +7 -0
  88. package/src/memory/learningModel.ts +7 -0
  89. package/src/memory/managementService.js +8 -7
  90. package/src/memory/managementService.ts +16 -8
  91. package/src/memory/memoryService.js +104 -33
  92. package/src/memory/memoryService.ts +132 -28
  93. package/src/memory/projectKnowledge.js +39 -3
  94. package/src/memory/projectKnowledge.ts +45 -3
  95. package/src/memory/recallQuery.js +56 -0
  96. package/src/memory/recallQuery.ts +58 -0
  97. package/src/memory/retrieval.js +39 -10
  98. package/src/memory/retrieval.ts +46 -11
  99. package/src/memory/turnJournal.js +75 -10
  100. package/src/memory/turnJournal.ts +66 -10
  101. package/src/memory/types.ts +7 -1
  102. package/src/prompts/delegation.js +2 -1
  103. package/src/prompts/delegation.ts +3 -1
  104. package/src/prompts/policyRegistry.js +9 -12
  105. package/src/prompts/policyRegistry.ts +9 -12
  106. package/src/prompts/promptAssembly.js +6 -5
  107. package/src/prompts/promptAssembly.ts +8 -6
  108. package/src/prompts/visualPresentation.js +39 -10
  109. package/src/prompts/visualPresentation.ts +38 -9
  110. package/src/runtime/answerPreparation.js +1 -1
  111. package/src/runtime/answerPreparation.ts +1 -1
  112. package/src/runtime/fileLock.js +19 -3
  113. package/src/runtime/fileLock.ts +19 -3
  114. package/src/runtime/invocationPolicy.js +1 -1
  115. package/src/runtime/invocationPolicy.ts +1 -1
  116. package/src/runtime/outputContract.js +0 -4
  117. package/src/runtime/outputContract.ts +0 -7
  118. package/src/session/runAgentTurn.js +3 -0
  119. package/src/session/runAgentTurn.ts +3 -0
  120. package/src/team/onlineToolContract.js +0 -1
  121. package/src/team/onlineToolContract.ts +0 -1
  122. package/src/tools/advisor/advisor.js +210 -358
  123. package/src/tools/advisor/advisor.ts +214 -376
  124. package/src/tools/agent/agentTypes.js +0 -1
  125. package/src/tools/agent/agentTypes.ts +0 -1
  126. package/src/tools/agent/coordinator.js +4 -1
  127. package/src/tools/agent/coordinator.ts +4 -1
  128. package/src/tools/bash/semantics.js +4 -5
  129. package/src/tools/bash/semantics.ts +4 -5
  130. package/src/tools/capabilities.js +2 -3
  131. package/src/tools/capabilities.ts +2 -4
  132. package/src/tools/index.js +0 -16
  133. package/src/tools/index.ts +0 -16
  134. package/src/tools/memory/maintenance.js +8 -9
  135. package/src/tools/memory/maintenance.ts +13 -11
  136. package/src/tools/memory/memory.js +5 -1
  137. package/src/tools/memory/memory.ts +5 -0
  138. package/src/tools/optimizeConversation/README.md +17 -6
  139. package/src/tools/toolPromptGuides.js +2 -9
  140. package/src/tools/toolPromptGuides.ts +2 -9
  141. package/src/types.ts +28 -105
  142. package/src/ui/App.js +2 -0
  143. package/src/ui/App.tsx +1 -0
  144. package/src/ui/MessageList.js +8 -44
  145. package/src/ui/MessageList.tsx +3 -81
  146. package/src/ui/hooks/useChat.js +2 -0
  147. package/src/ui/hooks/useChat.ts +2 -0
  148. package/src/ui/toolProgress.js +1 -1
  149. package/src/ui/toolProgress.ts +1 -1
  150. package/src/web/protocol.ts +10 -3
  151. package/src/advisor/advisorCoordinator.js +0 -734
  152. package/src/advisor/advisorCoordinator.ts +0 -825
  153. package/src/cli/wizards/sshHostWizard.js +0 -156
  154. package/src/cli/wizards/sshHostWizard.tsx +0 -214
  155. package/src/context/sshHosts.js +0 -290
  156. package/src/context/sshHosts.ts +0 -312
  157. package/src/loop/publication.js +0 -94
  158. package/src/loop/publication.ts +0 -124
  159. package/src/runtime/finalAnswer.js +0 -9
  160. package/src/runtime/finalAnswer.ts +0 -13
  161. package/src/tools/advisor/advisorDecision.js +0 -79
  162. package/src/tools/advisor/advisorDecision.ts +0 -92
  163. package/src/tools/ssh/ssh.js +0 -152
  164. package/src/tools/ssh/ssh.ts +0 -167
  165. package/src/tools/ssh/sshAvailability.js +0 -35
  166. package/src/tools/ssh/sshAvailability.ts +0 -39
  167. package/src/tools/ssh/sshClient.js +0 -177
  168. package/src/tools/ssh/sshClient.ts +0 -189
  169. package/src/tools/ssh/sshDownload.js +0 -77
  170. package/src/tools/ssh/sshDownload.ts +0 -92
  171. package/src/tools/ssh/sshHostsTool.js +0 -36
  172. package/src/tools/ssh/sshHostsTool.ts +0 -48
  173. package/src/tools/ssh/sshUpload.js +0 -72
  174. package/src/tools/ssh/sshUpload.ts +0 -85
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.18.12",
3
+ "version": "0.18.14",
4
4
  "description": "Bun-first 的本地 Agent 内核,支持工具、插件、工作流、上下文压缩、长期记忆与 OpenAI 兼容接口;Tauri 桌面宿主和 Node.js npm 发布可复用同一内核",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Bill Wang <leiwang0359@gmail.com>",
@@ -215,7 +215,6 @@
215
215
  "proper-lockfile": "^4.1.2",
216
216
  "react": "^18.3.1",
217
217
  "sharp": "^0.34.5",
218
- "ssh2": "^1.17.0",
219
218
  "turndown": "^7.2.4",
220
219
  "undici": "^6.28.1",
221
220
  "vscode-jsonrpc": "^8.2.1",
@@ -228,7 +227,6 @@
228
227
  "@types/node": "^20.14.0",
229
228
  "@types/proper-lockfile": "^4.1.4",
230
229
  "@types/react": "^18.3.0",
231
- "@types/ssh2": "^1.15.6",
232
230
  "bun-types": "^1.3.13",
233
231
  "typescript": "^5.5.0"
234
232
  },
@@ -503,7 +503,7 @@ export function createWorkflowContext(deps) {
503
503
  maxTurns: opts.maxTurns,
504
504
  allowedTools: allowed,
505
505
  toolBoundaryLabel: '当前工作流节点',
506
- advisorCheckpoints: opts.toolMode !== 'read-only',
506
+ advisorReminders: opts.toolMode !== 'read-only',
507
507
  availableTools: ctx.tools,
508
508
  ...(opts.allowedTools === undefined ? {} : { requiredTools: allowed }),
509
509
  ...executionServices(requestSignal, opts.toolMode),
@@ -749,7 +749,7 @@ export function createWorkflowContext(deps: ApiDeps): WorkflowContext {
749
749
  maxTurns: opts.maxTurns,
750
750
  allowedTools: allowed,
751
751
  toolBoundaryLabel: '当前工作流节点',
752
- advisorCheckpoints: opts.toolMode !== 'read-only',
752
+ advisorReminders: opts.toolMode !== 'read-only',
753
753
  availableTools: ctx.tools,
754
754
  ...(opts.allowedTools === undefined ? {} : { requiredTools: allowed }),
755
755
  ...executionServices(requestSignal, opts.toolMode),
@@ -36,7 +36,6 @@ const tools: Tool[] = [
36
36
  'Write',
37
37
  'Skill',
38
38
  'Advisor',
39
- 'AdvisorDecision',
40
39
  'PresentResult',
41
40
  'AskUserQuestion',
42
41
  ].map((name) => ({
@@ -165,8 +164,7 @@ test('embedded tools exclude outer delivery/advisor/ask, preserve explicitly req
165
164
  expect(names).toContain('Read');
166
165
  for (const name of [
167
166
  'Advisor',
168
- 'AdvisorDecision',
169
- 'PresentResult',
167
+ 'PresentResult',
170
168
  'AskUserQuestion',
171
169
  ])
172
170
  expect(names).not.toContain(name);
@@ -177,8 +175,7 @@ test('embedded tools exclude outer delivery/advisor/ask, preserve explicitly req
177
175
  allowedTools: [
178
176
  'Read',
179
177
  'Advisor',
180
- 'AdvisorDecision',
181
- 'PresentResult',
178
+ 'PresentResult',
182
179
  'AskUserQuestion',
183
180
  ],
184
181
  isolated: true,
@@ -6,8 +6,21 @@ import { executeWorkflowFile } from '../src/runtime/exec.js';
6
6
 
7
7
  const dirs: string[] = [];
8
8
  afterEach(async () => {
9
- for (const dir of dirs.splice(0))
10
- await rm(dir, { recursive: true, force: true });
9
+ // Windows keeps a just-terminated process's cwd busy for ~200ms after the
10
+ // async taskkill (EBUSY). Retry instead of failing a test that passed; a
11
+ // genuinely leaked process would still hold the directory after 5s and fail.
12
+ // Explicit loop: Bun's rm does not honor maxRetries.
13
+ for (const dir of dirs.splice(0)) {
14
+ for (let attempt = 0; ; attempt++) {
15
+ try {
16
+ await rm(dir, { recursive: true, force: true });
17
+ break;
18
+ } catch (error) {
19
+ if ((error as NodeJS.ErrnoException).code !== 'EBUSY' || attempt >= 50) throw error;
20
+ await new Promise((resolve) => setTimeout(resolve, 100));
21
+ }
22
+ }
23
+ }
11
24
  });
12
25
  async function script(source: string) {
13
26
  const cwd = await mkdtemp(join(tmpdir(), 'wf-process-'));
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: ssh-remote-dev
3
+ description: Develop, inspect, test, and transfer files in a user-authorized remote working directory over SSH. Use when work must happen on a configured SSH host rather than only in the local workspace; prefer SSH config aliases and key-based authentication. Do not use for non-SSH services or as permission to access hosts the user has not placed in scope.
4
+ ---
5
+
6
+ # SSH Remote Development
7
+
8
+ Operate remote repositories through the system `ssh` and `scp` clients. Treat the remote host and filesystem as external state: this skill supplies a workflow, not standing authorization for unrelated hosts or destructive actions.
9
+
10
+ ## Establish the target
11
+
12
+ - Resolve the SSH destination from the user's named `~/.ssh/config` alias when possible. Never expose private-key contents, passwords, passphrases, tokens, or full SSH configuration in output.
13
+ - Establish one explicit remote working directory for the task. If the alias or directory is genuinely missing, ask for only that missing value.
14
+ - Prefer non-interactive key or SSH-agent authentication. Use `-o BatchMode=yes` and a short `-o ConnectTimeout=10` for probes so a command cannot hang on a password prompt.
15
+ - When the authorized host only supports a password, start `ssh` or `sftp` with a TTY and send the password only to the live prompt. Never place it in the command line, an environment variable, an askpass helper, a generated script, a project file, or the skill. Keep one bounded interactive session when practical instead of repeatedly handling the password, and close it when the task ends.
16
+ - Start with a read-only probe that prints the canonical directory, platform/shell information when relevant, and repository status. Verify that the canonical path is the intended target before any write.
17
+ - Each SSH invocation is stateless. Include the directory and required environment setup in every command, for example `cd -- '<remote-root>' && <command>`. Do not assume a previous `cd`, exported variable, virtual environment, or shell session persists.
18
+ - Default to a POSIX shell only when the probe confirms one. Adapt commands for a Windows/PowerShell remote instead of forcing Bash syntax.
19
+
20
+ ## Work in the remote repository
21
+
22
+ - Inspect before changing: read the relevant files, repository instructions, current branch, `git status --short`, and focused diffs. Preserve pre-existing edits and untracked files as user work.
23
+ - Disable Git paging per invocation for captured output, for example `git -c core.pager=cat status --short` or `git -c core.pager=cat diff --stat`. An interactive pager can consume later commands as keystrokes; if one opens unexpectedly, quit it before sending anything else.
24
+ - Keep remote commands narrowly scoped and outputs bounded. Prefer targeted searches and `head`, `tail`, or equivalent over dumping large files or logs.
25
+ - Run the smallest useful verification after changes, then report the exact remote path, files changed, and test result.
26
+ - Never run `git reset --hard`, checkout-based discard, force push, recursive deletion, or broad cleanup unless the user explicitly requests that exact destructive outcome. Validate canonical targets immediately before an authorized destructive operation.
27
+ - For a recoverable removal, prefer moving the item under `<remote-root>/.codex-trash/<timestamp>/` and tell the user where it went. Do not claim this is recoverable if it crosses filesystems or the move failed.
28
+
29
+ ## Edit files safely
30
+
31
+ For a trivial, deterministic change, a carefully quoted remote command is acceptable. For non-trivial source edits, use this conflict-aware workflow:
32
+
33
+ 1. Record the remote file's identity and hash (`sha256sum` or platform equivalent), and whether it exists.
34
+ 2. Download it to a task-specific staging directory inside the local writable workspace.
35
+ 3. Edit the staged file with `apply_patch` and run any useful local checks.
36
+ 4. Recheck the remote hash immediately before upload. If it changed, stop and reconcile instead of overwriting concurrent work.
37
+ 5. Upload to a uniquely named temporary file in the same remote directory. Preserve the original mode when relevant, then atomically rename the temporary file over the destination.
38
+ 6. Clean up only the temporary artifact created by this workflow. Re-read or hash the installed file and inspect `git diff` remotely.
39
+
40
+ For a new file, first verify the destination still does not exist. For multiple related files, use one task-specific staging tree and repeat the conflict check for every existing destination. Avoid brittle `sed`/regex rewrites for semantic code changes.
41
+
42
+ ## Transfer files
43
+
44
+ - Use `scp -- <local-path> <alias>:<remote-path>` for uploads and the inverse for downloads. Quote local and remote paths independently; do not construct commands from untrusted remote output.
45
+ - Keep local downloads within the current workspace or another user-approved writable directory.
46
+ - Upload writes are external mutations. Use the temporary-upload-and-rename pattern for files that replace existing content.
47
+ - Do not add `StrictHostKeyChecking=no`. If host-key verification fails or a stored key changes, stop and ask the user to verify the fingerprint through a trusted channel.
48
+ - For a large read-only code review, a selective local mirror is acceptable: copy only first-party source, tests, manifests, and sanitized configuration examples into a clearly named task cache. Exclude dependency trees, build outputs, caches, logs, data, model weights, nested third-party repositories, and live secret files. Treat the mirror as non-authoritative, do not run Git operations from it, and re-read remote files before any later edit.
49
+
50
+ ## Long-running work
51
+
52
+ - Do not hold one SSH call open indefinitely. For a user-requested service or long test on a POSIX host, use `nohup`/`setsid` with a task-specific log and PID file, return promptly, and verify it in a fresh SSH call.
53
+ - A timeout or disconnected channel does not prove whether a remote command stopped. Check the PID, log, process marker, or expected artifact before retrying so duplicate jobs are not started.
54
+ - Stop only the recorded PID after verifying its command line belongs to this task. Do not use broad `pkill` patterns when an exact PID is available.
55
+
56
+ ## Permissions and failures
57
+
58
+ - Request host/network execution approval when the runtime requires it. A skill cannot bypass the sandbox or approval system.
59
+ - Do not install packages, change SSH server configuration, alter firewall rules, modify system services, or write outside the agreed remote working directory unless the user separately authorizes it.
60
+ - On authentication failure, report the alias and failure class without echoing credentials. On network failure, distinguish DNS, timeout, refusal, host-key, and authentication errors before retrying.
61
+ - Limit automatic retries to one for transient connection failures. Do not retry commands with uncertain side effects until their remote result has been checked.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "SSH Remote Development"
3
+ short_description: "Safely develop in a remote workspace over SSH"
4
+ default_prompt: "Use $ssh-remote-dev to inspect and develop in my configured remote SSH workspace."
@@ -1,24 +1,62 @@
1
- # 自动顾问与文字交付契约
2
-
3
- TUI 和 Desktop 复用 `runQuery` 的检查点与工具事件,不在前端自行发起第二模型请求。
4
- 仅已配置、启用且为自动模式的 Advisor 才进入有效工具集;仅手动模式保持手动调用。
5
-
6
- 实质交付不限定为文件。调研结论、计算、方案比较、推荐也需要第二意见。
7
- 提示词要求主模型主动识别;运行时补充确定性的文本信号(分析请求对应的
8
- 非简短回答、含数字的成本/风险/推荐等),并保留变更、验证、正式成果与调查证据触发器。
9
- 这些信号是可测试的启发式,不是完整语义分类器;短小含蓄的结论仍可能漏判,
10
- 没有任何长度单独触发规则。不要将“自信”或“没有文件”当作豁免条件。
11
-
12
- 执行前、连续失败、文字与文件交付共享待复核检查点。若模型仍试图继续解决或交付,
13
- 宿主补一次对应 reason 的 Advisor 调用;用户明确不满、证据冲突、无法判断的表达
14
- 也可建立检查点。只读诊断和人类澄清仍可进行。
15
- 通过同一工具权限、取消、历史和 UI 事件通道执行,并将最新待交付草稿交给顾问。
16
- 主模型读取意见后重新生成操作或回复,不执行咨询前预生成的后续操作。
17
- 已完成同阶段咨询不重复调用;交付复核后成果又发生写入,重新开启交付复核;
18
- 新增证据后的新冲突可以再问,普通读取本身不重开交付检查。
19
- 明确仅修正拼写/文案的小修改不机械进行执行前或变更交付检查。
20
- 顾问失败、被权限拒绝或达到执行上限时允许有界降级,并明确提示未复核;不无限重试,
21
- 不将未调用、调用失败或顾问意见冒充验证证据。忽略提醒不再算尝试,
22
- 实际失败/拒绝作为咨询结果留痕,未执行的检查点跨中断保留。
23
-
24
- 回归入口:`bun test test/deliveryReview.test.ts test/loop.advisorGate.test.ts`。
1
+ # 顾问:模型决定,宿主只提醒
2
+
3
+ 顾问的用法向 Claude Code 自带的 advisor 看齐:**谁来问、什么时候问由主模型自己判断**
4
+ (规则写在 `prompts/policyRegistry.ts::capability.advisor` 和 `tools/advisor/advisor.ts::description`),
5
+ 顾问看到的是**整段对话**(上下文细则见 `CONTEXT.md`)。宿主不拦截、不扣答案、不替模型调用——
6
+ 这三样是旧 `AdvisorCoordinator`(867 行)的主要复杂度来源,而且都以降低回答实时性、制造误伤为代价:
7
+ 开着顾问时每个回答被藏起来整块出现,写文件被暂缓,合成的 Advisor 调用以模型名义写进历史。已全部移除,
8
+ 连同 `AdvisorDecision`(采纳留痕)和 `shareSensitive` 开关。
9
+
10
+ ## 宿主做的唯一一件事:AdvisorNudge
11
+
12
+ `advisorNudge.ts` 只在一次 `runQuery` 内记账(改动、验证、调查证据、成功的咨询次数、卡住信号),
13
+ 产出两样东西:
14
+
15
+ 1. **提醒**——写进下一次请求的动态上下文(`loop.ts::buildRequestTransientContext`):
16
+ - 实质请求(分析/调研/比较/计算/设计/推荐/成本或风险判断,或已调查够多)且还没咨询、还没动手:
17
+ "要动手修改,或要给出分析结论之前,先咨询一次";
18
+ - 已有实质成果、自上次咨询后又有新进展:给出最终结论或交付之前先咨询;
19
+ - 同一问题连续失败/没有进展:继续重试之前先咨询(只读诊断和换路径不受影响);
20
+ - 请求里有明显不满/证据冲突/无法判断的表达:先核查再咨询。
21
+ 文字不带计数——它只在状态翻转时变化,不会每个请求都改写缓存前缀。顾问不可用、
22
+ `advisorReminders:false`(只读内嵌节点)、简短即时问答、纯文案/routine 维护请求:都不提醒。
23
+ 2. **一条非阻塞 warning**——最终回答发布时,实质成果仍没经顾问复核则提示一次
24
+ ("没有经过顾问复核;不能把它视为已复核")。回答本身已经实时显示,warning 在它之后。
25
+ 澄清提问和一两句话不算成果(草稿只能豁免自己,不能触发)。
26
+
27
+ "请求是否实质"仍是写之前就能知道的启发式(`deliveryReview.ts::requestNeedsTextReview`):
28
+ "继续/展开第二点"沿用上一条请求的判定,"计算机/对比度"这类只是含了分析词的不算。
29
+ 这是可测试的启发式,不是语义分类器;不要把"自信"或"没有文件"当成豁免条件。
30
+
31
+ ## 弱模型兜底:贴近动作的提示 + 可选的补问(`advisor.enforcement`)
32
+
33
+ 动态上下文里的提醒离模型的下一步很远,弱基座模型多轮之后基本读不到。两处补强,都不拦截、不扣答案、不伪造调用:
34
+
35
+ 1. **首次改动提示**(始终开启):实质请求里第一次动手改东西、却还没咨询过——提示作为一行文字贴在
36
+ **那条工具结果**末尾(`takeActionHint`,整个运行内一次)。工具照常执行;工具结果是模型下一次请求里最近的内容。
37
+ 2. **补问回合**(配置 `advisor.enforcement: 'followup'`,默认 `'remind'`):实质回答发布后仍未复核,
38
+ loop 追加一条系统提示用户消息(`pushAdvisorFollowUpMarker`)再发一次请求,让**模型自己**调用 Advisor——
39
+ 回答不撤回,Advisor 调用是模型发出的。每个 `runQuery` 最多一次;模型不理会就由 warning 收尾。
40
+ 代价是多一次全上下文请求,只给不会主动咨询的基座开。`RunQueryOptions.advisorFollowUp` 可显式覆盖配置。
41
+
42
+ ## 唯一的"暂缓":同一批里排在 Advisor 之后的调用
43
+
44
+ 模型一次响应里同时发出 `Advisor` 和后续调用时,排在 Advisor **之后**的调用不执行(回给模型的是
45
+ `disposition: 'deferred'` 的说明),让它先读完意见再重新规划——与自己用顾问的方式一致:"读完意见再操作"。
46
+ 排在 Advisor 之前的调用(比如同批的 Read)照常执行,并且已经在顾问收到的历史里
47
+ (`test/advisorBackground.test.ts` 锁这条)。这只是一次批内的顺序规则,不是检查点:顾问不会因此
48
+ 推迟任何独立的工具调用,也不会扣下回答。
49
+
50
+ ## 谁来停住重复失败
51
+
52
+ `StallGuard`:同一调用连续两次返回相同失败,第三次在执行前被拒绝——**与顾问是否可用无关**。
53
+ 顾问只提醒之后,没有别的东西能停住循环。成功的咨询会让守卫清零(`noteConsultation`),
54
+ 否则顾问建议"修好环境再试"时同一条命令仍被拒。
55
+
56
+ ## 降级
57
+
58
+ 每次咨询只尝试一次;不可用、失败、空答、超时都以 `ok:false` 回到主模型,由它自行继续,
59
+ 不自动重试、不转成人工审批、不宣称获得验证。不设咨询配额。
60
+
61
+ 回归入口:`bun test test/advisorNudge.test.ts test/loop.advisorNudge.test.ts test/deliveryReview.test.ts
62
+ test/advisorContextFidelity.test.ts test/advisor.test.ts`。
@@ -0,0 +1,199 @@
1
+ import { isCosmeticRequest, isReviewProcessQuestion, isRoutineMaintenancePath, isRoutineMaintenanceRequest, needsTextDeliveryReview, requestNeedsTextReview, reviewSignal, } from './deliveryReview.js';
2
+ /**
3
+ * ============================================================
4
+ * src/advisor/advisorNudge.ts —— 顾问的"提醒",不是"闸门"
5
+ * ------------------------------------------------------------
6
+ * 谁来问顾问由模型自己决定(时机规则在提示词与 Advisor 工具说明里),宿主只做一件事:
7
+ * 把"你还没问"这个事实,用动态上下文里的一行话告诉模型。
8
+ *
9
+ * 它**不**拦截工具、**不**扣下回答、**不**替模型伪造 Advisor 调用——这三样是旧
10
+ * AdvisorCoordinator 的主要复杂度来源,而且都以降低回答的实时性、增加误伤为代价
11
+ * (开着顾问时每个回答被藏起来整块出现;写文件被暂缓;合成的 Advisor 调用以模型名义
12
+ * 写进历史)。提醒是否被采纳由模型判断;没被采纳时,唯一的后果是最终回答附一条
13
+ * 非阻塞的 warning,如实说明"这份实质成果没经顾问复核"。
14
+ *
15
+ * 只在**一次 runQuery** 内记账,不跨回合恢复(上一回合问没问过,与这一回合无关)。
16
+ *
17
+ * 提醒文字刻意不带计数:它进入当轮请求的易变上下文,文字稳定则只在状态翻转时
18
+ * 改变一两次,不会每个请求都改写前缀。
19
+ * ============================================================
20
+ */
21
+ const INVESTIGATION_TOOLS = new Set([
22
+ 'Read', 'Glob', 'Grep', 'LSP', 'WebSearch', 'WebFetch', 'WebBrowser',
23
+ 'TerminalCapture', 'RecallMemory', 'SearchConversations',
24
+ ]);
25
+ const MUTATING_ACTIVITY = new Set(['created', 'modified', 'generated', 'deleted']);
26
+ /** 调查证据攒到这个数,就算做了足以需要复核的实事(与旧触发阈值一致)。 */
27
+ const EVIDENCE_THRESHOLD = 3;
28
+ export class AdvisorNudge {
29
+ #request;
30
+ #previousRequest;
31
+ #available;
32
+ #consultations = 0;
33
+ /** 进度计数:每次改动、验证、调查证据 +1。咨询时记下当时的值。 */
34
+ #progress = 0;
35
+ #consultedAtProgress = -1;
36
+ #mutations = 0;
37
+ #nonRoutineMutation = false;
38
+ #verifications = 0;
39
+ #evidence = 0;
40
+ #delivered = false;
41
+ #signal;
42
+ #stalled = new Set();
43
+ #warned = false;
44
+ #followedUp = false;
45
+ #actionHintDue = false;
46
+ #actionHinted = false;
47
+ constructor(input) {
48
+ this.#request = input.currentRequest;
49
+ this.#previousRequest = input.previousRequest;
50
+ this.#available = input.advisorAvailable;
51
+ this.#signal = reviewSignal(input.currentRequest);
52
+ }
53
+ setAvailable(available) {
54
+ this.#available = available;
55
+ }
56
+ /** 本次运行内成功咨询的次数。 */
57
+ get consultations() {
58
+ return this.#consultations;
59
+ }
60
+ observeToolResult(call, result, semantics) {
61
+ if (!result.ok && result.disposition === 'deferred')
62
+ return;
63
+ const name = call.function.name;
64
+ if (name === 'Advisor') {
65
+ if (result.ok) {
66
+ this.#consultations++;
67
+ this.#consultedAtProgress = this.#progress;
68
+ this.#stalled.clear();
69
+ this.#signal = undefined;
70
+ }
71
+ return;
72
+ }
73
+ if (name === 'OptimizeConversation')
74
+ return;
75
+ const mutationsBefore = this.#mutations;
76
+ const metadata = result.metadata;
77
+ if (metadata?.kind === 'process_execution' && metadata.commandKind !== 'other'
78
+ && ['completed', 'failed'].includes(metadata.status)) {
79
+ this.#verifications++;
80
+ this.#progress++;
81
+ }
82
+ if (!result.ok)
83
+ return;
84
+ if (metadata?.kind === 'file_activity') {
85
+ const changes = metadata.files.filter((file) => MUTATING_ACTIVITY.has(file.activity));
86
+ this.#mutations += changes.length;
87
+ this.#progress += changes.length;
88
+ this.#nonRoutineMutation ||= changes.some((file) => file.activity === 'deleted' || !isRoutineMaintenancePath(file.relativePath));
89
+ }
90
+ else if (semantics?.mutatesWorkspace) {
91
+ this.#mutations++;
92
+ this.#progress++;
93
+ this.#nonRoutineMutation = true;
94
+ }
95
+ if (metadata?.kind === 'result_manifest') {
96
+ this.#delivered = true;
97
+ this.#progress++;
98
+ }
99
+ if (INVESTIGATION_TOOLS.has(name)) {
100
+ this.#evidence++;
101
+ this.#progress++;
102
+ }
103
+ // 第一次动手改东西、却还没问过顾问:提示贴在这条工具结果上(见 takeActionHint)。
104
+ if (this.#mutations > mutationsBefore && this.#available && this.#consultations === 0
105
+ && !this.#actionHinted && this.#substantive()) {
106
+ this.#actionHintDue = true;
107
+ }
108
+ }
109
+ /**
110
+ * 贴在"第一次改动"那条工具结果末尾的一行提示(整个运行内最多一次)。
111
+ * 动态上下文里的提醒离模型的下一步很远,弱模型多轮之后基本读不到;工具结果是它下一次
112
+ * 请求里最近的内容。工具照常执行,这只是文字,不推迟、不拦截。
113
+ */
114
+ takeActionHint() {
115
+ if (!this.#actionHintDue)
116
+ return '';
117
+ this.#actionHintDue = false;
118
+ this.#actionHinted = true;
119
+ return '[Advisor 提醒] 你已开始动手改动,但本回合还没有咨询顾问。继续下一步之前先调用 Advisor 复核方案;简短的即时问答不需要。';
120
+ }
121
+ /** 连续同类失败或没有进展:继续重试之前先问(提醒,不阻止只读诊断或换路径)。 */
122
+ notifyStalled(signature) {
123
+ this.#stalled.add(signature);
124
+ }
125
+ /** 本回合做过足以需要复核的实事(改动、验证、正式成果、足量调查证据)。 */
126
+ #hasWork() {
127
+ return (this.#mutations > 0 && !isCosmeticRequest(this.#request))
128
+ || this.#verifications > 0
129
+ || this.#delivered
130
+ || this.#evidence >= EVIDENCE_THRESHOLD;
131
+ }
132
+ /** 这个请求是否需要第二意见:做了实事,或请求本身就是分析/比较/推荐/成本风险判断。 */
133
+ #substantive() {
134
+ if (isReviewProcessQuestion(this.#request) && this.#mutations === 0 && !this.#delivered)
135
+ return false;
136
+ if (isRoutineMaintenanceRequest(this.#request) && !this.#nonRoutineMutation && !this.#delivered)
137
+ return false;
138
+ return this.#hasWork() || requestNeedsTextReview(this.#request, this.#previousRequest);
139
+ }
140
+ /** 最近一次咨询之后,是否又有了新的进展(或根本没问过)。 */
141
+ #unreviewed() {
142
+ return this.#consultedAtProgress < 0 || this.#progress > this.#consultedAtProgress;
143
+ }
144
+ /** 要交付的实质成果是否缺一次复核;最终回答发布时据此决定是否附 warning。 */
145
+ needsDeliveryReview() {
146
+ return this.#available && this.#substantive() && this.#unreviewed();
147
+ }
148
+ /** 下一次请求的动态提醒。顾问不可用时为空。 */
149
+ reminder() {
150
+ if (!this.#available)
151
+ return '';
152
+ const lines = [];
153
+ if (this.#stalled.size > 0) {
154
+ lines.push('Advisor 提醒:检测到同一问题连续失败或执行没有进展。继续重试之前先咨询 Advisor(只读诊断和换路径不受影响)。');
155
+ }
156
+ if (this.#signal && this.#consultations === 0) {
157
+ lines.push(this.#signal === 'user_dissatisfaction'
158
+ ? 'Advisor 提醒:用户对结果明显不满。先核查已有材料,再咨询 Advisor 对照用户的验收项。'
159
+ : this.#signal === 'evidence_conflict'
160
+ ? 'Advisor 提醒:材料之间存在冲突。核查后仍无法判断哪边对,请咨询 Advisor。'
161
+ : 'Advisor 提醒:你可能还不确定该怎么处理。动手之前可以先咨询 Advisor。');
162
+ }
163
+ const substantive = this.#substantive();
164
+ if (substantive && this.#consultations === 0 && this.#mutations === 0 && !this.#delivered) {
165
+ lines.push('Advisor 提醒:本回合还没有咨询顾问。要动手修改,或要给出分析结论之前,先咨询一次 Advisor;简短的即时问答不需要。');
166
+ }
167
+ else if (substantive && this.#unreviewed()) {
168
+ lines.push('Advisor 提醒:本回合已有实质成果(改动、验证或调查结论),自上次咨询后又有新进展。给出最终结论或交付之前,先咨询一次 Advisor。');
169
+ }
170
+ return lines.join('\n');
171
+ }
172
+ #answerNeedsReview(answer) {
173
+ if (!this.needsDeliveryReview())
174
+ return false;
175
+ // 没做过实事、只是一段回答时,澄清提问和一两句话不算成果(草稿只能豁免自己)。
176
+ // 补问之后的那句简短回应不会自己够格;要不要警告看的是被补问的那版实质回答。
177
+ return this.#followedUp || this.#hasWork() || needsTextDeliveryReview(this.#request, answer, this.#previousRequest);
178
+ }
179
+ /**
180
+ * 这版回答发布后是否该补问一次顾问(`advisor.enforcement: 'followup'`,给不会主动咨询的弱模型)。
181
+ * 判据与 warning 相同,整个运行内只给一次;回答本身已显示,不撤回。
182
+ */
183
+ followUpDue(answer) {
184
+ return !this.#followedUp && this.#answerNeedsReview(answer);
185
+ }
186
+ markFollowedUp() {
187
+ this.#followedUp = true;
188
+ }
189
+ /**
190
+ * 最终回答发布时调用:实质成果没经顾问复核,则返回一条(本次运行内只给一次)
191
+ * 非阻塞的 warning;已复核、顾问不可用、或不是实质成果则不打扰。
192
+ */
193
+ unreviewedWarning(answer) {
194
+ if (this.#warned || !this.#answerNeedsReview(answer))
195
+ return undefined;
196
+ this.#warned = true;
197
+ return '本回答包含实质结论或成果,但没有经过顾问复核;不能把它视为已复核。';
198
+ }
199
+ }