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.
- package/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- 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
|
+
}
|
package/src/memory/types.ts
CHANGED
|
@@ -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 {
|
package/src/plugins/pluginApi.ts
CHANGED
|
@@ -21,9 +21,21 @@
|
|
|
21
21
|
* ============================================================
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import type {
|
|
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 {
|
|
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?:
|
|
42
|
-
promptCache?:
|
|
53
|
+
transientBundle?: ContextBundle;
|
|
54
|
+
promptCache?: PromptCacheContext;
|
|
43
55
|
/** Canonical host-issued trust context; plugins may narrow it but never elevate it. */
|
|
44
|
-
runtimeContext?:
|
|
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
|
-
):
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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 = {
|
|
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
|
/**
|