minimal-agent 0.11.0 → 0.12.0

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 (128) hide show
  1. package/package.json +4 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/index.js +5 -3
  7. package/plugins/workflow-runner/src/index.ts +31 -13
  8. package/plugins/workflow-runner/src/multimodal.js +2 -1
  9. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  10. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  11. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
  14. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  15. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  16. package/plugins/workflow-runner/src/types.ts +26 -7
  17. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  18. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  19. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  20. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  21. package/src/cli/print.js +19 -4
  22. package/src/cli/print.ts +17 -4
  23. package/src/cli/streamJson.js +19 -4
  24. package/src/cli/streamJson.ts +17 -4
  25. package/src/config/configDefaults.js +25 -0
  26. package/src/config/configDefaults.ts +33 -0
  27. package/src/config/configFile.js +2 -21
  28. package/src/config/configFile.ts +11 -24
  29. package/src/config/configManagement.js +1 -1
  30. package/src/config/configManagement.ts +2 -2
  31. package/src/context/compact.js +4 -0
  32. package/src/context/compact.ts +4 -0
  33. package/src/context/contextBundle.js +38 -13
  34. package/src/context/contextBundle.ts +47 -13
  35. package/src/harness/stallGuard.js +19 -4
  36. package/src/harness/stallGuard.ts +30 -7
  37. package/src/loop.js +95 -11
  38. package/src/loop.ts +125 -16
  39. package/src/main.js +8 -1
  40. package/src/main.tsx +7 -1
  41. package/src/memory/constants.js +10 -0
  42. package/src/memory/constants.ts +13 -0
  43. package/src/memory/episodicMemory.js +274 -2
  44. package/src/memory/episodicMemory.ts +306 -1
  45. package/src/memory/fileStore.js +24 -4
  46. package/src/memory/fileStore.ts +22 -2
  47. package/src/memory/memoryService.js +79 -12
  48. package/src/memory/memoryService.ts +108 -9
  49. package/src/memory/repository.js +415 -85
  50. package/src/memory/repository.ts +437 -86
  51. package/src/memory/turnJournal.js +95 -0
  52. package/src/memory/turnJournal.ts +120 -0
  53. package/src/memory/types.ts +16 -0
  54. package/src/plugins/pluginApi.ts +30 -6
  55. package/src/plugins/pluginRunner.js +6 -2
  56. package/src/plugins/pluginRunner.ts +12 -7
  57. package/src/prompts/policyRegistry.js +27 -0
  58. package/src/prompts/policyRegistry.ts +27 -0
  59. package/src/prompts/promptAssembly.js +39 -7
  60. package/src/prompts/promptAssembly.ts +145 -59
  61. package/src/session/runAgentTurn.js +43 -21
  62. package/src/session/runAgentTurn.ts +68 -26
  63. package/src/session/runTurn.js +11 -46
  64. package/src/session/runTurn.ts +12 -46
  65. package/src/session/runtimeStore.js +149 -0
  66. package/src/session/runtimeStore.ts +197 -0
  67. package/src/session/sessionState.js +5 -0
  68. package/src/session/sessionState.ts +14 -0
  69. package/src/tools/agent/agent.js +143 -14
  70. package/src/tools/agent/agent.ts +149 -14
  71. package/src/tools/agent/agentTaskService.js +60 -0
  72. package/src/tools/agent/agentTaskService.ts +113 -0
  73. package/src/tools/agent/agentTypes.js +5 -0
  74. package/src/tools/agent/agentTypes.ts +5 -0
  75. package/src/tools/bash/bash.js +31 -11
  76. package/src/tools/bash/bash.ts +29 -10
  77. package/src/tools/capabilities.js +5 -0
  78. package/src/tools/capabilities.ts +11 -1
  79. package/src/tools/edit/edit.js +7 -4
  80. package/src/tools/edit/edit.ts +7 -4
  81. package/src/tools/edit/multi-edit.js +4 -2
  82. package/src/tools/edit/multi-edit.ts +4 -2
  83. package/src/tools/glob/glob.js +156 -40
  84. package/src/tools/glob/glob.ts +185 -46
  85. package/src/tools/index.js +35 -17
  86. package/src/tools/index.ts +81 -20
  87. package/src/tools/lsp/lsp.js +165 -22
  88. package/src/tools/lsp/lsp.ts +206 -36
  89. package/src/tools/powershell/powershell.js +116 -26
  90. package/src/tools/powershell/powershell.ts +155 -30
  91. package/src/tools/presentresult/presentresult.js +10 -0
  92. package/src/tools/presentresult/presentresult.ts +10 -0
  93. package/src/tools/read/read.js +244 -29
  94. package/src/tools/read/read.ts +391 -59
  95. package/src/tools/shared/diffSummary.js +44 -0
  96. package/src/tools/shared/diffSummary.ts +63 -0
  97. package/src/tools/shared/fileActivity.js +2 -1
  98. package/src/tools/shared/fileActivity.ts +3 -0
  99. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  100. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  101. package/src/tools/shared/toolOutputStore.js +97 -0
  102. package/src/tools/shared/toolOutputStore.ts +110 -0
  103. package/src/tools/skill/skill.js +86 -0
  104. package/src/tools/skill/skill.ts +94 -0
  105. package/src/tools/snip/snip.js +67 -0
  106. package/src/tools/snip/snip.ts +80 -0
  107. package/src/tools/tasks/backgroundTaskService.js +79 -28
  108. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  109. package/src/tools/tasks/taskManagement.js +163 -0
  110. package/src/tools/tasks/taskManagement.ts +180 -0
  111. package/src/tools/tasks/taskOutput.js +64 -14
  112. package/src/tools/tasks/taskOutput.ts +102 -23
  113. package/src/tools/tasks/taskStop.js +48 -8
  114. package/src/tools/tasks/taskStop.ts +69 -9
  115. package/src/tools/tasks/taskStore.js +46 -0
  116. package/src/tools/tasks/taskStore.ts +77 -0
  117. package/src/tools/terminal/terminalCapture.js +167 -0
  118. package/src/tools/terminal/terminalCapture.ts +179 -0
  119. package/src/tools/trash/trash.js +233 -0
  120. package/src/tools/trash/trash.ts +283 -0
  121. package/src/tools/worktree/sessionWorktree.js +128 -0
  122. package/src/tools/worktree/sessionWorktree.ts +158 -0
  123. package/src/tools/write/write.js +6 -2
  124. package/src/tools/write/write.ts +6 -2
  125. package/src/types.ts +12 -0
  126. package/src/ui/hooks/useChat.js +31 -11
  127. package/src/ui/hooks/useChat.ts +31 -13
  128. package/src/web/protocol.ts +78 -3
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ============================================================
3
+ * src/memory/turnJournal.ts —— 「一个用户回合」的会话原文记账
4
+ * ------------------------------------------------------------
5
+ * 把 user 输入与本回合最终 assistant 回答写进 `episodes/*.jsonl`,
6
+ * 给 `SearchConversations` 提供可检索的历史原文。
7
+ *
8
+ * ★ 为什么是独立模块、而不是塞进 loop.ts:
9
+ * `runQuery` 不等于「一个用户回合」——子 Agent(tools/agent/agent.ts 直接调
10
+ * runQuery,且**携带同一份 workspaceId/sessionId**)和 workflow 的每个 llm/agent 步
11
+ * 都会各调一次 runQuery。记在那里会把子任务提示词当成「用户说的话」灌进历史检索,
12
+ * 污染的正是这个功能要保护的东西。
13
+ *
14
+ * 也不能只留在 `session/runTurn.ts`:TUI 走的是 `ui/hooks/useChat.ts` → runWithPlugins,
15
+ * **根本不经过 runTurn**。此前的后果是 TUI 对话永远不入库,而一次性的
16
+ * `.backfill-<ws>.json` 标记又让 SearchConversations 在首次搜索后永久停更 ——
17
+ * 工具宣称"搜索本机历史会话原文",实际对 TUI 用户静默失效。
18
+ *
19
+ * 所以:实现只有这一份,调用点恰好落在两个真正的「用户回合」边界上
20
+ * (runTurn 与 useChat.submit),子 Agent 与 workflow 步一概不记。
21
+ * ============================================================
22
+ */
23
+ import { applyEpisodeRetention, episodicMemoryService, finishRetentionSweep, shouldSweepRetention, } from './episodicMemory.js';
24
+ import { memoryService } from './memoryService.js';
25
+ function messageText(message) {
26
+ if (typeof message.content === 'string')
27
+ return message.content;
28
+ if (!Array.isArray(message.content))
29
+ return '';
30
+ return message.content
31
+ .filter((part) => part.type === 'text')
32
+ .map((part) => part.text)
33
+ .join('\n');
34
+ }
35
+ function lastAssistantMessage(messages, start) {
36
+ for (let index = messages.length - 1; index >= start; index -= 1) {
37
+ if (messages[index]?.role === 'assistant')
38
+ return messages[index];
39
+ }
40
+ return undefined;
41
+ }
42
+ /**
43
+ * 会话原文的保留清理:节流后台执行,绝不阻塞本回合。
44
+ * 与 `clipboardImage::pruneOldTempImages` 同款 fire-and-forget,不需要常驻定时器。
45
+ */
46
+ function sweepRetentionInBackground(workspaceId) {
47
+ if (!shouldSweepRetention(workspaceId))
48
+ return;
49
+ void (async () => {
50
+ try {
51
+ const settings = await memoryService.settings();
52
+ if (!settings.retention)
53
+ return;
54
+ await applyEpisodeRetention(workspaceId, settings.retention);
55
+ }
56
+ finally {
57
+ finishRetentionSweep(workspaceId);
58
+ }
59
+ })().catch(() => undefined);
60
+ }
61
+ /**
62
+ * 开一次回合记账。缺权限 / 缺 workspaceId / 缺 sessionId 都返回 null(不记,也不报错)——
63
+ * 记账是附加能力,绝不能反过来阻断对话。
64
+ */
65
+ export async function startTurnJournal(input) {
66
+ const context = input.memoryContext;
67
+ // 判据是 `!== 'denied'`:只要宿主没有明确拒绝记忆访问就记账,`read-only` 也照记。
68
+ // 这是**有意放松**的(2026-08-13 明确决定:优先保证检索效果与性能)——收紧成"只有可写
69
+ // 权限才记"会让 `-p`(默认 read-only)和部分 Desktop 场景的历史检索大面积失效。
70
+ // 真正的隐私边界由 `denied` 与保留窗口(MemorySettings.retention)负责,不靠这里收窄。
71
+ if (!context || context.access === 'denied' || !context.workspaceId || !context.sessionId) {
72
+ return null;
73
+ }
74
+ const target = { workspaceId: context.workspaceId, sessionId: context.sessionId };
75
+ await episodicMemoryService.append({
76
+ ...target,
77
+ role: 'user',
78
+ content: input.userInput,
79
+ ...(input.userMessageId ? { messageId: input.userMessageId } : {}),
80
+ }).catch(() => undefined);
81
+ sweepRetentionInBackground(target.workspaceId);
82
+ return {
83
+ async finish(history, startIndex) {
84
+ const assistant = lastAssistantMessage(history, startIndex);
85
+ const content = assistant ? messageText(assistant) : '';
86
+ if (!content)
87
+ return;
88
+ await episodicMemoryService.append({
89
+ ...target,
90
+ role: 'assistant',
91
+ content,
92
+ }).catch(() => undefined);
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ============================================================
3
+ * src/memory/turnJournal.ts —— 「一个用户回合」的会话原文记账
4
+ * ------------------------------------------------------------
5
+ * 把 user 输入与本回合最终 assistant 回答写进 `episodes/*.jsonl`,
6
+ * 给 `SearchConversations` 提供可检索的历史原文。
7
+ *
8
+ * ★ 为什么是独立模块、而不是塞进 loop.ts:
9
+ * `runQuery` 不等于「一个用户回合」——子 Agent(tools/agent/agent.ts 直接调
10
+ * runQuery,且**携带同一份 workspaceId/sessionId**)和 workflow 的每个 llm/agent 步
11
+ * 都会各调一次 runQuery。记在那里会把子任务提示词当成「用户说的话」灌进历史检索,
12
+ * 污染的正是这个功能要保护的东西。
13
+ *
14
+ * 也不能只留在 `session/runTurn.ts`:TUI 走的是 `ui/hooks/useChat.ts` → runWithPlugins,
15
+ * **根本不经过 runTurn**。此前的后果是 TUI 对话永远不入库,而一次性的
16
+ * `.backfill-<ws>.json` 标记又让 SearchConversations 在首次搜索后永久停更 ——
17
+ * 工具宣称"搜索本机历史会话原文",实际对 TUI 用户静默失效。
18
+ *
19
+ * 所以:实现只有这一份,调用点恰好落在两个真正的「用户回合」边界上
20
+ * (runTurn 与 useChat.submit),子 Agent 与 workflow 步一概不记。
21
+ * ============================================================
22
+ */
23
+
24
+ import {
25
+ applyEpisodeRetention,
26
+ episodicMemoryService,
27
+ finishRetentionSweep,
28
+ shouldSweepRetention,
29
+ } from './episodicMemory.js';
30
+ import { memoryService } from './memoryService.js';
31
+ import type { Message } from '../types.js';
32
+
33
+ export interface TurnJournalInput {
34
+ memoryContext?: {
35
+ access?: string;
36
+ workspaceId?: string;
37
+ sessionId?: string;
38
+ };
39
+ userInput: string;
40
+ userMessageId?: string;
41
+ }
42
+
43
+ export interface TurnJournal {
44
+ /** 回合收尾时调用:从 history 里取本回合最后一条 assistant 消息入库。 */
45
+ finish(history: readonly Message[], startIndex: number): Promise<void>;
46
+ }
47
+
48
+ function messageText(message: Message): string {
49
+ if (typeof message.content === 'string') return message.content;
50
+ if (!Array.isArray(message.content)) return '';
51
+ return message.content
52
+ .filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')
53
+ .map((part) => part.text)
54
+ .join('\n');
55
+ }
56
+
57
+ function lastAssistantMessage(
58
+ messages: readonly Message[],
59
+ start: number,
60
+ ): Message | undefined {
61
+ for (let index = messages.length - 1; index >= start; index -= 1) {
62
+ if (messages[index]?.role === 'assistant') return messages[index];
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * 会话原文的保留清理:节流后台执行,绝不阻塞本回合。
69
+ * 与 `clipboardImage::pruneOldTempImages` 同款 fire-and-forget,不需要常驻定时器。
70
+ */
71
+ function sweepRetentionInBackground(workspaceId: string): void {
72
+ if (!shouldSweepRetention(workspaceId)) return;
73
+ void (async () => {
74
+ try {
75
+ const settings = await memoryService.settings();
76
+ if (!settings.retention) return;
77
+ await applyEpisodeRetention(workspaceId, settings.retention);
78
+ } finally {
79
+ finishRetentionSweep(workspaceId);
80
+ }
81
+ })().catch(() => undefined);
82
+ }
83
+
84
+ /**
85
+ * 开一次回合记账。缺权限 / 缺 workspaceId / 缺 sessionId 都返回 null(不记,也不报错)——
86
+ * 记账是附加能力,绝不能反过来阻断对话。
87
+ */
88
+ export async function startTurnJournal(
89
+ input: TurnJournalInput,
90
+ ): Promise<TurnJournal | null> {
91
+ const context = input.memoryContext;
92
+ // 判据是 `!== 'denied'`:只要宿主没有明确拒绝记忆访问就记账,`read-only` 也照记。
93
+ // 这是**有意放松**的(2026-08-13 明确决定:优先保证检索效果与性能)——收紧成"只有可写
94
+ // 权限才记"会让 `-p`(默认 read-only)和部分 Desktop 场景的历史检索大面积失效。
95
+ // 真正的隐私边界由 `denied` 与保留窗口(MemorySettings.retention)负责,不靠这里收窄。
96
+ if (!context || context.access === 'denied' || !context.workspaceId || !context.sessionId) {
97
+ return null;
98
+ }
99
+ const target = { workspaceId: context.workspaceId, sessionId: context.sessionId };
100
+ await episodicMemoryService.append({
101
+ ...target,
102
+ role: 'user',
103
+ content: input.userInput,
104
+ ...(input.userMessageId ? { messageId: input.userMessageId } : {}),
105
+ }).catch(() => undefined);
106
+ sweepRetentionInBackground(target.workspaceId);
107
+
108
+ return {
109
+ async finish(history, startIndex) {
110
+ const assistant = lastAssistantMessage(history, startIndex);
111
+ const content = assistant ? messageText(assistant) : '';
112
+ if (!content) return;
113
+ await episodicMemoryService.append({
114
+ ...target,
115
+ role: 'assistant',
116
+ content,
117
+ }).catch(() => undefined);
118
+ },
119
+ };
120
+ }
@@ -71,10 +71,26 @@ export interface MemoryReviewWindows {
71
71
  decision: number;
72
72
  }
73
73
 
74
+ /**
75
+ * 会话原文日志(`episodes/*.jsonl`)的保留窗口。
76
+ *
77
+ * 记忆条目(records)是用户明确授权保存的稳定事实,只有用户能删;会话原文日志是为了
78
+ * `SearchConversations` 而留的**全量对话副本**,性质完全不同——它必须有保留上限,否则
79
+ * 明文对话会无限累积(仓库里其它 append-only 存储都有清理,只有它没有)。
80
+ */
81
+ export interface MemoryRetention {
82
+ /** 超过这个天数的会话原文自动清理。 */
83
+ episodeDays: number;
84
+ /** 单个工作区的会话原文总体积上限(MB),先到者先触发。 */
85
+ episodeMaxMegabytes: number;
86
+ }
87
+
74
88
  export interface MemorySettings {
75
89
  autoRemember: boolean;
76
90
  /** Optional on the public wire type for pre-v2 desktop clients. */
77
91
  reviewAfterDays?: MemoryReviewWindows;
92
+ /** Optional on the wire so pre-retention clients keep deserializing. */
93
+ retention?: MemoryRetention;
78
94
  }
79
95
 
80
96
  export interface MemoryDiagnostic {
@@ -21,9 +21,21 @@
21
21
  * ============================================================
22
22
  */
23
23
 
24
- import type { LoopEvent, Message, Provider, ToolCallContext } from '../types.js';
24
+ import type { ContextBundle } from '../context/contextBundle.js';
25
+ import type { AgentRuntimeContext } from '../runtime/agentRuntimeContext.js';
26
+ import type {
27
+ RunAgentTurnOptions,
28
+ RunAgentTurnResult,
29
+ } from '../session/runAgentTurn.js';
25
30
  import type { SessionState } from '../session/sessionState.js';
26
- import type { RunAgentTurnOptions, RunAgentTurnResult } from '../session/runAgentTurn.js';
31
+ import type {
32
+ LoopEvent,
33
+ Message,
34
+ PromptCacheContext,
35
+ Provider,
36
+ ToolCallContext,
37
+ ToolResult,
38
+ } from '../types.js';
27
39
 
28
40
  /** 框架传给插件的运行时上下文(仅暴露执行 LLM/工具 所需的最小集合)。 */
29
41
  export interface PluginContext {
@@ -38,12 +50,23 @@ export interface PluginContext {
38
50
  maxTurns?: number;
39
51
  /** 当前回合的瞬时外部上下文;插件不得把它持久化到 history。 */
40
52
  transientContext?: string;
41
- transientBundle?: import('../context/contextBundle.js').ContextBundle;
42
- promptCache?: import('../types.js').PromptCacheContext;
53
+ transientBundle?: ContextBundle;
54
+ promptCache?: PromptCacheContext;
43
55
  /** Canonical host-issued trust context; plugins may narrow it but never elevate it. */
44
- runtimeContext?: import('../runtime/agentRuntimeContext.js').AgentRuntimeContext;
56
+ runtimeContext?: AgentRuntimeContext;
45
57
  /** Host tool names are an upper bound. Plugins may narrow, never expand. */
46
58
  allowedTools?: readonly string[];
59
+ /**
60
+ * Host tool dispatcher. Rich plugins must use the same injected executor as
61
+ * the parent run so tests, embedders and host-specific dispatch remain on one
62
+ * execution path.
63
+ */
64
+ executeTool?: (
65
+ name: string,
66
+ rawArgsJson: string,
67
+ signal?: AbortSignal,
68
+ context?: ToolCallContext,
69
+ ) => Promise<ToolResult>;
47
70
  /**
48
71
  * 宿主的**授权与可见性上下文**,原样转交给工具层(`executeTool` 第四参)。
49
72
  *
@@ -100,7 +123,8 @@ export interface PluginEvent {
100
123
  export interface PluginApi {
101
124
  activate?(
102
125
  ctx: PluginRuntimeContext,
103
- ): Promise<void | (() => void)> | void | (() => void);
126
+ ): // biome-ignore lint/suspicious/noConfusingVoidType: lifecycle hooks intentionally accept synchronous void or an async disposer.
127
+ Promise<void | (() => void)> | void | (() => void);
104
128
  runCommand?(
105
129
  commandName: string,
106
130
  args: string,
@@ -13,9 +13,9 @@
13
13
  * 未知的 /xxx 当普通用户输入处理,不报错)
14
14
  * ============================================================
15
15
  */
16
- import { discoverPlugins, resolveCommand, buildCommandInput, } from './commandRouter.js';
17
- import { loadPluginApi } from './pluginLoader.js';
18
16
  import { runQuery } from '../loop.js';
17
+ import { buildCommandInput, discoverPlugins, resolveCommand, } from './commandRouter.js';
18
+ import { loadPluginApi } from './pluginLoader.js';
19
19
  export async function* runWithPlugins(userInput, options) {
20
20
  const { provider, history, signal } = options;
21
21
  const isSlashCommand = userInput.trimStart().startsWith('/');
@@ -30,6 +30,7 @@ export async function* runWithPlugins(userInput, options) {
30
30
  sessionState: options.sessionState,
31
31
  chat: options.chat,
32
32
  executeTool: options.executeTool,
33
+ commitFormalDelivery: options.commitFormalDelivery,
33
34
  toolGate: options.toolGate,
34
35
  transientContext: options.transientContext,
35
36
  transientBundle: options.transientBundle,
@@ -69,6 +70,7 @@ export async function* runWithPlugins(userInput, options) {
69
70
  sessionState: options.sessionState,
70
71
  chat: options.chat,
71
72
  executeTool: options.executeTool,
73
+ commitFormalDelivery: options.commitFormalDelivery,
72
74
  toolGate: options.toolGate,
73
75
  transientContext: options.transientContext,
74
76
  transientBundle: options.transientBundle,
@@ -104,6 +106,7 @@ export async function* runWithPlugins(userInput, options) {
104
106
  promptCache: options.promptCache,
105
107
  runtimeContext: options.runtimeContext,
106
108
  allowedTools: options.allowedTools,
109
+ executeTool: options.executeTool,
107
110
  // ⚠️ 宿主授权上下文**必须**跟着走。这一处曾经漏掉,而下面两条 runQuery 路径
108
111
  // 都带着它 —— 于是富插件成了三条路里唯一拿不到 browserRun/computerRun 的那条。
109
112
  // 后果不是"少个字段":workflow 里调 ComputerUse 恒返回「当前宿主未启用
@@ -129,6 +132,7 @@ export async function* runWithPlugins(userInput, options) {
129
132
  sessionState: options.sessionState,
130
133
  chat: options.chat,
131
134
  executeTool: options.executeTool,
135
+ commitFormalDelivery: options.commitFormalDelivery,
132
136
  toolGate: options.toolGate,
133
137
  transientContext: options.transientContext,
134
138
  transientBundle: options.transientBundle,
@@ -14,15 +14,15 @@
14
14
  * ============================================================
15
15
  */
16
16
 
17
+ import { type RunQueryOptions, runQuery } from '../loop.js';
18
+ import type { LoopEvent, Message, Provider } from '../types.js';
17
19
  import {
20
+ buildCommandInput,
18
21
  discoverPlugins,
19
22
  resolveCommand,
20
- buildCommandInput,
21
23
  } from './commandRouter.js';
22
- import { loadPluginApi } from './pluginLoader.js';
23
- import { runQuery, type RunQueryOptions } from '../loop.js';
24
- import type { LoopEvent, Message, Provider } from '../types.js';
25
24
  import type { PluginEvent } from './pluginApi.js';
25
+ import { loadPluginApi } from './pluginLoader.js';
26
26
 
27
27
  export interface PluginRunnerOptions {
28
28
  provider: Provider;
@@ -38,6 +38,7 @@ export interface PluginRunnerOptions {
38
38
  */
39
39
  chat?: RunQueryOptions['chat'];
40
40
  executeTool?: RunQueryOptions['executeTool'];
41
+ commitFormalDelivery?: RunQueryOptions['commitFormalDelivery'];
41
42
  /** 工具执行闸门(生产用):透传给 runQuery。见 src/toolGate.ts。 */
42
43
  toolGate?: RunQueryOptions['toolGate'];
43
44
  transientContext?: RunQueryOptions['transientContext'];
@@ -77,6 +78,7 @@ export async function* runWithPlugins(
77
78
  sessionState: options.sessionState,
78
79
  chat: options.chat,
79
80
  executeTool: options.executeTool,
81
+ commitFormalDelivery: options.commitFormalDelivery,
80
82
  toolGate: options.toolGate,
81
83
  transientContext: options.transientContext,
82
84
  transientBundle: options.transientBundle,
@@ -118,6 +120,7 @@ export async function* runWithPlugins(
118
120
  sessionState: options.sessionState,
119
121
  chat: options.chat,
120
122
  executeTool: options.executeTool,
123
+ commitFormalDelivery: options.commitFormalDelivery,
121
124
  toolGate: options.toolGate,
122
125
  transientContext: options.transientContext,
123
126
  transientBundle: options.transientBundle,
@@ -154,6 +157,7 @@ export async function* runWithPlugins(
154
157
  promptCache: options.promptCache,
155
158
  runtimeContext: options.runtimeContext,
156
159
  allowedTools: options.allowedTools,
160
+ executeTool: options.executeTool,
157
161
  // ⚠️ 宿主授权上下文**必须**跟着走。这一处曾经漏掉,而下面两条 runQuery 路径
158
162
  // 都带着它 —— 于是富插件成了三条路里唯一拿不到 browserRun/computerRun 的那条。
159
163
  // 后果不是"少个字段":workflow 里调 ComputerUse 恒返回「当前宿主未启用
@@ -180,10 +184,11 @@ export async function* runWithPlugins(
180
184
  sessionState: options.sessionState,
181
185
  chat: options.chat,
182
186
  executeTool: options.executeTool,
187
+ commitFormalDelivery: options.commitFormalDelivery,
183
188
  toolGate: options.toolGate,
184
- transientContext: options.transientContext,
185
- transientBundle: options.transientBundle,
186
- promptCache: options.promptCache,
189
+ transientContext: options.transientContext,
190
+ transientBundle: options.transientBundle,
191
+ promptCache: options.promptCache,
187
192
  userAttachments: options.userAttachments,
188
193
  userMessageId: options.userMessageId,
189
194
  userBrowserReferences: options.userBrowserReferences,
@@ -84,6 +84,8 @@ const ROUTABLE_TOOLS = [
84
84
  'ComputerUse', 'WebBrowser', 'WebFetch', 'WebSearch',
85
85
  'Download', 'Read', 'Glob', 'Grep', 'Edit', 'MultiEdit', 'Write', 'Bash',
86
86
  'NotebookEdit', 'LSP', 'PowerShell', 'TaskOutput', 'TaskStop',
87
+ 'Trash', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList', 'Skill',
88
+ 'EnterWorktree', 'ExitWorktree', 'TerminalCapture', 'Snip',
87
89
  ];
88
90
  const PARAMETERIZED_TOOLS = [
89
91
  ...ROUTABLE_TOOLS, ...MEMORY_TOOLS, 'AskUserQuestion', 'Advisor', 'Agent',
@@ -173,6 +175,31 @@ const CAPABILITY_POLICIES = [
173
175
  requires: ['background.process'],
174
176
  render: () => '- 后台任务按当前会话隔离。启动后保存 task_id;TaskOutput 可阻塞等待新输出,避免高频轮询;任务不再需要或需要中止时使用 TaskStop,不遗留服务进程。',
175
177
  },
178
+ {
179
+ id: 'capability.trash', layer: 'capability', profiles: ['main', 'subagent-general'], source: '06',
180
+ requiresTools: ['Trash'],
181
+ render: () => '- 删除项目文件或目录使用 Trash 移入系统回收站;清理 Git 未跟踪文件使用 Trash(action="git-clean")。Bash/PowerShell 不执行绕过回收站的永久删除命令。',
182
+ },
183
+ {
184
+ id: 'capability.task-management', layer: 'capability', profiles: ALL_PROFILES, source: '06',
185
+ requires: ['task.management'],
186
+ render: () => '- 多阶段、跨回合或存在依赖关系的工作用 TaskCreate/TaskUpdate 保持任务图;开始前检查 blockedBy,完成验证后才标记 completed。单步工作不必创建任务。',
187
+ },
188
+ {
189
+ id: 'capability.skill-invoke', layer: 'capability', profiles: ALL_PROFILES, source: '07',
190
+ requiresTools: ['Skill'],
191
+ render: () => '- 需求匹配已安装 Skill 或用户明确点名 Skill 时,用 Skill 按精确名称加载完整 SKILL.md;其中引用的资源再按需 Read,不预加载整个目录。',
192
+ },
193
+ {
194
+ id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
195
+ requires: ['worktree.session'],
196
+ render: () => '- 主任务需要隔离试验时可 EnterWorktree;完成后用 ExitWorktree apply/keep/discard 收口。apply 必须通过补丁检查且不自动提交;discard 不永久丢弃已有成果。',
197
+ },
198
+ {
199
+ id: 'capability.terminal-capture', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
200
+ requiresTools: ['TerminalCapture'],
201
+ render: () => '- 只有确需查看当前 TUI 屏幕状态时使用 TerminalCapture;项目命令输出仍通过 Bash/TaskOutput 获取。',
202
+ },
176
203
  {
177
204
  id: 'capability.web-research', layer: 'capability', profiles: ALL_PROFILES, source: '04',
178
205
  requiresAnyTools: ['WebSearch', 'WebFetch'],
@@ -133,6 +133,8 @@ const ROUTABLE_TOOLS = [
133
133
  'ComputerUse', 'WebBrowser', 'WebFetch', 'WebSearch',
134
134
  'Download', 'Read', 'Glob', 'Grep', 'Edit', 'MultiEdit', 'Write', 'Bash',
135
135
  'NotebookEdit', 'LSP', 'PowerShell', 'TaskOutput', 'TaskStop',
136
+ 'Trash', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList', 'Skill',
137
+ 'EnterWorktree', 'ExitWorktree', 'TerminalCapture', 'Snip',
136
138
  ];
137
139
 
138
140
  const PARAMETERIZED_TOOLS = [
@@ -215,6 +217,31 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
215
217
  requires: ['background.process'],
216
218
  render: () => '- 后台任务按当前会话隔离。启动后保存 task_id;TaskOutput 可阻塞等待新输出,避免高频轮询;任务不再需要或需要中止时使用 TaskStop,不遗留服务进程。',
217
219
  },
220
+ {
221
+ id: 'capability.trash', layer: 'capability', profiles: ['main','subagent-general'], source: '06',
222
+ requiresTools: ['Trash'],
223
+ render: () => '- 删除项目文件或目录使用 Trash 移入系统回收站;清理 Git 未跟踪文件使用 Trash(action="git-clean")。Bash/PowerShell 不执行绕过回收站的永久删除命令。',
224
+ },
225
+ {
226
+ id: 'capability.task-management', layer: 'capability', profiles: ALL_PROFILES, source: '06',
227
+ requires: ['task.management'],
228
+ render: () => '- 多阶段、跨回合或存在依赖关系的工作用 TaskCreate/TaskUpdate 保持任务图;开始前检查 blockedBy,完成验证后才标记 completed。单步工作不必创建任务。',
229
+ },
230
+ {
231
+ id: 'capability.skill-invoke', layer: 'capability', profiles: ALL_PROFILES, source: '07',
232
+ requiresTools: ['Skill'],
233
+ render: () => '- 需求匹配已安装 Skill 或用户明确点名 Skill 时,用 Skill 按精确名称加载完整 SKILL.md;其中引用的资源再按需 Read,不预加载整个目录。',
234
+ },
235
+ {
236
+ id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
237
+ requires: ['worktree.session'],
238
+ render: () => '- 主任务需要隔离试验时可 EnterWorktree;完成后用 ExitWorktree apply/keep/discard 收口。apply 必须通过补丁检查且不自动提交;discard 不永久丢弃已有成果。',
239
+ },
240
+ {
241
+ id: 'capability.terminal-capture', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
242
+ requiresTools: ['TerminalCapture'],
243
+ render: () => '- 只有确需查看当前 TUI 屏幕状态时使用 TerminalCapture;项目命令输出仍通过 Bash/TaskOutput 获取。',
244
+ },
218
245
  {
219
246
  id: 'capability.web-research', layer: 'capability', profiles: ALL_PROFILES, source: '04',
220
247
  requiresAnyTools: ['WebSearch', 'WebFetch'],
@@ -27,6 +27,7 @@ import { getSkillList, formatSkillHint } from './skillList.js';
27
27
  import { getPluginMetaList, formatPluginHint } from './pluginList.js';
28
28
  import { memoryService } from '../memory/memoryService.js';
29
29
  import { countTextTokens } from '../context/tokenCounter.js';
30
+ import { effectiveTaskStatus, listManagedTasks, } from '../tools/tasks/taskStore.js';
30
31
  import { assembleContextBundle, contextFragment, DEFAULT_TRANSIENT_BUDGET_TOKENS, } from '../context/contextBundle.js';
31
32
  import { assertPromptPolicyRegistry, capabilitiesForTools, enabledPolicyIds, renderCapabilityPolicyLayer, renderStablePolicyLayer, } from './policyRegistry.js';
32
33
  const stablePromptCache = new Map();
@@ -90,9 +91,9 @@ ${rules}`;
90
91
  }
91
92
  async function buildMemoryTransient(runtime, tools) {
92
93
  const names = new Set(tools.map((tool) => tool.name));
93
- const memoryAllowed = runtime.memoryAccess !== undefined && runtime.memoryAccess !== 'denied'
94
+ const memoryAllowed = (runtime.memoryAccess !== undefined && runtime.memoryAccess !== 'denied') ||
94
95
  // Compatibility is explicit-only: omitted ownerLocalSession never grants access.
95
- || (runtime.memoryAccess === undefined && runtime.ownerLocalSession === true);
96
+ (runtime.memoryAccess === undefined && runtime.ownerLocalSession === true);
96
97
  if (!memoryAllowed || !names.has('RecallMemory'))
97
98
  return null;
98
99
  try {
@@ -204,8 +205,12 @@ export async function buildTransientBundle(runtime, profile, tools, options = {}
204
205
  runtime.supervisedBrowser
205
206
  ? '宿主提供可监督浏览器:调用 WebBrowser 时用户可看到过程并在验证或登录环节接管。'
206
207
  : '',
207
- runtime.interactiveAsk ? '' : '当前宿主不能交互提问;不要等待用户选择,缺少关键输入时说明阻塞。',
208
- ].filter(Boolean).join('\n');
208
+ runtime.interactiveAsk
209
+ ? ''
210
+ : '当前宿主不能交互提问;不要等待用户选择,缺少关键输入时说明阻塞。',
211
+ ]
212
+ .filter(Boolean)
213
+ .join('\n');
209
214
  const fragments = [
210
215
  contextFragment({
211
216
  id: 'runtime.host-state',
@@ -216,7 +221,7 @@ export async function buildTransientBundle(runtime, profile, tools, options = {}
216
221
  content: runtimeLines,
217
222
  }),
218
223
  ];
219
- const [memory, advisor, skillHint, pluginHint, projectInstructions] = await Promise.all([
224
+ const [memory, advisor, skillHint, pluginHint, projectInstructions, activeTasks,] = await Promise.all([
220
225
  buildMemoryTransient(runtime, tools),
221
226
  buildAdvisorTransient(profile, tools),
222
227
  getSkillList(runtime.cwd, new Set(tools.map((tool) => tool.name)))
@@ -224,13 +229,28 @@ export async function buildTransientBundle(runtime, profile, tools, options = {}
224
229
  .catch(() => ''),
225
230
  options.includePlugins === false
226
231
  ? Promise.resolve('')
227
- : getPluginMetaList(runtime.cwd).then(formatPluginHint).catch(() => ''),
232
+ : getPluginMetaList(runtime.cwd)
233
+ .then(formatPluginHint)
234
+ .catch(() => ''),
228
235
  options.includeProjectInstructions === false
229
236
  ? Promise.resolve('')
230
237
  : loadProjectPromptInstructions(runtime.cwd).catch(() => ''),
238
+ runtime.sessionId &&
239
+ tools.some((tool) => ['TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList'].includes(tool.name))
240
+ ? listManagedTasks({
241
+ workspaceId: runtime.workspaceId,
242
+ sessionId: runtime.sessionId,
243
+ cwd: runtime.cwd,
244
+ }).catch(() => [])
245
+ : Promise.resolve([]),
231
246
  ]);
232
247
  if (memory) {
233
- const priorities = { persona: 82, preferences: 78, relevant: 70, health: 10 };
248
+ const priorities = {
249
+ persona: 82,
250
+ preferences: 78,
251
+ relevant: 70,
252
+ health: 10,
253
+ };
234
254
  for (const section of memory.sections) {
235
255
  fragments.push(contextFragment({
236
256
  id: `memory.${section.id}`,
@@ -276,6 +296,18 @@ export async function buildTransientBundle(runtime, profile, tools, options = {}
276
296
  required: true,
277
297
  content: `# 项目指令\n以下内容来自当前工作目录的 minimal-agent.md。行为约束和仓库约定必须遵守;架构事实若与当前代码冲突,以实际代码为准。不要擅自重写该文件。\n\n${projectInstructions}`,
278
298
  }));
299
+ const unfinished = activeTasks.filter((task) => task.status !== 'completed');
300
+ if (unfinished.length)
301
+ fragments.push(contextFragment({
302
+ id: 'tasks.active',
303
+ kind: 'task',
304
+ authority: 'constraint',
305
+ priority: 85,
306
+ content: `# Active tasks\n${unfinished
307
+ .slice(0, 20)
308
+ .map((task) => `- ${task.id} [${effectiveTaskStatus(task, activeTasks)}] ${task.title}`)
309
+ .join('\n')}`,
310
+ }));
279
311
  return assembleContextBundle(fragments, runtime.transientBudgetTokens ?? DEFAULT_TRANSIENT_BUDGET_TOKENS);
280
312
  }
281
313
  /**