minimal-agent 0.18.6 → 0.18.8
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/plugins/workflow-runner/src/runtime/context.js +5 -17
- package/plugins/workflow-runner/src/runtime/context.ts +4 -15
- package/plugins/workflow-runner/src/runtime/model.js +4 -2
- package/plugins/workflow-runner/src/runtime/model.ts +3 -2
- package/plugins/workflow-runner/test/api.test.ts +4 -2
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +5 -6
- package/plugins/workflow-runner/test/modelOutcome.test.ts +36 -0
- package/skills/dynamic-ui/SKILL.md +38 -278
- package/skills/dynamic-ui/mermaid.md +32 -0
- package/skills/dynamic-ui/scenes/architecture-and-flow.md +4 -1
- package/skills/dynamic-ui/scenes/data-visualization.md +5 -2
- package/skills/dynamic-ui/svg-design.md +251 -0
- package/skills/dynamic-ui/templates/README.md +2 -1
- package/skills/dynamic-ui/tokens/visual-tokens.md +4 -5
- package/src/advisor/README.md +24 -0
- package/src/advisor/advisorCoordinator.js +101 -45
- package/src/advisor/advisorCoordinator.ts +90 -51
- package/src/advisor/deliveryReview.js +35 -0
- package/src/advisor/deliveryReview.ts +30 -0
- package/src/advisor/presentation.js +10 -0
- package/src/advisor/presentation.ts +12 -0
- package/src/advisor/reviewContext.js +60 -0
- package/src/advisor/reviewContext.ts +72 -0
- package/src/context/persistContext.js +5 -1
- package/src/context/persistContext.ts +5 -2
- package/src/context/tokenCounter.js +3 -0
- package/src/context/tokenCounter.ts +2 -0
- package/src/llm/messageProvenance.js +1 -1
- package/src/llm/messageProvenance.ts +1 -1
- package/src/loop.js +226 -31
- package/src/loop.ts +205 -31
- package/src/memory/fileStore.js +2 -34
- package/src/memory/fileStore.ts +2 -26
- package/src/prompts/policyRegistry.js +13 -9
- package/src/prompts/policyRegistry.ts +15 -9
- 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/answerPreparation.js +9 -0
- package/src/runtime/answerPreparation.ts +11 -0
- package/src/runtime/eventSemantics.js +2 -0
- package/src/runtime/eventSemantics.ts +2 -0
- package/src/runtime/fileLock.js +75 -0
- package/src/runtime/fileLock.ts +73 -0
- package/src/runtime/finalAnswer.js +9 -0
- package/src/runtime/finalAnswer.ts +13 -0
- package/src/runtime/lifecycleTrace.js +16 -0
- package/src/runtime/lifecycleTrace.ts +22 -0
- 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/runTurn.js +15 -1
- package/src/session/runTurn.ts +13 -1
- package/src/session/runtimeStore.js +11 -28
- package/src/session/runtimeStore.ts +9 -23
- package/src/tools/advisor/advisor.js +56 -40
- package/src/tools/advisor/advisor.ts +57 -39
- package/src/tools/advisor/advisorDecision.js +6 -3
- package/src/tools/advisor/advisorDecision.ts +6 -3
- package/src/tools/agent/agent.js +14 -6
- package/src/tools/agent/agent.ts +14 -5
- package/src/tools/bash/semantics.js +91 -26
- package/src/tools/bash/semantics.ts +63 -33
- package/src/tools/index.js +4 -1
- package/src/tools/index.ts +4 -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/types.ts +19 -6
- package/src/ui/LiveArea.js +2 -1
- package/src/ui/LiveArea.tsx +2 -1
- package/src/ui/MessageList.js +18 -5
- package/src/ui/MessageList.tsx +25 -3
- package/src/ui/draftNotice.js +1 -8
- package/src/ui/draftNotice.ts +1 -10
- package/src/ui/hooks/useChat.js +25 -1
- package/src/ui/hooks/useChat.ts +22 -1
- package/src/web/protocol.ts +3 -1
|
@@ -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),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Tool, ToolAvailability, ToolCallContext } from '../types.js';
|
|
2
2
|
import type { ToolGate } from '../toolGate.js';
|
|
3
|
+
import { resolveOutputContract, type OutputContract } from './outputContract.js';
|
|
4
|
+
export { WEB_OUTPUT, resolveOutputContract, type OutputContract } from './outputContract.js';
|
|
3
5
|
import { isMemoryToolName } from '../tools/capabilities.js';
|
|
4
6
|
import type { ComputerAutomationIntent } from '../tools/computeruse/computerIntent.js';
|
|
5
7
|
|
|
@@ -26,6 +28,7 @@ export type MemoryAccessInput = MemoryAccess | 'local-owner';
|
|
|
26
28
|
*/
|
|
27
29
|
export interface AgentRuntimeContext {
|
|
28
30
|
readonly surface: AgentSurface;
|
|
31
|
+
readonly output?: OutputContract;
|
|
29
32
|
/** Concrete frontend identity; surface remains the execution capability class. */
|
|
30
33
|
readonly host?: 'desktop' | 'tui' | 'cli' | 'stream-json' | 'workflow';
|
|
31
34
|
readonly profile: AgentProfile;
|
|
@@ -75,6 +78,7 @@ export function createAgentRuntimeContext(
|
|
|
75
78
|
const { browser: rawBrowser, ...rest } = input;
|
|
76
79
|
return Object.freeze({
|
|
77
80
|
...rest,
|
|
81
|
+
output: resolveOutputContract(input.surface, input.output),
|
|
78
82
|
...(rawBrowser
|
|
79
83
|
? {
|
|
80
84
|
browser: normalizeBrowserRuntime(rawBrowser),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function answerPreparationNotice(reason) {
|
|
2
|
+
switch (reason) {
|
|
3
|
+
case 'advisor_checkpoint': return '交付前复核:正在核查内部候选结论,完成后发布最终答案。';
|
|
4
|
+
case 'formal_delivery': return '整理交付:正在核对委派结果或补齐正式成果提交。';
|
|
5
|
+
case 'interactive_question': return '等待关键决策:接下来通过交互问题确认,请在输入区作答。';
|
|
6
|
+
case 'empty_response': return '本轮未产生可显示的正文,未生成新的最终答复。';
|
|
7
|
+
case 'interrupted': return '生成未完成,未发布最终答案。';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoopEvent } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export function answerPreparationNotice(reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason'] | 'interrupted'): string {
|
|
4
|
+
switch (reason) {
|
|
5
|
+
case 'advisor_checkpoint': return '交付前复核:正在核查内部候选结论,完成后发布最终答案。';
|
|
6
|
+
case 'formal_delivery': return '整理交付:正在核对委派结果或补齐正式成果提交。';
|
|
7
|
+
case 'interactive_question': return '等待关键决策:接下来通过交互问题确认,请在输入区作答。';
|
|
8
|
+
case 'empty_response': return '本轮未产生可显示的正文,未生成新的最终答复。';
|
|
9
|
+
case 'interrupted': return '生成未完成,未发布最终答案。';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -4,6 +4,8 @@ export function isRecoverableLoopError(event) {
|
|
|
4
4
|
}
|
|
5
5
|
export function completionWarning(event) {
|
|
6
6
|
switch (event.completionStatus) {
|
|
7
|
+
case 'no_answer':
|
|
8
|
+
return event.warning || '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
7
9
|
case 'delivery_incomplete':
|
|
8
10
|
return event.warning || '交付未完成,请检查保留的结果。';
|
|
9
11
|
case 'delivery_unverified':
|
|
@@ -11,6 +11,8 @@ export function completionWarning(
|
|
|
11
11
|
event: Extract<LoopEvent, { type: 'turn_done' }>,
|
|
12
12
|
): string | undefined {
|
|
13
13
|
switch (event.completionStatus) {
|
|
14
|
+
case 'no_answer':
|
|
15
|
+
return event.warning || '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
14
16
|
case 'delivery_incomplete':
|
|
15
17
|
return event.warning || '交付未完成,请检查保留的结果。';
|
|
16
18
|
case 'delivery_unverified':
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
6
|
+
/** Shared heartbeat protocol for local file-backed stores, not a database lock.
|
|
7
|
+
* Never reclaim legacy regular-file locks: old hosts do not speak this protocol.
|
|
8
|
+
* proper-lockfile fails closed on compromised ownership (its default handler).
|
|
9
|
+
*/
|
|
10
|
+
export async function acquireStateFileLock(path, options = {}) {
|
|
11
|
+
const { signal, waitMs = 15_000, label = 'STATE' } = options;
|
|
12
|
+
signal?.throwIfAborted();
|
|
13
|
+
const target = resolve(path);
|
|
14
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
15
|
+
const deadline = Date.now() + waitMs;
|
|
16
|
+
while (true) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
const legacy = await stat(`${target}.lock`).catch((error) => {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return null;
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
if (legacy?.isFile()) {
|
|
24
|
+
throw new Error(`${label}_LEGACY_LOCK:请停止旧版本宿主并检查遗留锁:${target}.lock`);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
let owned = false;
|
|
28
|
+
let identity;
|
|
29
|
+
const guardedFs = {
|
|
30
|
+
...fs,
|
|
31
|
+
rmdir(lockPath, callback) {
|
|
32
|
+
if (!owned)
|
|
33
|
+
return fs.rmdir(lockPath, callback);
|
|
34
|
+
// Also guard immediate release, before the next heartbeat could detect
|
|
35
|
+
// replacement. Never recursively remove a lock directory.
|
|
36
|
+
fs.stat(lockPath, (error, current) => {
|
|
37
|
+
if (error || !identity || current.dev !== identity.dev ||
|
|
38
|
+
current.ino !== identity.ino || current.birthtimeMs !== identity.birthtimeMs) {
|
|
39
|
+
callback(Object.assign(new Error(`锁所有权已丢失:${target}`), { code: 'ECOMPROMISED' }));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
fs.rmdir(lockPath, callback);
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const release = await lockfile.lock(target, {
|
|
47
|
+
fs: guardedFs,
|
|
48
|
+
realpath: false,
|
|
49
|
+
retries: 0,
|
|
50
|
+
stale: 120_000,
|
|
51
|
+
update: 10_000,
|
|
52
|
+
});
|
|
53
|
+
owned = true;
|
|
54
|
+
try {
|
|
55
|
+
identity = await stat(`${target}.lock`);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
await release().catch(() => undefined);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
if (signal?.aborted) {
|
|
62
|
+
await release();
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
}
|
|
65
|
+
return release;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code !== 'ELOCKED')
|
|
69
|
+
throw error;
|
|
70
|
+
if (Date.now() >= deadline)
|
|
71
|
+
throw new Error(`${label}_LOCK_TIMEOUT:等待文件锁超时:${target}`);
|
|
72
|
+
await delay(40, undefined, { signal });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
6
|
+
|
|
7
|
+
/** Shared heartbeat protocol for local file-backed stores, not a database lock.
|
|
8
|
+
* Never reclaim legacy regular-file locks: old hosts do not speak this protocol.
|
|
9
|
+
* proper-lockfile fails closed on compromised ownership (its default handler).
|
|
10
|
+
*/
|
|
11
|
+
export async function acquireStateFileLock(
|
|
12
|
+
path: string,
|
|
13
|
+
options: { signal?: AbortSignal; waitMs?: number; label?: string } = {},
|
|
14
|
+
): Promise<() => Promise<void>> {
|
|
15
|
+
const { signal, waitMs = 15_000, label = 'STATE' } = options;
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
const target = resolve(path);
|
|
18
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
19
|
+
const deadline = Date.now() + waitMs;
|
|
20
|
+
while (true) {
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
const legacy = await stat(`${target}.lock`).catch((error) => {
|
|
23
|
+
if (error.code === 'ENOENT') return null;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
if (legacy?.isFile()) {
|
|
27
|
+
throw new Error(`${label}_LEGACY_LOCK:请停止旧版本宿主并检查遗留锁:${target}.lock`);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
let owned = false;
|
|
31
|
+
let identity: fs.Stats | undefined;
|
|
32
|
+
const guardedFs = {
|
|
33
|
+
...fs,
|
|
34
|
+
rmdir(lockPath: fs.PathLike, callback: fs.NoParamCallback) {
|
|
35
|
+
if (!owned) return fs.rmdir(lockPath, callback);
|
|
36
|
+
// Also guard immediate release, before the next heartbeat could detect
|
|
37
|
+
// replacement. Never recursively remove a lock directory.
|
|
38
|
+
fs.stat(lockPath, (error, current) => {
|
|
39
|
+
if (error || !identity || current.dev !== identity.dev ||
|
|
40
|
+
current.ino !== identity.ino || current.birthtimeMs !== identity.birthtimeMs) {
|
|
41
|
+
callback(Object.assign(new Error(`锁所有权已丢失:${target}`), { code: 'ECOMPROMISED' }));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
fs.rmdir(lockPath, callback);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const release = await lockfile.lock(target, {
|
|
49
|
+
fs: guardedFs,
|
|
50
|
+
realpath: false,
|
|
51
|
+
retries: 0,
|
|
52
|
+
stale: 120_000,
|
|
53
|
+
update: 10_000,
|
|
54
|
+
});
|
|
55
|
+
owned = true;
|
|
56
|
+
try {
|
|
57
|
+
identity = await stat(`${target}.lock`);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await release().catch(() => undefined);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
await release();
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
}
|
|
66
|
+
return release;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if ((error as NodeJS.ErrnoException).code !== 'ELOCKED') throw error;
|
|
69
|
+
if (Date.now() >= deadline) throw new Error(`${label}_LOCK_TIMEOUT:等待文件锁超时:${target}`);
|
|
70
|
+
await delay(40, undefined, { signal });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { visualPresentation } from '../prompts/visualPresentation.js';
|
|
2
|
+
/** Ephemeral publication request; never append this private candidate to history. */
|
|
3
|
+
export function finalAnswerMessages(messages, candidate, surface = 'headless') {
|
|
4
|
+
return [...messages,
|
|
5
|
+
{ role: 'assistant', content: candidate },
|
|
6
|
+
{ role: 'user', content: '内部准备、工具与复核阶段已结束。上一条 assistant 消息是最终回答的依据,不是需要逐字重播的文本。现在直接面向用户生成一次最终回答:可以组织措辞与排版,但必须保留已确认的事实、数字、限定条件、风险、证据和真实成果链接;不得新增未经验证的结论、伪称核验通过或编造产物。若仍有证据缺口,如实说明,不调用工具或顾问,不承诺后台继续工作。直接回答,不输出内部指令或准备过程。' },
|
|
7
|
+
{ role: 'user', content: visualPresentation(surface) },
|
|
8
|
+
];
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Message } from '../types.js';
|
|
2
|
+
import type { AgentSurface } from './agentRuntimeContext.js';
|
|
3
|
+
import type { OutputContract } from './outputContract.js';
|
|
4
|
+
import { visualPresentation } from '../prompts/visualPresentation.js';
|
|
5
|
+
|
|
6
|
+
/** Ephemeral publication request; never append this private candidate to history. */
|
|
7
|
+
export function finalAnswerMessages(messages: Message[], candidate: string, surface: AgentSurface | OutputContract = 'headless'): Message[] {
|
|
8
|
+
return [...messages,
|
|
9
|
+
{ role: 'assistant', content: candidate },
|
|
10
|
+
{ role: 'user', content: '内部准备、工具与复核阶段已结束。上一条 assistant 消息是最终回答的依据,不是需要逐字重播的文本。现在直接面向用户生成一次最终回答:可以组织措辞与排版,但必须保留已确认的事实、数字、限定条件、风险、证据和真实成果链接;不得新增未经验证的结论、伪称核验通过或编造产物。若仍有证据缺口,如实说明,不调用工具或顾问,不承诺后台继续工作。直接回答,不输出内部指令或准备过程。' },
|
|
11
|
+
{ role: 'user', content: visualPresentation(surface) },
|
|
12
|
+
];
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** Opt-in diagnostics: no prompts, paths, session IDs, tool arguments or model output. */
|
|
3
|
+
export function createLifecycleTrace(scope, enabled = process.env.MINIMAL_AGENT_TRACE_LIFECYCLE === '1', write = line => { process.stderr.write(line); }) {
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return () => { };
|
|
6
|
+
const traceId = randomUUID();
|
|
7
|
+
const start = performance.now();
|
|
8
|
+
let sequence = 0;
|
|
9
|
+
return phase => {
|
|
10
|
+
try {
|
|
11
|
+
write(`${JSON.stringify({ kind: 'agent_lifecycle', scope, traceId, sequence: ++sequence,
|
|
12
|
+
phase, timestamp: Date.now(), elapsedMs: Math.round(performance.now() - start) })}\n`);
|
|
13
|
+
}
|
|
14
|
+
catch { /* Diagnostics must never interrupt execution. */ }
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
type Phase = 'model_start' | 'model_end' | 'model_error' | 'progress_published' | 'final_published'
|
|
4
|
+
| 'save_start' | 'save_end' | 'save_error' | 'turn_done' | 'memory_start' | 'memory_end';
|
|
5
|
+
|
|
6
|
+
/** Opt-in diagnostics: no prompts, paths, session IDs, tool arguments or model output. */
|
|
7
|
+
export function createLifecycleTrace(
|
|
8
|
+
scope: 'loop' | 'host',
|
|
9
|
+
enabled = process.env.MINIMAL_AGENT_TRACE_LIFECYCLE === '1',
|
|
10
|
+
write: (line: string) => void = line => { process.stderr.write(line); },
|
|
11
|
+
): (phase: Phase) => void {
|
|
12
|
+
if (!enabled) return () => {};
|
|
13
|
+
const traceId = randomUUID();
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
let sequence = 0;
|
|
16
|
+
return phase => {
|
|
17
|
+
try {
|
|
18
|
+
write(`${JSON.stringify({ kind: 'agent_lifecycle', scope, traceId, sequence: ++sequence,
|
|
19
|
+
phase, timestamp: Date.now(), elapsedMs: Math.round(performance.now() - start) })}\n`);
|
|
20
|
+
} catch { /* Diagnostics must never interrupt execution. */ }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const WEB_OUTPUT = Object.freeze({
|
|
2
|
+
format: 'rich-markdown', diagrams: Object.freeze(['mermaid', 'svg']),
|
|
3
|
+
images: true, attachments: true, finalDelivery: 'stream',
|
|
4
|
+
});
|
|
5
|
+
/** Compatibility defaults only. Hosts should declare their actual renderer. */
|
|
6
|
+
export function resolveOutputContract(surface, output) {
|
|
7
|
+
const value = output ?? (surface === 'desktop' ? WEB_OUTPUT : {
|
|
8
|
+
format: surface === 'tui' || surface === 'cli' ? 'terminal' : 'unknown',
|
|
9
|
+
diagrams: [], images: false, attachments: false,
|
|
10
|
+
finalDelivery: surface === 'tui' ? 'stream' : 'atomic',
|
|
11
|
+
});
|
|
12
|
+
return Object.freeze({ ...value, diagrams: Object.freeze(value.format === 'rich-markdown' ? [...new Set(value.diagrams)] : []) });
|
|
13
|
+
}
|
|
14
|
+
/** Publication changes tool access, not the renderer or local execution rights. */
|
|
15
|
+
export function toolsForResponsePhase(tools, phase) {
|
|
16
|
+
return phase === 'publication' ? [] : [...tools];
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentSurface } from './agentRuntimeContext.js';
|
|
2
|
+
|
|
3
|
+
/** Trusted host presentation declaration, never an execution permission. */
|
|
4
|
+
export interface OutputContract {
|
|
5
|
+
readonly format: 'rich-markdown' | 'terminal' | 'structured' | 'unknown';
|
|
6
|
+
readonly diagrams: readonly ('mermaid' | 'svg')[];
|
|
7
|
+
readonly images: boolean;
|
|
8
|
+
readonly attachments: boolean;
|
|
9
|
+
readonly finalDelivery: 'stream' | 'atomic';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const WEB_OUTPUT: OutputContract = Object.freeze({
|
|
13
|
+
format: 'rich-markdown', diagrams: Object.freeze(['mermaid', 'svg'] as const),
|
|
14
|
+
images: true, attachments: true, finalDelivery: 'stream',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Compatibility defaults only. Hosts should declare their actual renderer. */
|
|
18
|
+
export function resolveOutputContract(surface: AgentSurface, output?: OutputContract): OutputContract {
|
|
19
|
+
const value: OutputContract = output ?? (surface === 'desktop' ? WEB_OUTPUT : {
|
|
20
|
+
format: surface === 'tui' || surface === 'cli' ? 'terminal' : 'unknown',
|
|
21
|
+
diagrams: [], images: false, attachments: false,
|
|
22
|
+
finalDelivery: surface === 'tui' ? 'stream' : 'atomic',
|
|
23
|
+
});
|
|
24
|
+
return Object.freeze({ ...value, diagrams: Object.freeze(
|
|
25
|
+
value.format === 'rich-markdown' ? [...new Set(value.diagrams)] : [],
|
|
26
|
+
) });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ResponsePhase = 'preparation' | 'publication';
|
|
30
|
+
|
|
31
|
+
/** Publication changes tool access, not the renderer or local execution rights. */
|
|
32
|
+
export function toolsForResponsePhase<T>(tools: readonly T[], phase: ResponsePhase): T[] {
|
|
33
|
+
return phase === 'publication' ? [] : [...tools];
|
|
34
|
+
}
|
|
@@ -5,7 +5,8 @@ import { listSessionSummaries, updateSessionMetadata, } from '../context/session
|
|
|
5
5
|
import { workspaceIndexService } from '../context/workspaceIndex.js';
|
|
6
6
|
import { createPromptCacheContext } from '../llm/promptCache.js';
|
|
7
7
|
import { modelRefFromLegacy } from '../modelRef.js';
|
|
8
|
-
import {
|
|
8
|
+
import { browserPromptRuntime, buildTransientPromptBundle, } from '../prompts/system.js';
|
|
9
|
+
import { assembleStablePrompt } from '../prompts/promptAssembly.js';
|
|
9
10
|
import { createAgentRuntimeContext, filterToolsForRuntime, memoryContextFromRuntime, } from '../runtime/agentRuntimeContext.js';
|
|
10
11
|
import { filterToolsForCategories } from '../team/toolPolicy.js';
|
|
11
12
|
import { ALL_TOOLS } from '../tools/index.js';
|
|
@@ -119,6 +120,7 @@ deps = {}) {
|
|
|
119
120
|
// 稳定层进 history[0](runTurn 每轮重写它,必须逐字节可复现,见 runTurn.ts 的缓存不变量);
|
|
120
121
|
// 易变层每轮单独计算,仅附加到当前用户消息的模型副本。
|
|
121
122
|
const runtimeContext = createAgentRuntimeContext({
|
|
123
|
+
output: opts.output,
|
|
122
124
|
surface: 'headless',
|
|
123
125
|
profile: 'main',
|
|
124
126
|
cwd: effectiveCwd,
|
|
@@ -146,11 +148,14 @@ deps = {}) {
|
|
|
146
148
|
const turnTools = provider.source === 'gateway'
|
|
147
149
|
? filterToolsForCategories(runtimeTools, effectiveGatewayCategories)
|
|
148
150
|
: runtimeTools;
|
|
149
|
-
const systemPrompt = await
|
|
151
|
+
const systemPrompt = await assembleStablePrompt({ cwd: effectiveCwd, tools: turnTools, runtime: {
|
|
152
|
+
surface: runtimeContext.surface, output: runtimeContext.output,
|
|
153
|
+
} });
|
|
150
154
|
const transientBundle = await buildTransientPromptBundle({
|
|
151
155
|
cwd: effectiveCwd,
|
|
152
156
|
tools: turnTools,
|
|
153
157
|
runtime: {
|
|
158
|
+
output: runtimeContext.output,
|
|
154
159
|
surface: 'headless',
|
|
155
160
|
cwd: effectiveCwd,
|
|
156
161
|
workspaceId: workspace?.id,
|