minimal-agent 0.18.7 → 0.18.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/context/persistContext.js +5 -1
- package/src/context/persistContext.ts +5 -2
- package/src/loop.js +75 -25
- package/src/loop.ts +72 -23
- package/src/memory/factLearning.js +3 -3
- package/src/memory/factLearning.ts +3 -7
- package/src/memory/fileStore.js +2 -34
- package/src/memory/fileStore.ts +2 -26
- package/src/memory/jobPolicy.js +21 -13
- package/src/memory/jobPolicy.ts +22 -11
- package/src/memory/learning.js +3 -3
- package/src/memory/learning.ts +3 -3
- package/src/memory/learningModel.js +4 -2
- package/src/memory/learningModel.ts +4 -2
- package/src/prompts/delegation.js +15 -0
- package/src/prompts/delegation.ts +20 -0
- package/src/prompts/policyRegistry.js +7 -10
- package/src/prompts/policyRegistry.ts +9 -10
- package/src/prompts/promptAssembly.js +16 -23
- package/src/prompts/promptAssembly.ts +22 -27
- package/src/prompts/visualPresentation.js +28 -0
- package/src/prompts/visualPresentation.ts +26 -0
- package/src/runtime/agentRuntimeContext.js +3 -0
- package/src/runtime/agentRuntimeContext.ts +4 -0
- package/src/runtime/fileLock.js +75 -0
- package/src/runtime/fileLock.ts +73 -0
- package/src/runtime/finalAnswer.js +4 -2
- package/src/runtime/finalAnswer.ts +6 -2
- package/src/runtime/outputContract.js +17 -0
- package/src/runtime/outputContract.ts +34 -0
- package/src/session/runAgentTurn.js +7 -2
- package/src/session/runAgentTurn.ts +7 -2
- package/src/session/runtimeStore.js +11 -28
- package/src/session/runtimeStore.ts +9 -23
- package/src/tools/agent/agent.js +9 -6
- package/src/tools/agent/agent.ts +9 -5
- package/src/tools/memory/maintenance.js +2 -0
- package/src/tools/memory/maintenance.ts +2 -0
- package/src/tools/memory/memory.js +58 -14
- package/src/tools/memory/memory.ts +57 -13
- package/src/tools/memory/policy.js +16 -1
- package/src/tools/memory/policy.ts +16 -1
- package/src/tools/skill/searchSkills.js +2 -2
- package/src/tools/skill/searchSkills.ts +2 -2
- package/src/tools/skill/skill.js +4 -0
- package/src/tools/skill/skill.ts +3 -0
- package/src/tools/toolPromptGuides.js +4 -3
- package/src/tools/toolPromptGuides.ts +4 -3
- package/src/types.ts +4 -0
- package/src/web/protocol.ts +3 -1
package/src/memory/fileStore.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import lockfile from 'proper-lockfile';
|
|
3
2
|
import {
|
|
4
3
|
chmod,
|
|
5
4
|
mkdir,
|
|
@@ -10,14 +9,12 @@ import {
|
|
|
10
9
|
stat,
|
|
11
10
|
} from 'node:fs/promises';
|
|
12
11
|
import { dirname, join } from 'node:path';
|
|
13
|
-
import {
|
|
12
|
+
import { acquireStateFileLock } from '../runtime/fileLock.js';
|
|
14
13
|
|
|
15
14
|
import { agentStateDir } from '../context/agentHome.js';
|
|
16
15
|
import { CATEGORY_DIR, MEMORY_ID_PATTERN } from './constants.js';
|
|
17
16
|
import type { MemoryRecord, MemoryScope } from './types.js';
|
|
18
17
|
|
|
19
|
-
const LOCK_STALE_MS = 120_000;
|
|
20
|
-
const LOCK_WAIT_MS = 15_000;
|
|
21
18
|
let mutationQueue: Promise<void> = Promise.resolve();
|
|
22
19
|
|
|
23
20
|
export function memoryRootDir(): string {
|
|
@@ -84,28 +81,7 @@ export async function acquireFileLock(
|
|
|
84
81
|
): Promise<() => Promise<void>> {
|
|
85
82
|
signal?.throwIfAborted();
|
|
86
83
|
await ensurePrivateDir(dirname(path));
|
|
87
|
-
|
|
88
|
-
while (true) {
|
|
89
|
-
signal?.throwIfAborted();
|
|
90
|
-
try {
|
|
91
|
-
// Do not steal old-version file locks. Restart old hosts before upgrading;
|
|
92
|
-
// their locks must be reviewed rather than aged out under a live writer.
|
|
93
|
-
const legacy = await stat(`${path}.lock`).catch(error => {
|
|
94
|
-
if (error.code === 'ENOENT') return null;
|
|
95
|
-
throw error;
|
|
96
|
-
});
|
|
97
|
-
if (legacy?.isFile()) throw new Error('MEMORY_LEGACY_LOCK:请停止旧版本宿主并检查遗留记忆锁');
|
|
98
|
-
const release = await lockfile.lock(path, {
|
|
99
|
-
realpath: false, retries: 0, stale: LOCK_STALE_MS, update: 10_000,
|
|
100
|
-
});
|
|
101
|
-
if (signal?.aborted) { await release(); signal.throwIfAborted(); }
|
|
102
|
-
return release;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if ((error as NodeJS.ErrnoException).code !== 'ELOCKED') throw error;
|
|
105
|
-
if (Date.now() >= deadline) throw new Error('等待记忆文件锁超时');
|
|
106
|
-
await delay(40, undefined, { signal });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
84
|
+
return acquireStateFileLock(path, { signal, label: 'MEMORY' });
|
|
109
85
|
}
|
|
110
86
|
|
|
111
87
|
export async function atomicWriteFile(
|
package/src/memory/jobPolicy.js
CHANGED
|
@@ -13,23 +13,31 @@ export function recordJobFailure(job, error) {
|
|
|
13
13
|
: /VERSION_CONFLICT|SOURCE_CHANGED/.test(message) ? 'conflict'
|
|
14
14
|
: /timeout|超时|429|503|fetch|network|ECONN|连接/i.test(message) ? 'transient'
|
|
15
15
|
: 'unknown';
|
|
16
|
-
|
|
16
|
+
// Changing failure categories is not recovery. Otherwise alternating provider
|
|
17
|
+
// and proposal failures can keep a source retrying forever.
|
|
18
|
+
const consecutive = (job.failure?.consecutive ?? 0) + 1;
|
|
17
19
|
job.attempts = (job.attempts ?? 0) + 1;
|
|
18
|
-
job.failure = { kind, consecutive, paused: consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
20
|
+
job.failure = { kind, consecutive, paused: Boolean(job.failure?.paused) || consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
19
21
|
job.nextAttemptAt = retryAt(consecutive);
|
|
20
22
|
}
|
|
21
23
|
export function jobEligible(job, force = false, now = Date.now()) {
|
|
22
24
|
return force || (!job.failure?.paused && (job.nextAttemptAt ?? 0) <= now);
|
|
23
25
|
}
|
|
24
|
-
const
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
const health = new Map();
|
|
27
|
+
/** Background services never write over a host's TUI. Durable queues retain
|
|
28
|
+
* source failures; this bounded process snapshot also exposes scheduler/IO errors
|
|
29
|
+
* that could not be persisted. No source text, paths or provider secrets. */
|
|
30
|
+
export function recordBackgroundLearningFailure(workspaceId, partition, phase) {
|
|
31
|
+
const key = `${workspaceId}:${partition}`;
|
|
32
|
+
const previous = health.get(key);
|
|
33
|
+
health.delete(key);
|
|
34
|
+
health.set(key, { partition, phase, failures: (previous?.failures ?? 0) + 1, at: Date.now() });
|
|
35
|
+
if (health.size > 128)
|
|
36
|
+
health.delete(health.keys().next().value);
|
|
37
|
+
}
|
|
38
|
+
export function backgroundLearningHealth(workspaceId) {
|
|
39
|
+
return ['facts', 'experience'].flatMap(partition => {
|
|
40
|
+
const value = health.get(`${workspaceId}:${partition}`);
|
|
41
|
+
return value ? [{ ...value }] : [];
|
|
42
|
+
});
|
|
35
43
|
}
|
package/src/memory/jobPolicy.ts
CHANGED
|
@@ -16,9 +16,11 @@ export function recordJobFailure(job: Job, error: unknown): void {
|
|
|
16
16
|
: /VERSION_CONFLICT|SOURCE_CHANGED/.test(message) ? 'conflict'
|
|
17
17
|
: /timeout|超时|429|503|fetch|network|ECONN|连接/i.test(message) ? 'transient'
|
|
18
18
|
: 'unknown';
|
|
19
|
-
|
|
19
|
+
// Changing failure categories is not recovery. Otherwise alternating provider
|
|
20
|
+
// and proposal failures can keep a source retrying forever.
|
|
21
|
+
const consecutive = (job.failure?.consecutive ?? 0) + 1;
|
|
20
22
|
job.attempts = (job.attempts ?? 0) + 1;
|
|
21
|
-
job.failure = { kind, consecutive, paused: consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
23
|
+
job.failure = { kind, consecutive, paused: Boolean(job.failure?.paused) || consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
22
24
|
job.nextAttemptAt = retryAt(consecutive);
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -26,13 +28,22 @@ export function jobEligible(job: Job, force = false, now = Date.now()): boolean
|
|
|
26
28
|
return force || (!job.failure?.paused && (job.nextAttemptAt ?? 0) <= now);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
type Partition = 'facts' | 'experience';
|
|
32
|
+
type Health = { partition: Partition; phase: 'learning' | 'scheduling'; failures: number; at: number };
|
|
33
|
+
const health = new Map<string, Health>();
|
|
34
|
+
/** Background services never write over a host's TUI. Durable queues retain
|
|
35
|
+
* source failures; this bounded process snapshot also exposes scheduler/IO errors
|
|
36
|
+
* that could not be persisted. No source text, paths or provider secrets. */
|
|
37
|
+
export function recordBackgroundLearningFailure(workspaceId: string, partition: Partition, phase: Health['phase']): void {
|
|
38
|
+
const key = `${workspaceId}:${partition}`;
|
|
39
|
+
const previous = health.get(key);
|
|
40
|
+
health.delete(key);
|
|
41
|
+
health.set(key, { partition, phase, failures: (previous?.failures ?? 0) + 1, at: Date.now() });
|
|
42
|
+
if (health.size > 128) health.delete(health.keys().next().value!);
|
|
43
|
+
}
|
|
44
|
+
export function backgroundLearningHealth(workspaceId: string): Health[] {
|
|
45
|
+
return (['facts', 'experience'] as const).flatMap(partition => {
|
|
46
|
+
const value = health.get(`${workspaceId}:${partition}`);
|
|
47
|
+
return value ? [{ ...value }] : [];
|
|
48
|
+
});
|
|
38
49
|
}
|
package/src/memory/learning.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { parseMemoryProposal } from './proposal.js';
|
|
7
7
|
import { acquireModelSlot, MAX_MODEL_DURATION_MS } from './modelBudget.js';
|
|
8
|
-
import { jobEligible, recordJobFailure,
|
|
8
|
+
import { jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { ensurePrivateDir } from './fileStore.js';
|
|
@@ -343,7 +343,7 @@ export function scheduleMemoryMaintenance(options) {
|
|
|
343
343
|
void runMemoryMaintenance(options).catch((error) => {
|
|
344
344
|
if (options.signal?.aborted)
|
|
345
345
|
return;
|
|
346
|
-
|
|
346
|
+
recordBackgroundLearningFailure(options.workspaceId, 'experience', 'learning');
|
|
347
347
|
}).finally(async () => {
|
|
348
348
|
const settings = await memoryService.settings();
|
|
349
349
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || options.signal?.aborted || !settings.autoRemember || !settings.learning?.enabled)
|
|
@@ -358,7 +358,7 @@ export function scheduleMemoryMaintenance(options) {
|
|
|
358
358
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || active.has(experienceDirectory(options.workspaceId)))
|
|
359
359
|
return;
|
|
360
360
|
wakeAt(experienceDirectory(options.workspaceId), Math.max(next, Date.now() + 1000), () => scheduleMemoryMaintenance({ ...options, force: false }), options.signal);
|
|
361
|
-
}).catch(
|
|
361
|
+
}).catch(() => recordBackgroundLearningFailure(options.workspaceId, 'experience', 'scheduling'));
|
|
362
362
|
}
|
|
363
363
|
/** Tests and orderly hosts may drain without polling or busy waiting. */
|
|
364
364
|
export async function drainMemoryMaintenance() {
|
package/src/memory/learning.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { parseMemoryProposal } from './proposal.js';
|
|
7
7
|
import { acquireModelSlot, MAX_MODEL_DURATION_MS } from './modelBudget.js';
|
|
8
|
-
import { jobEligible, recordJobFailure,
|
|
8
|
+
import { jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { ensurePrivateDir } from './fileStore.js';
|
|
@@ -423,7 +423,7 @@ export function scheduleMemoryMaintenance(
|
|
|
423
423
|
if (active.has(experienceDirectory(options.workspaceId))) return;
|
|
424
424
|
void runMemoryMaintenance(options).catch((error) => {
|
|
425
425
|
if (options.signal?.aborted) return;
|
|
426
|
-
|
|
426
|
+
recordBackgroundLearningFailure(options.workspaceId, 'experience', 'learning');
|
|
427
427
|
}).finally(async () => {
|
|
428
428
|
const settings = await memoryService.settings();
|
|
429
429
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || options.signal?.aborted || !settings.autoRemember || !settings.learning?.enabled) return;
|
|
@@ -436,7 +436,7 @@ export function scheduleMemoryMaintenance(
|
|
|
436
436
|
// Cancellation can happen during the preceding disk reads.
|
|
437
437
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || active.has(experienceDirectory(options.workspaceId))) return;
|
|
438
438
|
wakeAt(experienceDirectory(options.workspaceId), Math.max(next, Date.now() + 1000), () => scheduleMemoryMaintenance({ ...options, force: false }), options.signal);
|
|
439
|
-
}).catch(
|
|
439
|
+
}).catch(() => recordBackgroundLearningFailure(options.workspaceId, 'experience', 'scheduling'));
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
/** Tests and orderly hosts may drain without polling or busy waiting. */
|
|
@@ -43,8 +43,10 @@ export function createLearningModel(provider, execute = chat) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
signal.throwIfAborted();
|
|
46
|
-
if (!finished
|
|
47
|
-
throw new
|
|
46
|
+
if (!finished)
|
|
47
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(截断或缺少正常结束标记),保留待处理资料');
|
|
48
|
+
if (!text.trim())
|
|
49
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(空正文),保留待处理资料');
|
|
48
50
|
return { text, usage };
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -49,8 +49,10 @@ export function createLearningModel(
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
signal.throwIfAborted();
|
|
52
|
-
if (!finished
|
|
53
|
-
throw new
|
|
52
|
+
if (!finished)
|
|
53
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(截断或缺少正常结束标记),保留待处理资料');
|
|
54
|
+
if (!text.trim())
|
|
55
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(空正文),保留待处理资料');
|
|
54
56
|
return { text, usage };
|
|
55
57
|
};
|
|
56
58
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** One delegation contract shared by the main prompt, tool discovery and runtime. */
|
|
2
|
+
export const RESEARCH_DELEGATION_POLICY = `# 大范围阅读与并行调研
|
|
3
|
+
- 先读项目说明、目录、入口与基线,建立问题地图,不先串行读完整仓库。批量阅读涉及多个独立模块、各需多轮探索时,优先同轮派发多个 Agent(agent_type="explore");数量取决于独立工作量和宿主额度,不按文件数量机械切块。
|
|
4
|
+
- 少量已知文件同轮批量读;强依赖调用链自行跟进。只读委派无需写入铺路或 worktree,不为获得 shell 升级 general。
|
|
5
|
+
- 自包含任务书优先用 context.researchQuestions 写清问题、范围、停止条件,附基线、已知证据与排除项,不复制主历史。
|
|
6
|
+
- 有独立工作时后台派发,否则同轮前台派发;不立即轮询或重复调查。整合证据、冲突与缺口;摘要不代表全文覆盖,并发不保证降低总 Token。`;
|
|
7
|
+
export const AGENT_TOOL_DESCRIPTION = '启动隔离子任务(fork)并返回 agent_id。大型多模块批量阅读先定位入口,再优先同轮派发独立 explore 调查;少量已知文件直接批量读。explore 默认只读,general 使用 write_scopes 或 worktree。任务书自包含,context.researchQuestions 定义问题、范围、停止条件;AgentReport.findings 返回证据结论。run_in_background 立即返回供主 Agent 推进独立工作;resume_agent_id 续接子任务私有历史。reportMaxChars 限制预览,完整交接由 TaskOutput 获取。';
|
|
8
|
+
export function delegationRuntimeGuidance(input) {
|
|
9
|
+
if (!input.available)
|
|
10
|
+
return '';
|
|
11
|
+
const remaining = Math.max(0, input.maxCalls - input.callsUsed);
|
|
12
|
+
if (!remaining)
|
|
13
|
+
return '本轮子任务派发次数已用完:收口已有子任务,其余调查由主 Agent 使用当前工具完成,不重复尝试派发。';
|
|
14
|
+
return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(8, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One delegation contract shared by the main prompt, tool discovery and runtime. */
|
|
2
|
+
export const RESEARCH_DELEGATION_POLICY = `# 大范围阅读与并行调研
|
|
3
|
+
- 先读项目说明、目录、入口与基线,建立问题地图,不先串行读完整仓库。批量阅读涉及多个独立模块、各需多轮探索时,优先同轮派发多个 Agent(agent_type="explore");数量取决于独立工作量和宿主额度,不按文件数量机械切块。
|
|
4
|
+
- 少量已知文件同轮批量读;强依赖调用链自行跟进。只读委派无需写入铺路或 worktree,不为获得 shell 升级 general。
|
|
5
|
+
- 自包含任务书优先用 context.researchQuestions 写清问题、范围、停止条件,附基线、已知证据与排除项,不复制主历史。
|
|
6
|
+
- 有独立工作时后台派发,否则同轮前台派发;不立即轮询或重复调查。整合证据、冲突与缺口;摘要不代表全文覆盖,并发不保证降低总 Token。`;
|
|
7
|
+
|
|
8
|
+
export const AGENT_TOOL_DESCRIPTION = '启动隔离子任务(fork)并返回 agent_id。大型多模块批量阅读先定位入口,再优先同轮派发独立 explore 调查;少量已知文件直接批量读。explore 默认只读,general 使用 write_scopes 或 worktree。任务书自包含,context.researchQuestions 定义问题、范围、停止条件;AgentReport.findings 返回证据结论。run_in_background 立即返回供主 Agent 推进独立工作;resume_agent_id 续接子任务私有历史。reportMaxChars 限制预览,完整交接由 TaskOutput 获取。';
|
|
9
|
+
|
|
10
|
+
export function delegationRuntimeGuidance(input: {
|
|
11
|
+
available: boolean;
|
|
12
|
+
maxConcurrent: number;
|
|
13
|
+
callsUsed: number;
|
|
14
|
+
maxCalls: number;
|
|
15
|
+
}): string {
|
|
16
|
+
if (!input.available) return '';
|
|
17
|
+
const remaining = Math.max(0, input.maxCalls - input.callsUsed);
|
|
18
|
+
if (!remaining) return '本轮子任务派发次数已用完:收口已有子任务,其余调查由主 Agent 使用当前工具完成,不重复尝试派发。';
|
|
19
|
+
return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(8, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
|
|
20
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CODE_REVIEW_RUBRIC } from './codeReview.js';
|
|
2
|
+
import { RESEARCH_DELEGATION_POLICY } from './delegation.js';
|
|
2
3
|
import { capabilityForToolName, } from '../tools/capabilities.js';
|
|
3
4
|
import { skillInvokeInstruction } from './skillList.js';
|
|
5
|
+
import { visualPresentation } from './visualPresentation.js';
|
|
4
6
|
export function capabilitiesForTools(tools) {
|
|
5
7
|
const result = new Set();
|
|
6
8
|
for (const tool of tools) {
|
|
@@ -194,15 +196,10 @@ const CAPABILITY_POLICIES = [
|
|
|
194
196
|
},
|
|
195
197
|
{
|
|
196
198
|
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
197
|
-
render: ({ surface, toolNames }) => [
|
|
198
|
-
|
|
199
|
-
surface === 'desktop'
|
|
200
|
-
? '- Desktop/Web 聊天支持 mermaid 和 svg 围栏,不用裸 HTML。'
|
|
201
|
-
: surface === 'cli' || surface === 'tui'
|
|
202
|
-
? '- CLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。'
|
|
203
|
-
: '- 跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。',
|
|
199
|
+
render: ({ surface, output, toolNames }) => [
|
|
200
|
+
visualPresentation(output ?? surface),
|
|
204
201
|
toolNames.has('Skill') || toolNames.has('Read')
|
|
205
|
-
? '-
|
|
202
|
+
? '- 配图前加载可用的 dynamic-ui。'
|
|
206
203
|
: '',
|
|
207
204
|
].filter(Boolean).join('\n'),
|
|
208
205
|
},
|
|
@@ -264,7 +261,7 @@ const CAPABILITY_POLICIES = [
|
|
|
264
261
|
{
|
|
265
262
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
266
263
|
requiresTools: ['Agent'],
|
|
267
|
-
render: () =>
|
|
264
|
+
render: () => RESEARCH_DELEGATION_POLICY,
|
|
268
265
|
},
|
|
269
266
|
{
|
|
270
267
|
id: 'capability.agent-handoff', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -299,7 +296,7 @@ const CAPABILITY_POLICIES = [
|
|
|
299
296
|
if (toolNames.has('RecallMemory'))
|
|
300
297
|
rules.push('自包含任务无需检索记忆。依赖跨会话用户信息、偏好、决定或项目背景时,先检查已注入证据;缺少相关信息再用 RecallMemory,避免重复检索或直接反问用户。查询无关命中时停止扩大检索;旧结论与现场冲突时重新查证,未在本轮核验的历史结论标明来源及可能过时。');
|
|
301
298
|
if (toolNames.has('RecallMemory') && toolNames.has('UpdateMemory'))
|
|
302
|
-
rules.push('
|
|
299
|
+
rules.push('RecallMemory 读版本 → UpdateMemory:update 原话/review 待复核/confirm 确认;冲突重读,拒绝不重试。');
|
|
303
300
|
if (toolNames.has('ForgetMemory'))
|
|
304
301
|
rules.push('ForgetMemory 仅响应当前明确遗忘要求。');
|
|
305
302
|
if (toolNames.has('ConfirmMemory'))
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Tool } from '../types.js';
|
|
2
2
|
import { CODE_REVIEW_RUBRIC } from './codeReview.js';
|
|
3
|
+
import { RESEARCH_DELEGATION_POLICY } from './delegation.js';
|
|
3
4
|
import type { AgentProfile, AgentSurface } from '../runtime/agentRuntimeContext.js';
|
|
4
5
|
import {
|
|
5
6
|
capabilityForToolName,
|
|
6
7
|
type CapabilityId,
|
|
7
8
|
} from '../tools/capabilities.js';
|
|
8
9
|
import { skillInvokeInstruction } from './skillList.js';
|
|
10
|
+
import { visualPresentation } from './visualPresentation.js';
|
|
11
|
+
import type { OutputContract } from '../runtime/outputContract.js';
|
|
9
12
|
|
|
10
13
|
export type PromptPolicyLayer =
|
|
11
14
|
| 'authority'
|
|
@@ -31,6 +34,7 @@ export type { CapabilityId } from '../tools/capabilities.js';
|
|
|
31
34
|
export interface StablePromptContext {
|
|
32
35
|
profile: AgentProfile;
|
|
33
36
|
surface: AgentSurface;
|
|
37
|
+
output?: OutputContract;
|
|
34
38
|
cwd: string;
|
|
35
39
|
capabilities: ReadonlySet<CapabilityId>;
|
|
36
40
|
toolNames: ReadonlySet<string>;
|
|
@@ -241,15 +245,10 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
241
245
|
},
|
|
242
246
|
{
|
|
243
247
|
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
244
|
-
render: ({ surface, toolNames }) => [
|
|
245
|
-
|
|
246
|
-
surface === 'desktop'
|
|
247
|
-
? '- Desktop/Web 聊天支持 mermaid 和 svg 围栏,不用裸 HTML。'
|
|
248
|
-
: surface === 'cli' || surface === 'tui'
|
|
249
|
-
? '- CLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。'
|
|
250
|
-
: '- 跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。',
|
|
248
|
+
render: ({ surface, output, toolNames }) => [
|
|
249
|
+
visualPresentation(output ?? surface),
|
|
251
250
|
toolNames.has('Skill') || toolNames.has('Read')
|
|
252
|
-
? '-
|
|
251
|
+
? '- 配图前加载可用的 dynamic-ui。'
|
|
253
252
|
: '',
|
|
254
253
|
].filter(Boolean).join('\n'),
|
|
255
254
|
},
|
|
@@ -311,7 +310,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
311
310
|
{
|
|
312
311
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
313
312
|
requiresTools: ['Agent'],
|
|
314
|
-
render: () =>
|
|
313
|
+
render: () => RESEARCH_DELEGATION_POLICY,
|
|
315
314
|
},
|
|
316
315
|
{
|
|
317
316
|
id: 'capability.agent-handoff', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -343,7 +342,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
343
342
|
];
|
|
344
343
|
if (toolNames.has('RememberMemory')) rules.push('RememberMemory 保存明确要求记住的新事实;后台提炼启用时普通陈述交后台处理,不重复写入。自动记忆关闭时仅响应明确保存要求;失败如实说明,不阻断任务。');
|
|
345
344
|
if (toolNames.has('RecallMemory')) rules.push('自包含任务无需检索记忆。依赖跨会话用户信息、偏好、决定或项目背景时,先检查已注入证据;缺少相关信息再用 RecallMemory,避免重复检索或直接反问用户。查询无关命中时停止扩大检索;旧结论与现场冲突时重新查证,未在本轮核验的历史结论标明来源及可能过时。');
|
|
346
|
-
if (toolNames.has('RecallMemory') && toolNames.has('UpdateMemory')) rules.push('
|
|
345
|
+
if (toolNames.has('RecallMemory') && toolNames.has('UpdateMemory')) rules.push('RecallMemory 读版本 → UpdateMemory:update 原话/review 待复核/confirm 确认;冲突重读,拒绝不重试。');
|
|
347
346
|
if (toolNames.has('ForgetMemory')) rules.push('ForgetMemory 仅响应当前明确遗忘要求。');
|
|
348
347
|
if (toolNames.has('ConfirmMemory')) rules.push('用户重新确认相同事实时使用 ConfirmMemory 更新确认时间,不创建副本。');
|
|
349
348
|
if (toolNames.has('ReviewMemories')) rules.push('已过复核周期的记录只降低优先级并提示复核,不自动删除。');
|
|
@@ -235,17 +235,24 @@ async function buildAdvisorTransient(profile, tools) {
|
|
|
235
235
|
return '';
|
|
236
236
|
return buildAdvisorStatusLine();
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
/** One default contract for stable, transient and combined prompt entry points.
|
|
239
|
+
* Preserve explicit legacy fields at this compatibility boundary; omission never
|
|
240
|
+
* grants memory, browser or vision capabilities.
|
|
241
|
+
*/
|
|
242
|
+
function resolvePromptRuntime(options) {
|
|
243
|
+
return {
|
|
244
|
+
surface: (options.profile ?? 'main') === 'main' ? 'cli' : 'subagent',
|
|
242
245
|
conversationMode: 'private',
|
|
243
|
-
cwd,
|
|
246
|
+
cwd: options.cwd,
|
|
244
247
|
interactiveAsk: true,
|
|
245
248
|
supervisedBrowser: false,
|
|
246
249
|
ownerLocalSession: false,
|
|
247
250
|
...options.runtime,
|
|
248
251
|
};
|
|
252
|
+
}
|
|
253
|
+
export async function assemblePromptEnvelope(options) {
|
|
254
|
+
const { cwd, tools, profile = 'main', includeProjectInstructions = true, includePlugins = profile === 'main', } = options;
|
|
255
|
+
const runtime = resolvePromptRuntime(options);
|
|
249
256
|
const { base, capabilities, stableContext } = await buildStableBase(cwd, tools, profile, runtime);
|
|
250
257
|
const contextBundle = await buildTransientBundle(runtime, profile, tools, {
|
|
251
258
|
includeProjectInstructions,
|
|
@@ -273,6 +280,7 @@ async function buildStableBase(cwd, tools, profile, runtime) {
|
|
|
273
280
|
const stableContext = {
|
|
274
281
|
profile,
|
|
275
282
|
surface: runtime.surface,
|
|
283
|
+
output: runtime.output,
|
|
276
284
|
cwd,
|
|
277
285
|
capabilities,
|
|
278
286
|
toolNames: new Set(tools.map((tool) => tool.name)),
|
|
@@ -289,19 +297,12 @@ async function buildStableBase(cwd, tools, profile, runtime) {
|
|
|
289
297
|
/** Build only the cache-stable layer; performs no memory or project scans. */
|
|
290
298
|
export async function assembleStablePrompt(options) {
|
|
291
299
|
const profile = options.profile ?? 'main';
|
|
292
|
-
const runtime =
|
|
293
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
294
|
-
conversationMode: 'private',
|
|
295
|
-
cwd: options.cwd,
|
|
296
|
-
interactiveAsk: true,
|
|
297
|
-
supervisedBrowser: false,
|
|
298
|
-
ownerLocalSession: false,
|
|
299
|
-
...options.runtime,
|
|
300
|
-
};
|
|
300
|
+
const runtime = resolvePromptRuntime(options);
|
|
301
301
|
const key = [
|
|
302
302
|
options.cwd,
|
|
303
303
|
profile,
|
|
304
304
|
runtime.surface,
|
|
305
|
+
JSON.stringify(runtime.output ?? null),
|
|
305
306
|
runtime.conversationMode,
|
|
306
307
|
createHash('sha256')
|
|
307
308
|
.update(JSON.stringify(options.tools.map((tool) => ({
|
|
@@ -544,15 +545,7 @@ export async function buildTransientContext(options) {
|
|
|
544
545
|
/** Structured counterpart used by the Harness for request-time budget reduction. */
|
|
545
546
|
export async function buildTransientPromptBundle(options) {
|
|
546
547
|
const profile = options.profile ?? 'main';
|
|
547
|
-
const runtime =
|
|
548
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
549
|
-
conversationMode: 'private',
|
|
550
|
-
cwd: options.cwd,
|
|
551
|
-
interactiveAsk: true,
|
|
552
|
-
supervisedBrowser: false,
|
|
553
|
-
ownerLocalSession: false,
|
|
554
|
-
...options.runtime,
|
|
555
|
-
};
|
|
548
|
+
const runtime = resolvePromptRuntime(options);
|
|
556
549
|
return buildTransientBundle(runtime, profile, options.tools, {
|
|
557
550
|
includeProjectInstructions: options.includeProjectInstructions ?? true,
|
|
558
551
|
includePlugins: options.includePlugins ?? profile === 'main',
|
|
@@ -88,6 +88,7 @@ const stablePromptCache = new Map<string, string>();
|
|
|
88
88
|
import { normalizeBrowserRuntime } from '../runtime/browserRuntimeContext.js';
|
|
89
89
|
|
|
90
90
|
export interface PromptRuntimeContext {
|
|
91
|
+
output?: import('../runtime/outputContract.js').OutputContract;
|
|
91
92
|
surface: PromptSurface;
|
|
92
93
|
host?: 'desktop' | 'tui' | 'cli' | 'stream-json' | 'workflow';
|
|
93
94
|
conversationMode: import('../runtime/agentRuntimeContext.js').ConversationMode;
|
|
@@ -377,6 +378,22 @@ async function buildAdvisorTransient(
|
|
|
377
378
|
return buildAdvisorStatusLine();
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
/** One default contract for stable, transient and combined prompt entry points.
|
|
382
|
+
* Preserve explicit legacy fields at this compatibility boundary; omission never
|
|
383
|
+
* grants memory, browser or vision capabilities.
|
|
384
|
+
*/
|
|
385
|
+
function resolvePromptRuntime(options: AssemblePromptOptions): PromptRuntimeContext {
|
|
386
|
+
return {
|
|
387
|
+
surface: (options.profile ?? 'main') === 'main' ? 'cli' : 'subagent',
|
|
388
|
+
conversationMode: 'private',
|
|
389
|
+
cwd: options.cwd,
|
|
390
|
+
interactiveAsk: true,
|
|
391
|
+
supervisedBrowser: false,
|
|
392
|
+
ownerLocalSession: false,
|
|
393
|
+
...options.runtime,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
380
397
|
export async function assemblePromptEnvelope(
|
|
381
398
|
options: AssemblePromptOptions,
|
|
382
399
|
): Promise<PromptEnvelope> {
|
|
@@ -387,15 +404,7 @@ export async function assemblePromptEnvelope(
|
|
|
387
404
|
includeProjectInstructions = true,
|
|
388
405
|
includePlugins = profile === 'main',
|
|
389
406
|
} = options;
|
|
390
|
-
const runtime
|
|
391
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
392
|
-
conversationMode: 'private',
|
|
393
|
-
cwd,
|
|
394
|
-
interactiveAsk: true,
|
|
395
|
-
supervisedBrowser: false,
|
|
396
|
-
ownerLocalSession: false,
|
|
397
|
-
...options.runtime,
|
|
398
|
-
};
|
|
407
|
+
const runtime = resolvePromptRuntime(options);
|
|
399
408
|
|
|
400
409
|
const { base, capabilities, stableContext } = await buildStableBase(
|
|
401
410
|
cwd,
|
|
@@ -439,6 +448,7 @@ async function buildStableBase(
|
|
|
439
448
|
const stableContext: StablePromptContext = {
|
|
440
449
|
profile,
|
|
441
450
|
surface: runtime.surface,
|
|
451
|
+
output: runtime.output,
|
|
442
452
|
cwd,
|
|
443
453
|
capabilities,
|
|
444
454
|
toolNames: new Set(tools.map((tool) => tool.name)),
|
|
@@ -458,19 +468,12 @@ export async function assembleStablePrompt(
|
|
|
458
468
|
options: AssemblePromptOptions,
|
|
459
469
|
): Promise<string> {
|
|
460
470
|
const profile = options.profile ?? 'main';
|
|
461
|
-
const runtime
|
|
462
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
463
|
-
conversationMode: 'private',
|
|
464
|
-
cwd: options.cwd,
|
|
465
|
-
interactiveAsk: true,
|
|
466
|
-
supervisedBrowser: false,
|
|
467
|
-
ownerLocalSession: false,
|
|
468
|
-
...options.runtime,
|
|
469
|
-
};
|
|
471
|
+
const runtime = resolvePromptRuntime(options);
|
|
470
472
|
const key = [
|
|
471
473
|
options.cwd,
|
|
472
474
|
profile,
|
|
473
475
|
runtime.surface,
|
|
476
|
+
JSON.stringify(runtime.output ?? null),
|
|
474
477
|
runtime.conversationMode,
|
|
475
478
|
createHash('sha256')
|
|
476
479
|
.update(
|
|
@@ -784,15 +787,7 @@ export async function buildTransientPromptBundle(
|
|
|
784
787
|
options: AssemblePromptOptions,
|
|
785
788
|
): Promise<ContextBundle> {
|
|
786
789
|
const profile = options.profile ?? 'main';
|
|
787
|
-
const runtime
|
|
788
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
789
|
-
conversationMode: 'private',
|
|
790
|
-
cwd: options.cwd,
|
|
791
|
-
interactiveAsk: true,
|
|
792
|
-
supervisedBrowser: false,
|
|
793
|
-
ownerLocalSession: false,
|
|
794
|
-
...options.runtime,
|
|
795
|
-
};
|
|
790
|
+
const runtime = resolvePromptRuntime(options);
|
|
796
791
|
return buildTransientBundle(runtime, profile, options.tools, {
|
|
797
792
|
includeProjectInstructions: options.includeProjectInstructions ?? true,
|
|
798
793
|
includePlugins: options.includePlugins ?? profile === 'main',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolveOutputContract } from '../runtime/outputContract.js';
|
|
2
|
+
/** One output contract shared by preparation and tool-free publication. */
|
|
3
|
+
export function visualPresentation(target) {
|
|
4
|
+
const output = typeof target === 'string' ? resolveOutputContract(target) : target;
|
|
5
|
+
const common = '按需配图,优先 Mermaid/SVG;用户指定 ASCII、代码/目录树除外。';
|
|
6
|
+
if (output.format === 'rich-markdown')
|
|
7
|
+
return `${common}\nDesktop/Web 聊天支持 ${output.diagrams.length ? output.diagrams.join(' 和 ') + ' 围栏' : 'Markdown,未声明图形渲染'},不用裸 HTML。仅用已声明的图形格式替代字符框图;最终表达同样生效。${output.images ? '支持图片。' : ''}${output.attachments ? '附件须真实提交。' : ''}无法可靠画图则用文字/表格。`;
|
|
8
|
+
if (output.format === 'terminal')
|
|
9
|
+
return `${common}\nCLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。`;
|
|
10
|
+
if (output.format === 'structured')
|
|
11
|
+
return '仅按调用方结构化协议输出,不插入 Markdown 围栏或图形源码。';
|
|
12
|
+
return `${common}\n跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。`;
|
|
13
|
+
}
|
|
14
|
+
/** Conservative signal on PRIVATE candidates only, never a frontend converter.
|
|
15
|
+
* Require box borders in prose fences; ordinary code, trees and arrows are not diagrams.
|
|
16
|
+
*/
|
|
17
|
+
export function needsDiagramPreparation(candidate, request) {
|
|
18
|
+
const rejectsAscii = /(?:不要|不用|避免|别用|without|not|instead of)[^\n。!?]{0,24}(?:ascii|字符图|纯文本图)/i.test(request);
|
|
19
|
+
if (!rejectsAscii && /ascii|字符图|纯文本图|原样|不要.*(?:mermaid|svg)/i.test(request))
|
|
20
|
+
return false;
|
|
21
|
+
const fences = candidate.matchAll(/^(?:```|~~~)(?:text|plaintext)?[ \t]*\r?\n([\s\S]*?)^(?:```|~~~)[ \t]*$/gm);
|
|
22
|
+
for (const [, body] of fences) {
|
|
23
|
+
const borders = body.split('\n').filter(line => /[┌┐└┘┏┓┗┛+][─━\-]{3,}[┌┐└┘┏┓┗┛+]/.test(line));
|
|
24
|
+
if (borders.length >= 2 && /[│┃|].*\S.*[│┃|]/.test(body))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentSurface } from '../runtime/agentRuntimeContext.js';
|
|
2
|
+
import { resolveOutputContract, type OutputContract } from '../runtime/outputContract.js';
|
|
3
|
+
|
|
4
|
+
/** One output contract shared by preparation and tool-free publication. */
|
|
5
|
+
export function visualPresentation(target: AgentSurface | OutputContract): string {
|
|
6
|
+
const output = typeof target === 'string' ? resolveOutputContract(target) : target;
|
|
7
|
+
const common = '按需配图,优先 Mermaid/SVG;用户指定 ASCII、代码/目录树除外。';
|
|
8
|
+
if (output.format === 'rich-markdown') return `${common}\nDesktop/Web 聊天支持 ${output.diagrams.length ? output.diagrams.join(' 和 ') + ' 围栏' : 'Markdown,未声明图形渲染'},不用裸 HTML。仅用已声明的图形格式替代字符框图;最终表达同样生效。${output.images ? '支持图片。' : ''}${output.attachments ? '附件须真实提交。' : ''}无法可靠画图则用文字/表格。`;
|
|
9
|
+
if (output.format === 'terminal') return `${common}\nCLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。`;
|
|
10
|
+
if (output.format === 'structured') return '仅按调用方结构化协议输出,不插入 Markdown 围栏或图形源码。';
|
|
11
|
+
return `${common}\n跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Conservative signal on PRIVATE candidates only, never a frontend converter.
|
|
15
|
+
* Require box borders in prose fences; ordinary code, trees and arrows are not diagrams.
|
|
16
|
+
*/
|
|
17
|
+
export function needsDiagramPreparation(candidate: string, request: string): boolean {
|
|
18
|
+
const rejectsAscii = /(?:不要|不用|避免|别用|without|not|instead of)[^\n。!?]{0,24}(?:ascii|字符图|纯文本图)/i.test(request);
|
|
19
|
+
if (!rejectsAscii && /ascii|字符图|纯文本图|原样|不要.*(?:mermaid|svg)/i.test(request)) return false;
|
|
20
|
+
const fences = candidate.matchAll(/^(?:```|~~~)(?:text|plaintext)?[ \t]*\r?\n([\s\S]*?)^(?:```|~~~)[ \t]*$/gm);
|
|
21
|
+
for (const [, body] of fences) {
|
|
22
|
+
const borders = body!.split('\n').filter(line => /[┌┐└┘┏┓┗┛+][─━\-]{3,}[┌┐└┘┏┓┗┛+]/.test(line));
|
|
23
|
+
if (borders.length >= 2 && /[│┃|].*\S.*[│┃|]/.test(body!)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveOutputContract } from './outputContract.js';
|
|
2
|
+
export { WEB_OUTPUT, resolveOutputContract } from './outputContract.js';
|
|
1
3
|
import { isMemoryToolName } from '../tools/capabilities.js';
|
|
2
4
|
import { normalizeBrowserRuntime } from './browserRuntimeContext.js';
|
|
3
5
|
export function normalizeMemoryAccess(access) {
|
|
@@ -12,6 +14,7 @@ export function createAgentRuntimeContext(input) {
|
|
|
12
14
|
const { browser: rawBrowser, ...rest } = input;
|
|
13
15
|
return Object.freeze({
|
|
14
16
|
...rest,
|
|
17
|
+
output: resolveOutputContract(input.surface, input.output),
|
|
15
18
|
...(rawBrowser
|
|
16
19
|
? {
|
|
17
20
|
browser: normalizeBrowserRuntime(rawBrowser),
|