minimal-agent 0.18.10 → 0.18.11
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/advisor/advisorCoordinator.js +30 -6
- package/src/advisor/advisorCoordinator.ts +23 -6
- package/src/advisor/deliveryReview.js +23 -0
- package/src/advisor/deliveryReview.ts +22 -0
- package/src/advisor/presentation.js +3 -1
- package/src/advisor/presentation.ts +4 -1
- package/src/loop/README.md +44 -0
- package/src/loop/deliveryIntent.js +22 -0
- package/src/loop/deliveryIntent.ts +39 -0
- package/src/loop/messages.js +146 -0
- package/src/loop/messages.ts +166 -0
- package/src/loop/options.js +1 -0
- package/src/loop/options.ts +155 -0
- package/src/loop/progressStream.js +40 -0
- package/src/loop/progressStream.ts +44 -0
- package/src/loop/publication.js +94 -0
- package/src/loop/publication.ts +124 -0
- package/src/loop/requestPreparation.js +180 -0
- package/src/loop/requestPreparation.ts +226 -0
- package/src/loop/toolBatch.js +99 -0
- package/src/loop/toolBatch.ts +123 -0
- package/src/loop/toolExecutor.js +304 -0
- package/src/loop/toolExecutor.ts +363 -0
- package/src/loop/toolPresentation.js +135 -0
- package/src/loop/toolPresentation.ts +134 -0
- package/src/loop/toolSemantics.js +45 -0
- package/src/loop/toolSemantics.ts +51 -0
- package/src/loop.js +368 -1113
- package/src/loop.ts +480 -1380
- package/src/tools/advisor/advisor.js +5 -2
- package/src/tools/advisor/advisor.ts +6 -2
- package/src/tools/bash/semantics.js +33 -1
- package/src/tools/bash/semantics.ts +27 -1
- package/src/types.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.11",
|
|
4
4
|
"description": "Bun-first 的本地 Agent 内核,支持工具、插件、工作流、上下文压缩、长期记忆与 OpenAI 兼容接口;Tauri 桌面宿主和 Node.js npm 发布可复用同一内核",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { needsTextDeliveryReview, reviewSignal, isCosmeticRequest } from './deliveryReview.js';
|
|
2
|
+
import { needsTextDeliveryReview, reviewSignal, isCosmeticRequest, isReviewProcessQuestion, isRoutineMaintenanceRequest, isRoutineMaintenancePath } from './deliveryReview.js';
|
|
3
3
|
import { expandAdvisorHistory } from '../context/advisorHistory.js';
|
|
4
4
|
import { provenanceLabel } from '../llm/messageProvenance.js';
|
|
5
5
|
/**
|
|
@@ -100,6 +100,7 @@ export class AdvisorCoordinator {
|
|
|
100
100
|
#evidenceVersion = 0;
|
|
101
101
|
#investigationEvidence = 0;
|
|
102
102
|
#mutations = 0;
|
|
103
|
+
#nonRoutineMutation = false;
|
|
103
104
|
#verifications = 0;
|
|
104
105
|
#deliveryProduced = false;
|
|
105
106
|
#deliveryDraft;
|
|
@@ -376,8 +377,17 @@ export class AdvisorCoordinator {
|
|
|
376
377
|
if (semantics.mutatesWorkspace) {
|
|
377
378
|
if (this.#required?.reviewReason)
|
|
378
379
|
return this.#defer(this.#required.checkpoint, this.#required.reason, this.#required.key);
|
|
379
|
-
if (isCosmeticRequest(this.#currentRequest))
|
|
380
|
+
if (isCosmeticRequest(this.#currentRequest) && !semantics.highRisk)
|
|
380
381
|
return { allow: true };
|
|
382
|
+
if (isRoutineMaintenanceRequest(this.#currentRequest) && !semantics.highRisk
|
|
383
|
+
&& ['Edit', 'MultiEdit', 'Write'].includes(call.function.name)) {
|
|
384
|
+
try {
|
|
385
|
+
const path = JSON.parse(call.function.arguments).file_path;
|
|
386
|
+
if (typeof path === 'string' && isRoutineMaintenancePath(path))
|
|
387
|
+
return { allow: true };
|
|
388
|
+
}
|
|
389
|
+
catch { /* Unknown scope retains review. */ }
|
|
390
|
+
}
|
|
381
391
|
return this.requestCheckpoint('pre-execution', '即将执行可能修改工作区的操作,先复核执行方案');
|
|
382
392
|
}
|
|
383
393
|
return { allow: true };
|
|
@@ -409,6 +419,11 @@ export class AdvisorCoordinator {
|
|
|
409
419
|
this.#reminders.push('检测到连续同类失败或执行没有进展。继续重试前请优先调用 Advisor 获取第二意见;这是提醒,不阻止只读诊断或切换路径。');
|
|
410
420
|
}
|
|
411
421
|
shouldCheckDelivery() {
|
|
422
|
+
// Only exempt a process explanation when no real work or delivery occurred.
|
|
423
|
+
if (isReviewProcessQuestion(this.#currentRequest) && this.#mutations === 0 && !this.#deliveryProduced)
|
|
424
|
+
return false;
|
|
425
|
+
if (isRoutineMaintenanceRequest(this.#currentRequest) && !this.#nonRoutineMutation && !this.#deliveryProduced)
|
|
426
|
+
return false;
|
|
412
427
|
return ((this.#mutations > 0 && !isCosmeticRequest(this.#currentRequest)) ||
|
|
413
428
|
this.#verifications > 0 ||
|
|
414
429
|
this.#deliveryProduced ||
|
|
@@ -432,7 +447,8 @@ export class AdvisorCoordinator {
|
|
|
432
447
|
this.#deliveryDraft = draft;
|
|
433
448
|
if (!this.#advisorAvailable)
|
|
434
449
|
return { allow: true };
|
|
435
|
-
this.#
|
|
450
|
+
if (!isReviewProcessQuestion(this.#currentRequest) || this.#mutations > 0 || this.#deliveryProduced)
|
|
451
|
+
this.#requireSignal(draft ?? '', true);
|
|
436
452
|
if (this.#required?.reviewReason)
|
|
437
453
|
return this.#defer(this.#required.checkpoint, this.#required.reason, this.#required.key);
|
|
438
454
|
// A later successful mutation changes the reviewed artifact. Ordinary reads
|
|
@@ -458,6 +474,7 @@ export class AdvisorCoordinator {
|
|
|
458
474
|
}
|
|
459
475
|
beginConsultation(reason) {
|
|
460
476
|
this.#commitTask();
|
|
477
|
+
const trigger = this.#required ?? this.#softCheckpoint;
|
|
461
478
|
// Explicit phase reviews settle that phase, even when initiated before a host reminder.
|
|
462
479
|
const explicit = reason === 'plan_review' ? 'pre-execution' : reason === 'delivery_review' ? 'pre-delivery' : undefined;
|
|
463
480
|
const active = explicit
|
|
@@ -471,6 +488,8 @@ export class AdvisorCoordinator {
|
|
|
471
488
|
this.#softCheckpoint = { checkpoint: active.checkpoint, key: active.key };
|
|
472
489
|
return {
|
|
473
490
|
kind: 'advisor_consultation',
|
|
491
|
+
triggerSource: trigger ? 'checkpoint' : 'model',
|
|
492
|
+
triggerReason: this.#required?.reason ?? (trigger ? '运行时复核提醒' : `模型主动咨询:${reason ?? '未指定原因'}`),
|
|
474
493
|
consultationId: crypto.randomUUID(),
|
|
475
494
|
checkpoint: active.checkpoint,
|
|
476
495
|
checkpointKey: active.key,
|
|
@@ -577,10 +596,15 @@ export class AdvisorCoordinator {
|
|
|
577
596
|
this.#verifications++;
|
|
578
597
|
if (!result.ok)
|
|
579
598
|
return;
|
|
580
|
-
if (metadata?.kind === 'file_activity')
|
|
581
|
-
|
|
582
|
-
|
|
599
|
+
if (metadata?.kind === 'file_activity') {
|
|
600
|
+
const changes = metadata.files.filter(file => ['created', 'modified', 'generated', 'deleted'].includes(file.activity));
|
|
601
|
+
this.#mutations += changes.length;
|
|
602
|
+
this.#nonRoutineMutation ||= changes.some(file => file.activity === 'deleted' || !isRoutineMaintenancePath(file.relativePath));
|
|
603
|
+
}
|
|
604
|
+
else if (semantics?.mutatesWorkspace) {
|
|
583
605
|
this.#mutations++;
|
|
606
|
+
this.#nonRoutineMutation = true;
|
|
607
|
+
}
|
|
584
608
|
if (metadata?.kind === 'result_manifest')
|
|
585
609
|
this.#deliveryProduced = true;
|
|
586
610
|
if (INVESTIGATION_TOOLS.has(name))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { needsTextDeliveryReview, reviewSignal, isCosmeticRequest } from './deliveryReview.js';
|
|
2
|
+
import { needsTextDeliveryReview, reviewSignal, isCosmeticRequest, isReviewProcessQuestion, isRoutineMaintenanceRequest, isRoutineMaintenancePath } from './deliveryReview.js';
|
|
3
3
|
import { expandAdvisorHistory } from '../context/advisorHistory.js';
|
|
4
4
|
import { provenanceLabel } from '../llm/messageProvenance.js';
|
|
5
5
|
|
|
@@ -145,6 +145,7 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
145
145
|
#evidenceVersion = 0;
|
|
146
146
|
#investigationEvidence = 0;
|
|
147
147
|
#mutations = 0;
|
|
148
|
+
#nonRoutineMutation = false;
|
|
148
149
|
#verifications = 0;
|
|
149
150
|
#deliveryProduced = false;
|
|
150
151
|
#deliveryDraft?: string;
|
|
@@ -437,7 +438,14 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
437
438
|
}
|
|
438
439
|
if (semantics.mutatesWorkspace) {
|
|
439
440
|
if (this.#required?.reviewReason) return this.#defer(this.#required.checkpoint, this.#required.reason!, this.#required.key);
|
|
440
|
-
if (isCosmeticRequest(this.#currentRequest)) return { allow: true };
|
|
441
|
+
if (isCosmeticRequest(this.#currentRequest) && !semantics.highRisk) return { allow: true };
|
|
442
|
+
if (isRoutineMaintenanceRequest(this.#currentRequest) && !semantics.highRisk
|
|
443
|
+
&& ['Edit', 'MultiEdit', 'Write'].includes(call.function.name)) {
|
|
444
|
+
try {
|
|
445
|
+
const path = JSON.parse(call.function.arguments).file_path;
|
|
446
|
+
if (typeof path === 'string' && isRoutineMaintenancePath(path)) return { allow: true };
|
|
447
|
+
} catch { /* Unknown scope retains review. */ }
|
|
448
|
+
}
|
|
441
449
|
return this.requestCheckpoint(
|
|
442
450
|
'pre-execution',
|
|
443
451
|
'即将执行可能修改工作区的操作,先复核执行方案',
|
|
@@ -477,6 +485,9 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
477
485
|
}
|
|
478
486
|
|
|
479
487
|
shouldCheckDelivery(): boolean {
|
|
488
|
+
// Only exempt a process explanation when no real work or delivery occurred.
|
|
489
|
+
if (isReviewProcessQuestion(this.#currentRequest) && this.#mutations === 0 && !this.#deliveryProduced) return false;
|
|
490
|
+
if (isRoutineMaintenanceRequest(this.#currentRequest) && !this.#nonRoutineMutation && !this.#deliveryProduced) return false;
|
|
480
491
|
return (
|
|
481
492
|
(this.#mutations > 0 && !isCosmeticRequest(this.#currentRequest)) ||
|
|
482
493
|
this.#verifications > 0 ||
|
|
@@ -500,7 +511,8 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
500
511
|
beforeDelivery(draft?: string, force = false): CheckpointDecision {
|
|
501
512
|
if (draft !== undefined) this.#deliveryDraft = draft;
|
|
502
513
|
if (!this.#advisorAvailable) return { allow: true };
|
|
503
|
-
this.#
|
|
514
|
+
if (!isReviewProcessQuestion(this.#currentRequest) || this.#mutations > 0 || this.#deliveryProduced)
|
|
515
|
+
this.#requireSignal(draft ?? '', true);
|
|
504
516
|
if (this.#required?.reviewReason) return this.#defer(this.#required.checkpoint, this.#required.reason!, this.#required.key);
|
|
505
517
|
// A later successful mutation changes the reviewed artifact. Ordinary reads
|
|
506
518
|
// and wording changes do not reopen the checkpoint.
|
|
@@ -529,6 +541,7 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
529
541
|
|
|
530
542
|
beginConsultation(reason?: AdvisorReason): AdvisorConsultationMetadata {
|
|
531
543
|
this.#commitTask();
|
|
544
|
+
const trigger = this.#required ?? this.#softCheckpoint;
|
|
532
545
|
// Explicit phase reviews settle that phase, even when initiated before a host reminder.
|
|
533
546
|
const explicit = reason === 'plan_review' ? 'pre-execution' : reason === 'delivery_review' ? 'pre-delivery' : undefined;
|
|
534
547
|
const active = explicit
|
|
@@ -542,6 +555,8 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
542
555
|
this.#softCheckpoint = { checkpoint: active.checkpoint, key: active.key };
|
|
543
556
|
return {
|
|
544
557
|
kind: 'advisor_consultation',
|
|
558
|
+
triggerSource: trigger ? 'checkpoint' : 'model',
|
|
559
|
+
triggerReason: this.#required?.reason ?? (trigger ? '运行时复核提醒' : `模型主动咨询:${reason ?? '未指定原因'}`),
|
|
545
560
|
consultationId: crypto.randomUUID(),
|
|
546
561
|
checkpoint: active.checkpoint,
|
|
547
562
|
checkpointKey: active.key,
|
|
@@ -665,9 +680,11 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
665
680
|
if (metadata?.kind === 'process_execution' && metadata.commandKind !== 'other'
|
|
666
681
|
&& ['completed', 'failed'].includes(metadata.status)) this.#verifications++;
|
|
667
682
|
if (!result.ok) return;
|
|
668
|
-
if (metadata?.kind === 'file_activity')
|
|
669
|
-
['created', 'modified', 'generated', 'deleted'].includes(file.activity))
|
|
670
|
-
|
|
683
|
+
if (metadata?.kind === 'file_activity') {
|
|
684
|
+
const changes = metadata.files.filter(file => ['created', 'modified', 'generated', 'deleted'].includes(file.activity));
|
|
685
|
+
this.#mutations += changes.length;
|
|
686
|
+
this.#nonRoutineMutation ||= changes.some(file => file.activity === 'deleted' || !isRoutineMaintenancePath(file.relativePath));
|
|
687
|
+
} else if (semantics?.mutatesWorkspace) { this.#mutations++; this.#nonRoutineMutation = true; }
|
|
671
688
|
if (metadata?.kind === 'result_manifest') this.#deliveryProduced = true;
|
|
672
689
|
if (INVESTIGATION_TOOLS.has(name)) this.#investigationEvidence++;
|
|
673
690
|
}
|
|
@@ -5,6 +5,8 @@ export function needsTextDeliveryReview(request, draft) {
|
|
|
5
5
|
const text = draft.trim();
|
|
6
6
|
if (!text)
|
|
7
7
|
return false;
|
|
8
|
+
if (isReviewProcessQuestion(request))
|
|
9
|
+
return false;
|
|
8
10
|
const conclusion = /建议|推荐|结论|成本|费用|计费|预算|风险|冲突|估算|recommend|conclusion|cost|pricing|estimate|conflict|risk/i;
|
|
9
11
|
const task = /分析|调研|评估|比较|对比|审查|复核|计算|设计|方案|analy[sz]|research|evaluat|compar|review|calculat|design/i;
|
|
10
12
|
// Numeric estimates and decisions can be short and still deserve review.
|
|
@@ -17,7 +19,28 @@ export function needsTextDeliveryReview(request, draft) {
|
|
|
17
19
|
return (text.length >= 40 && conclusion.test(text))
|
|
18
20
|
|| (task.test(request) && text.length >= 40 && !/[??]$/.test(text));
|
|
19
21
|
}
|
|
22
|
+
/** Explain the review process without recursively reviewing that explanation.
|
|
23
|
+
* A request to change code or review real deliverables is not a process question.
|
|
24
|
+
*/
|
|
25
|
+
export function isReviewProcessQuestion(request) {
|
|
26
|
+
if (/修复|重构|实现|开发|部署|修改|审查代码|评审代码|\b(?:implement|refactor|deploy|fix)\b/i.test(request))
|
|
27
|
+
return false;
|
|
28
|
+
return /顾问|咨询|advisor/i.test(request)
|
|
29
|
+
&& /几次|次数|频繁|频率|太多|重复咨询|咨询时机|触发|为什么.{0,10}(?:咨询|调用)|为何.{0,10}(?:咨询|调用)|\b(?:how many|too often|frequency)\b/i.test(request);
|
|
30
|
+
}
|
|
31
|
+
/** Narrow, explicit maintenance scope, not a line-count-based risk exemption. */
|
|
32
|
+
export function isRoutineMaintenanceRequest(request) {
|
|
33
|
+
if (/权限|认证|密钥|安全|支付|迁移|部署|协议|架构|删除|\b(?:auth|secret|security|payment|migration|deploy|delete)\b/i.test(request))
|
|
34
|
+
return false;
|
|
35
|
+
return isCosmeticRequest(request) ||
|
|
36
|
+
/^(?:请)?(?:只|仅)(?:修改|修正|改|调整|更新|整理).{0,40}(?:注释|README|\.gitignore|忽略规则)[。.!!]?$/i.test(request.trim());
|
|
37
|
+
}
|
|
38
|
+
export function isRoutineMaintenancePath(path) {
|
|
39
|
+
return /(?:^|[\\/])(?:README(?:\.(?:md|txt|rst))?|\.gitignore)$/i.test(path);
|
|
40
|
+
}
|
|
20
41
|
export function reviewSignal(text) {
|
|
42
|
+
if (isReviewProcessQuestion(text))
|
|
43
|
+
return;
|
|
21
44
|
// Topic discussion and explicit absence of conflict are not incident signals.
|
|
22
45
|
if (/^(?:什么是|解释一下|介绍一下|如何处理|what is|explain)\s*/i.test(text.trim()))
|
|
23
46
|
return;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export function needsTextDeliveryReview(request: string, draft: string): boolean {
|
|
5
5
|
const text = draft.trim();
|
|
6
6
|
if (!text) return false;
|
|
7
|
+
if (isReviewProcessQuestion(request)) return false;
|
|
7
8
|
const conclusion = /建议|推荐|结论|成本|费用|计费|预算|风险|冲突|估算|recommend|conclusion|cost|pricing|estimate|conflict|risk/i;
|
|
8
9
|
const task = /分析|调研|评估|比较|对比|审查|复核|计算|设计|方案|analy[sz]|research|evaluat|compar|review|calculat|design/i;
|
|
9
10
|
// Numeric estimates and decisions can be short and still deserve review.
|
|
@@ -15,7 +16,28 @@ export function needsTextDeliveryReview(request: string, draft: string): boolean
|
|
|
15
16
|
|| (task.test(request) && text.length >= 40 && !/[??]$/.test(text));
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** Explain the review process without recursively reviewing that explanation.
|
|
20
|
+
* A request to change code or review real deliverables is not a process question.
|
|
21
|
+
*/
|
|
22
|
+
export function isReviewProcessQuestion(request: string): boolean {
|
|
23
|
+
if (/修复|重构|实现|开发|部署|修改|审查代码|评审代码|\b(?:implement|refactor|deploy|fix)\b/i.test(request)) return false;
|
|
24
|
+
return /顾问|咨询|advisor/i.test(request)
|
|
25
|
+
&& /几次|次数|频繁|频率|太多|重复咨询|咨询时机|触发|为什么.{0,10}(?:咨询|调用)|为何.{0,10}(?:咨询|调用)|\b(?:how many|too often|frequency)\b/i.test(request);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Narrow, explicit maintenance scope, not a line-count-based risk exemption. */
|
|
29
|
+
export function isRoutineMaintenanceRequest(request: string): boolean {
|
|
30
|
+
if (/权限|认证|密钥|安全|支付|迁移|部署|协议|架构|删除|\b(?:auth|secret|security|payment|migration|deploy|delete)\b/i.test(request)) return false;
|
|
31
|
+
return isCosmeticRequest(request) ||
|
|
32
|
+
/^(?:请)?(?:只|仅)(?:修改|修正|改|调整|更新|整理).{0,40}(?:注释|README|\.gitignore|忽略规则)[。.!!]?$/i.test(request.trim());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isRoutineMaintenancePath(path: string): boolean {
|
|
36
|
+
return /(?:^|[\\/])(?:README(?:\.(?:md|txt|rst))?|\.gitignore)$/i.test(path);
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
export function reviewSignal(text: string): 'user_dissatisfaction' | 'evidence_conflict' | 'uncertainty' | undefined {
|
|
40
|
+
if (isReviewProcessQuestion(text)) return;
|
|
19
41
|
// Topic discussion and explicit absence of conflict are not incident signals.
|
|
20
42
|
if (/^(?:什么是|解释一下|介绍一下|如何处理|what is|explain)\s*/i.test(text.trim())) return;
|
|
21
43
|
text = text.replace(/(?:没有|不存在|未发现|并无).{0,8}(?:冲突|矛盾|不一致)/g, '');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Display only: never rewrite the model's tool result or persisted audit. */
|
|
2
2
|
export function advisorOpinionText(content) {
|
|
3
|
-
return content.replace(/^consultationId: [^\r\n]+\r?\n
|
|
3
|
+
return content.replace(/^consultationId: [^\r\n]+\r?\n(?:triggerSource: ([^\r\n]+)\r?\ntriggerReason: ([^\r\n]*)\r?\n)?调用 AdvisorDecision 时必须原样使用上面的完整 consultationId。\r?\n(?:\r?\n)?/, (_header, source, reason) => source
|
|
4
|
+
? `触发:${source === 'checkpoint' ? '运行时检查点' : source === 'model' ? '模型主动咨询' : '来源未记录'}${reason ? ` · ${reason}` : ''}\n\n`
|
|
5
|
+
: '');
|
|
4
6
|
}
|
|
5
7
|
export function advisorDecisionLabel(decision) {
|
|
6
8
|
return { accept: '已采纳', partial: '部分采纳', reject: '未采纳' }[decision] ?? '已记录';
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Display only: never rewrite the model's tool result or persisted audit. */
|
|
2
2
|
export function advisorOpinionText(content: string): string {
|
|
3
|
-
return content.replace(/^consultationId: [^\r\n]+\r?\n
|
|
3
|
+
return content.replace(/^consultationId: [^\r\n]+\r?\n(?:triggerSource: ([^\r\n]+)\r?\ntriggerReason: ([^\r\n]*)\r?\n)?调用 AdvisorDecision 时必须原样使用上面的完整 consultationId。\r?\n(?:\r?\n)?/,
|
|
4
|
+
(_header, source?: string, reason?: string) => source
|
|
5
|
+
? `触发:${source === 'checkpoint' ? '运行时检查点' : source === 'model' ? '模型主动咨询' : '来源未记录'}${reason ? ` · ${reason}` : ''}\n\n`
|
|
6
|
+
: '');
|
|
4
7
|
}
|
|
5
8
|
|
|
6
9
|
export function advisorDecisionLabel(decision: string): string {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Agent loop boundaries
|
|
2
|
+
|
|
3
|
+
`../loop.ts` remains the public `runQuery` entry. The implementation uses explicit
|
|
4
|
+
phases and async generators, not a configurable middleware framework. Continuation
|
|
5
|
+
decisions stay visible in one place: retry, consult, execute, publish or interrupt.
|
|
6
|
+
|
|
7
|
+
| Module | Responsibility |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `options.ts` | Existing host injection contract; type-only dependencies |
|
|
10
|
+
| `requestPreparation.ts` | Automatic compaction, budget checks, request-local tool fitting |
|
|
11
|
+
| `toolExecutor.ts` | Single-call authorization, rules, leases, execution and delivery receipt |
|
|
12
|
+
| `toolBatch.ts` | Safe concurrency segments, question/advisor barriers and ordered commits |
|
|
13
|
+
| `progressStream.ts` | Callback-to-generator progress bridge and worker settlement |
|
|
14
|
+
| `publication.ts` | Final expression stream and fallback before first visible text |
|
|
15
|
+
| `messages.ts` | Model message projection, interrupts, screenshot lifecycle, tool deltas |
|
|
16
|
+
| `toolPresentation.ts` | Human-readable tool descriptions; no execution policy |
|
|
17
|
+
| `toolSemantics.ts` | Advisor side-effect classification |
|
|
18
|
+
| `deliveryIntent.ts` | Existing formal-delivery intent policy |
|
|
19
|
+
|
|
20
|
+
## Ownership and invariants
|
|
21
|
+
|
|
22
|
+
- The orchestrator owns history, request continuation and turn accounting. Modules
|
|
23
|
+
receive narrow inputs or explicit callbacks, not a shared universal mutable bag.
|
|
24
|
+
- History replacement goes through the orchestrator so usage estimates and cache
|
|
25
|
+
edit state are reset consistently. Accumulated turn usage is not reset.
|
|
26
|
+
- The effective tool set is both the model catalog and execution boundary. Emergency
|
|
27
|
+
fitting is request-local; it cannot expand host permissions.
|
|
28
|
+
- Tool progress is live, but results enter history in model tool-call order.
|
|
29
|
+
Writes and interactive/advisor barriers do not become parallel work.
|
|
30
|
+
- Host execution hooks start only after preflight checks. UI `tool_start` is a
|
|
31
|
+
separate event: even a denied/deferred request receives a visible result.
|
|
32
|
+
- Final candidate text remains private until delivery checks finish. Publication
|
|
33
|
+
does not execute tools or replace a prefix already shown to the user.
|
|
34
|
+
- Cancellation belongs to the host AbortSignal. Progress stream disposal waits for
|
|
35
|
+
active workers to settle; it does not grant permission to leave tools orphaned.
|
|
36
|
+
- Existing exports from `loop.ts` remain compatible for TUI, Desktop, subagents and
|
|
37
|
+
workflow hosts. The new directory is internal, not a second runtime or protocol.
|
|
38
|
+
|
|
39
|
+
## Verification
|
|
40
|
+
|
|
41
|
+
Run `bun run typecheck`, `bun test test/loop*.test.ts test/finalAnswer.stream.test.ts`
|
|
42
|
+
and the full Agent suite. Verify Desktop against a refreshed local Agent dependency,
|
|
43
|
+
then run its typecheck, tests and web build. npm build must also include the new
|
|
44
|
+
modules; no migration or server/database dependency is introduced.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const DELIVERY_ARTIFACT = '(?:文件|合同|协议|方案|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
|
|
2
|
+
const DELIVERY_ACTION = '(?:发送|(?<![开研])发|交付|提交|(?<!宣)传(?!统)|制作|生成|创建|导出|输出|修改|更新|完成|写|起草|做|设计|整理)';
|
|
3
|
+
const ENGLISH_DELIVERY_ARTIFACT = '(?:file|contract|document|report|spreadsheet|workbook|slides?|presentation|pdf|image|code|script|archive|zip|html|webpage|website)';
|
|
4
|
+
const ENGLISH_DELIVERY_ACTION = '(?:send|deliver|submit|provide|create|generate|export|update|modify|finish|produce|draft|write|make|build)';
|
|
5
|
+
/** Whether this turn explicitly asks for a concrete artifact that should be formally delivered. */
|
|
6
|
+
export function expectsFormalDelivery(input) {
|
|
7
|
+
// Questions about producing artifacts are not instructions to produce them.
|
|
8
|
+
// Evaluate clauses independently so “解释原因,然后生成报告” still delivers.
|
|
9
|
+
const clauses = input.split(/[。!?!?;;\n]|然后|随后|并且/).filter(Boolean);
|
|
10
|
+
return clauses.some(expectsDeliveryClause);
|
|
11
|
+
}
|
|
12
|
+
function expectsDeliveryClause(input) {
|
|
13
|
+
if (/为什么|为何|如何|怎么|怎样|是否|能否|能不能|不要|不必|无需|只(?:需|要)?(?:解释|分析|评估|讨论)|\b(?:why|how|whether|explain|analy[sz]e|discuss|do not|don't)\b/i.test(input))
|
|
14
|
+
return false;
|
|
15
|
+
const compact = input.replace(/\s+/g, '');
|
|
16
|
+
const chinese = new RegExp(`${DELIVERY_ACTION}(?:给)?我?.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}${DELIVERY_ACTION}(?:给)?我?|给我.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}给我`, 'i');
|
|
17
|
+
const english = new RegExp(`\\b${ENGLISH_DELIVERY_ACTION}\\b.{0,80}\\b${ENGLISH_DELIVERY_ARTIFACT}\\b|\\b${ENGLISH_DELIVERY_ARTIFACT}\\b.{0,80}\\b${ENGLISH_DELIVERY_ACTION}\\b`, 'i');
|
|
18
|
+
return chinese.test(compact) || english.test(input);
|
|
19
|
+
}
|
|
20
|
+
export const DELIVERY_CORRECTION_REMINDER = `The user requested a concrete final deliverable, but no successful PresentResult call has been made.
|
|
21
|
+
Do not repeat the answer or mention a path only. Call PresentResult now with the actual final files and/or final http(s) links.
|
|
22
|
+
Submit only final outputs, never temporary scripts, unpacked assets, caches, or reference sources. If no valid result exists, explain that honestly without inventing one.`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const DELIVERY_ARTIFACT =
|
|
2
|
+
'(?:文件|合同|协议|方案|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
|
|
3
|
+
const DELIVERY_ACTION =
|
|
4
|
+
'(?:发送|(?<![开研])发|交付|提交|(?<!宣)传(?!统)|制作|生成|创建|导出|输出|修改|更新|完成|写|起草|做|设计|整理)';
|
|
5
|
+
const ENGLISH_DELIVERY_ARTIFACT =
|
|
6
|
+
'(?:file|contract|document|report|spreadsheet|workbook|slides?|presentation|pdf|image|code|script|archive|zip|html|webpage|website)';
|
|
7
|
+
const ENGLISH_DELIVERY_ACTION =
|
|
8
|
+
'(?:send|deliver|submit|provide|create|generate|export|update|modify|finish|produce|draft|write|make|build)';
|
|
9
|
+
|
|
10
|
+
/** Whether this turn explicitly asks for a concrete artifact that should be formally delivered. */
|
|
11
|
+
export function expectsFormalDelivery(input: string): boolean {
|
|
12
|
+
// Questions about producing artifacts are not instructions to produce them.
|
|
13
|
+
// Evaluate clauses independently so “解释原因,然后生成报告” still delivers.
|
|
14
|
+
const clauses = input.split(/[。!?!?;;\n]|然后|随后|并且/).filter(Boolean);
|
|
15
|
+
return clauses.some(expectsDeliveryClause);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function expectsDeliveryClause(input: string): boolean {
|
|
19
|
+
if (
|
|
20
|
+
/为什么|为何|如何|怎么|怎样|是否|能否|能不能|不要|不必|无需|只(?:需|要)?(?:解释|分析|评估|讨论)|\b(?:why|how|whether|explain|analy[sz]e|discuss|do not|don't)\b/i.test(
|
|
21
|
+
input,
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
return false;
|
|
25
|
+
const compact = input.replace(/\s+/g, '');
|
|
26
|
+
const chinese = new RegExp(
|
|
27
|
+
`${DELIVERY_ACTION}(?:给)?我?.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}${DELIVERY_ACTION}(?:给)?我?|给我.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}给我`,
|
|
28
|
+
'i',
|
|
29
|
+
);
|
|
30
|
+
const english = new RegExp(
|
|
31
|
+
`\\b${ENGLISH_DELIVERY_ACTION}\\b.{0,80}\\b${ENGLISH_DELIVERY_ARTIFACT}\\b|\\b${ENGLISH_DELIVERY_ARTIFACT}\\b.{0,80}\\b${ENGLISH_DELIVERY_ACTION}\\b`,
|
|
32
|
+
'i',
|
|
33
|
+
);
|
|
34
|
+
return chinese.test(compact) || english.test(input);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DELIVERY_CORRECTION_REMINDER = `The user requested a concrete final deliverable, but no successful PresentResult call has been made.
|
|
38
|
+
Do not repeat the answer or mention a path only. Call PresentResult now with the actual final files and/or final http(s) links.
|
|
39
|
+
Submit only final outputs, never temporary scripts, unpacked assets, caches, or reference sources. If no valid result exists, explain that honestly without inventing one.`;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { buildUserContent } from '../multimodal.js';
|
|
3
|
+
// ---------------- 辅助函数 ----------------
|
|
4
|
+
/**
|
|
5
|
+
* 易变上下文只存在于 provider 请求副本中,不会进入会话历史。
|
|
6
|
+
*/
|
|
7
|
+
export const SYSTEM_REMINDER_OPEN = '<system-reminder>';
|
|
8
|
+
export const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
|
|
9
|
+
/**
|
|
10
|
+
* Attach transient context to the current turn's user message in a model-only
|
|
11
|
+
* clone. Later tool-loop requests reuse the same decorated message, making each
|
|
12
|
+
* serialized request a strict prefix extension of the previous one.
|
|
13
|
+
*/
|
|
14
|
+
export function buildModelMessages(history, transientContext, currentUserMessageId) {
|
|
15
|
+
history = history.filter((message) => !(message.role === 'system' && message._ephemeral));
|
|
16
|
+
const transient = transientContext?.trim();
|
|
17
|
+
if (!transient)
|
|
18
|
+
return history;
|
|
19
|
+
let index = currentUserMessageId
|
|
20
|
+
? history.findIndex((message) => message.role === 'user' && message.id === currentUserMessageId)
|
|
21
|
+
: -1;
|
|
22
|
+
if (index < 0) {
|
|
23
|
+
for (let cursor = history.length - 1; cursor >= 0; cursor--) {
|
|
24
|
+
if (history[cursor]?.role === 'user') {
|
|
25
|
+
index = cursor;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (index < 0)
|
|
31
|
+
return history;
|
|
32
|
+
const target = history[index];
|
|
33
|
+
if (target.role !== 'user')
|
|
34
|
+
return history;
|
|
35
|
+
const reminder = `${SYSTEM_REMINDER_OPEN}\n${transient}\n${SYSTEM_REMINDER_CLOSE}`;
|
|
36
|
+
const content = typeof target.content === 'string'
|
|
37
|
+
? `${target.content}\n\n${reminder}`
|
|
38
|
+
: [...target.content, { type: 'text', text: reminder }];
|
|
39
|
+
const next = [...history];
|
|
40
|
+
next[index] = { ...target, content };
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
// 关键节点图像核验的合成消息标记:既是给模型的提示前缀,也是 evictPriorVisionShots 识别旧图消息的锚。
|
|
44
|
+
export const VISION_SHOT_MARKER = '【实况图像】';
|
|
45
|
+
/**
|
|
46
|
+
* 历史标记。这套机制最初只有 WebBrowser 用得上,文案写死了「浏览器」;
|
|
47
|
+
* 现在 Bash 的 ATTACH_IMAGE sentinel 也能产图(屏幕截图 / 生成的产物),文案必须中性。
|
|
48
|
+
* 但**持久化会话里仍存着旧前缀的消息**,淘汰逻辑必须继续认它 ——
|
|
49
|
+
* 否则老会话里的旧截图永远不会被降级,图片一路累积撑爆上下文。
|
|
50
|
+
*/
|
|
51
|
+
const LEGACY_VISION_SHOT_MARKERS = ['【浏览器实况截图】'];
|
|
52
|
+
const VISION_SHOT_PROMPT = `${VISION_SHOT_MARKER}上面是刚才工具执行后附带的真实图像(浏览器页面 / 屏幕截图 / 生成的产物)。请仔细看图核对:是否真的到达/呈现了你预期的状态?若与预期不符(空白/报错/弹窗遮挡/验证拦截/内容不对),不要假设成功,如实说明并调整下一步。`;
|
|
53
|
+
/** 一条 user 消息的文本是否是「实况图像」合成消息(含历史前缀)。 */
|
|
54
|
+
function isVisionShotText(text) {
|
|
55
|
+
return text.startsWith(VISION_SHOT_MARKER) || LEGACY_VISION_SHOT_MARKERS.some((marker) => text.startsWith(marker));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 把本轮工具附带的实况截图(ToolResult.visionImages)组装成一条带图 user 消息喂给模型自我核验。
|
|
59
|
+
* - 仅在模型明确支持识图时注入(provider.supportsVision === true);否则丢弃(不假装核验、也不给非视觉端点发图触发 400)。
|
|
60
|
+
* - 注入前先把历史里旧的截图消息降级为纯文字占位(evictPriorVisionShots),保证上下文里最多一张实况图,
|
|
61
|
+
* 避免图片累积撑爆上下文(呼应浏览器 ADR「高频帧只存内存、只持久化最终结果」)。
|
|
62
|
+
* - **失败的工具结果同样回填**:出错现场那张图往往比成功时更有信息量(权限弹窗、
|
|
63
|
+
* 验证拦截、意外的前台窗口)。此前这里要求 `ok === true`,逼得想附图的工具只能
|
|
64
|
+
* 把失败伪装成成功——见 types.ts 里 ToolResult 那段注释。
|
|
65
|
+
*/
|
|
66
|
+
export async function injectVisionFollowups(history, settled, provider) {
|
|
67
|
+
if (provider.supportsVision === false)
|
|
68
|
+
return null;
|
|
69
|
+
const images = settled.flatMap((s) => s.result.visionImages ?? []);
|
|
70
|
+
if (images.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
evictPriorVisionShots(history);
|
|
73
|
+
// 工具可以给自己这批图配一句说明(visionNote)。内核只负责把它拼在通用提示之后,
|
|
74
|
+
// 不解读内容——「这是一组 1Hz 连续帧」这类语义只有产图的工具知道。
|
|
75
|
+
const notes = settled
|
|
76
|
+
.flatMap((s) => (s.result.visionNote ? [s.result.visionNote] : []))
|
|
77
|
+
.filter((note, index, all) => all.indexOf(note) === index);
|
|
78
|
+
const text = notes.length > 0 ? `${VISION_SHOT_PROMPT}\n\n${notes.join('\n\n')}` : VISION_SHOT_PROMPT;
|
|
79
|
+
const content = await buildUserContent({ text, images });
|
|
80
|
+
const msg = {
|
|
81
|
+
role: 'user',
|
|
82
|
+
content,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
};
|
|
86
|
+
history.push(msg);
|
|
87
|
+
return msg;
|
|
88
|
+
}
|
|
89
|
+
/** 把历史里旧的实况图像 user 消息降级为纯文字占位、剔除其图片,释放上下文(只保留最近一张真图)。 */
|
|
90
|
+
export function evictPriorVisionShots(history) {
|
|
91
|
+
for (const msg of history) {
|
|
92
|
+
if (msg.role !== 'user' || !Array.isArray(msg.content))
|
|
93
|
+
continue;
|
|
94
|
+
const textPart = msg.content.find((p) => p.type === 'text');
|
|
95
|
+
if (textPart?.type === 'text' && isVisionShotText(textPart.text)) {
|
|
96
|
+
msg.content = `${VISION_SHOT_MARKER}(此前的实况图像已从上下文移除以节省空间。)`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 用户按ESC手动中断
|
|
102
|
+
*/
|
|
103
|
+
export function pushManualInterruptMarker(history) {
|
|
104
|
+
history.push({
|
|
105
|
+
role: 'user',
|
|
106
|
+
content: '(用户按下了 ESC/Ctrl+C 中断了任务。等待用户新指令。)',
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
id: crypto.randomUUID(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 有新的用户输入在队列中,当前安全停靠
|
|
113
|
+
*/
|
|
114
|
+
export function pushQueuedInputMarker(history) {
|
|
115
|
+
history.push({
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: [
|
|
118
|
+
'(系统提示:用户在工作过程中发送了新的消息。',
|
|
119
|
+
'你现在看到的是上一轮工作在被打断前已完成的部分。',
|
|
120
|
+
'请阅读用户的新消息,判断:',
|
|
121
|
+
'1. 如果是不相关的新任务:简要确认已收到,转向新任务,可在新任务完成后询问是否回到之前工作',
|
|
122
|
+
'2. 如果是补充信息或前置依赖:把新信息纳入考虑,判断是否需要调整已做的工作,然后继续',
|
|
123
|
+
'3. 如果是改变方向或取消:评估已完成的部分,说明你对变更的理解,按新方向推进)',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
id: crypto.randomUUID(),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** 把一条 tool_call_delta 合并到累积数组上 */
|
|
130
|
+
export function mergeToolCallDelta(acc, ev) {
|
|
131
|
+
let slot = acc[ev.index];
|
|
132
|
+
if (!slot) {
|
|
133
|
+
slot = {
|
|
134
|
+
id: '',
|
|
135
|
+
type: 'function',
|
|
136
|
+
function: { name: '', arguments: '' },
|
|
137
|
+
};
|
|
138
|
+
acc[ev.index] = slot;
|
|
139
|
+
}
|
|
140
|
+
if (ev.id)
|
|
141
|
+
slot.id = ev.id;
|
|
142
|
+
if (ev.name)
|
|
143
|
+
slot.function.name += ev.name;
|
|
144
|
+
if (ev.argumentsDelta)
|
|
145
|
+
slot.function.arguments += ev.argumentsDelta;
|
|
146
|
+
}
|