minimal-agent 0.18.5 → 0.18.7
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/contextBundle.js +23 -3
- package/src/context/contextBundle.ts +28 -7
- 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 +203 -30
- package/src/loop.ts +183 -30
- package/src/memory/experienceStore.js +1 -0
- package/src/memory/experienceStore.ts +1 -0
- package/src/memory/memoryService.js +3 -2
- package/src/memory/memoryService.ts +3 -0
- package/src/memory/projectKnowledge.js +1 -0
- package/src/memory/projectKnowledge.ts +1 -0
- package/src/memory/retrieval.js +4 -4
- package/src/memory/retrieval.ts +4 -4
- package/src/memory/types.ts +1 -0
- package/src/prompts/policyRegistry.js +31 -17
- package/src/prompts/policyRegistry.ts +31 -16
- package/src/prompts/promptAssembly.js +3 -9
- package/src/prompts/promptAssembly.ts +3 -9
- 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/finalAnswer.js +7 -0
- package/src/runtime/finalAnswer.ts +9 -0
- package/src/runtime/lifecycleTrace.js +16 -0
- package/src/runtime/lifecycleTrace.ts +22 -0
- package/src/session/runTurn.js +15 -1
- package/src/session/runTurn.ts +13 -1
- 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 +7 -1
- package/src/tools/agent/agent.ts +7 -1
- 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/types.ts +15 -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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import { needsTextDeliveryReview, reviewSignal, isCosmeticRequest } from './deliveryReview.js';
|
|
2
3
|
import { expandAdvisorHistory } from '../context/advisorHistory.js';
|
|
3
4
|
import { provenanceLabel } from '../llm/messageProvenance.js';
|
|
4
5
|
|
|
@@ -17,9 +18,6 @@ import type {
|
|
|
17
18
|
ToolResultMetadata,
|
|
18
19
|
} from '../types.js';
|
|
19
20
|
|
|
20
|
-
// A trigger can defer once. Changing command spelling never grants another deferral.
|
|
21
|
-
const MAX_CONSECUTIVE_DEFERRALS = 1;
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* contextPacket() 证据条目的数量与单条截断上限。
|
|
25
23
|
* 优先分配给近期、相关证据,同时保留早期依据。最终请求预算仍可裁剪本包。
|
|
@@ -52,6 +50,10 @@ function textContent(message: Message | undefined): string {
|
|
|
52
50
|
return '';
|
|
53
51
|
}
|
|
54
52
|
|
|
53
|
+
function manifestFingerprint(raw: string): string {
|
|
54
|
+
try { return JSON.stringify(JSON.parse(raw)); } catch { return raw; }
|
|
55
|
+
}
|
|
56
|
+
|
|
55
57
|
function toolNamesByCallId(history: readonly Message[]): Map<string, string> {
|
|
56
58
|
const names = new Map<string, string>();
|
|
57
59
|
for (const message of history) {
|
|
@@ -97,7 +99,7 @@ function checkpointKey(
|
|
|
97
99
|
signature?: string,
|
|
98
100
|
): string {
|
|
99
101
|
// pre-execution/pre-delivery 的意图是"本任务版本内只问一次"(见 beforeTool()
|
|
100
|
-
//
|
|
102
|
+
// 的执行方案复核)。旧实现把 workVersion/
|
|
101
103
|
// evidenceVersion 也编进 key,而这两个计数器在几乎每次成功的工具调用(包括
|
|
102
104
|
// 纯 Read/Grep,见 observeToolResult)后都会变化——"首次"因此从未真正生效,
|
|
103
105
|
// 任务里的每一次不同写入都会重新触发一轮完整 Advisor+AdvisorDecision,同时
|
|
@@ -134,10 +136,8 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
134
136
|
readonly #objective: string;
|
|
135
137
|
#advisorAvailable: boolean;
|
|
136
138
|
readonly #completed = new Set<string>();
|
|
137
|
-
readonly #deferredOnce = new Set<string>();
|
|
138
|
-
readonly #deferCounts = new Map<string, number>();
|
|
139
139
|
readonly #observedConsultations = new Set<string>();
|
|
140
|
-
readonly #
|
|
140
|
+
readonly #unavailableTransitions: AdvisorCheckpointMetadata[] = [];
|
|
141
141
|
readonly #softAlerts = new Set<string>();
|
|
142
142
|
#taskVersion: number;
|
|
143
143
|
#taskCommitted = false;
|
|
@@ -148,8 +148,11 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
148
148
|
#verifications = 0;
|
|
149
149
|
#deliveryProduced = false;
|
|
150
150
|
#deliveryDraft?: string;
|
|
151
|
-
#
|
|
152
|
-
#
|
|
151
|
+
#deliveryReviewedWork = -1;
|
|
152
|
+
#deliveryManifest?: string;
|
|
153
|
+
#reviewedManifest?: string;
|
|
154
|
+
#required?: { checkpoint: AdvisorCheckpoint; key: string; reason?: string; reviewReason?: AdvisorReason };
|
|
155
|
+
#softCheckpoint?: { checkpoint: AdvisorCheckpoint; key: string; reason?: string; reviewReason?: AdvisorReason };
|
|
153
156
|
#pending?: AdvisorConsultationMetadata;
|
|
154
157
|
#lastDecision?: AdvisorDecisionMetadata;
|
|
155
158
|
#reminders: string[] = [];
|
|
@@ -200,6 +203,10 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
200
203
|
|
|
201
204
|
for (let index = 0; index < input.history.length; index += 1) {
|
|
202
205
|
const message = input.history[index]!;
|
|
206
|
+
if (message.role === 'assistant') {
|
|
207
|
+
const submission = message.tool_calls?.find(call => call.function.name === 'PresentResult');
|
|
208
|
+
if (submission) this.#deliveryManifest = manifestFingerprint(submission.function.arguments);
|
|
209
|
+
}
|
|
203
210
|
// Text-only work has a delivery boundary too. Only answers preceding the
|
|
204
211
|
// current request start a new review cycle; replay inside a request does not.
|
|
205
212
|
if (message.role === 'assistant' && message.advisorTaskVersion !== undefined
|
|
@@ -235,6 +242,9 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
235
242
|
),
|
|
236
243
|
).length;
|
|
237
244
|
this.#workVersion += mutationCount;
|
|
245
|
+
} else if (message.advisorMutatesWorkspace && message.toolResultDisposition !== 'deferred'
|
|
246
|
+
&& !message.content.startsWith('Error: ')) {
|
|
247
|
+
this.#workVersion += 1;
|
|
238
248
|
}
|
|
239
249
|
const failed = message.content.startsWith('Error: ');
|
|
240
250
|
if (
|
|
@@ -289,9 +299,29 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
289
299
|
metadata: message.display_metadata,
|
|
290
300
|
}, { mutatesWorkspace: message.advisorMutatesWorkspace === true, highRisk: false });
|
|
291
301
|
}
|
|
302
|
+
if (this.#required && (!this.#required.key.startsWith(`${this.#taskVersion}:`)
|
|
303
|
+
|| this.#completed.has(this.#required.key))) this.#required = undefined;
|
|
304
|
+
this.#requireSignal(input.currentRequest);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#requireSignal(text: string, delivery = false): void {
|
|
308
|
+
const reason = reviewSignal(text);
|
|
309
|
+
if (!reason || !this.#advisorAvailable || this.#required) return;
|
|
310
|
+
const key = `${this.#taskVersion}:signal:${reason}:${this.#evidenceVersion}`;
|
|
311
|
+
if (this.#completed.has(key)) return;
|
|
312
|
+
this.#required = { checkpoint: delivery ? 'pre-delivery' : 'pre-execution', key, reviewReason: reason,
|
|
313
|
+
reason: `针对 ${reason} 复核用户反馈或证据;先核查已有材料,不把顾问意见当作事实。` };
|
|
314
|
+
this.#reminders.push(this.#required.reason!);
|
|
292
315
|
}
|
|
293
316
|
|
|
294
317
|
#applyReviewEvent(metadata: AdvisorCheckpointMetadata | AdvisorConsultationMetadata | AdvisorDecisionMetadata): void {
|
|
318
|
+
if (!(metadata.kind === 'advisor_checkpoint' && metadata.status === 'required')
|
|
319
|
+
&& this.#required?.key === metadata.checkpointKey) this.#required = undefined;
|
|
320
|
+
if (metadata.checkpoint === 'pre-delivery' && metadata.kind === 'advisor_consultation') {
|
|
321
|
+
this.#deliveryReviewedWork = metadata.workVersion;
|
|
322
|
+
this.#reviewedManifest = this.#deliveryManifest;
|
|
323
|
+
this.#completed.add(`${metadata.taskVersion}:pre-delivery`);
|
|
324
|
+
}
|
|
295
325
|
if (metadata.kind === 'advisor_consultation' && metadata.status === 'completed') {
|
|
296
326
|
if (this.#observedConsultations.has(metadata.consultationId)) return;
|
|
297
327
|
this.#observedConsultations.add(metadata.consultationId);
|
|
@@ -301,8 +331,8 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
301
331
|
return;
|
|
302
332
|
}
|
|
303
333
|
if (metadata.kind === 'advisor_checkpoint' && metadata.status === 'required') {
|
|
304
|
-
|
|
305
|
-
this.#
|
|
334
|
+
const signal = metadata.checkpointKey.match(/:signal:(user_dissatisfaction|evidence_conflict|uncertainty):/)?.[1] as AdvisorReason | undefined;
|
|
335
|
+
this.#required = { checkpoint: metadata.checkpoint, key: metadata.checkpointKey, reviewReason: signal, reason: '恢复尚未完成的顾问检查' };
|
|
306
336
|
return;
|
|
307
337
|
}
|
|
308
338
|
this.#completed.add(metadata.checkpointKey);
|
|
@@ -321,13 +351,11 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
321
351
|
: `顾问检查 ${metadata.checkpoint} 未完成,已降级继续;该次检查不构成验证证据。`);
|
|
322
352
|
}
|
|
323
353
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
checkpointKey: key, status: '
|
|
329
|
-
this.#applyReviewEvent(event);
|
|
330
|
-
this.#degradations.push(event);
|
|
354
|
+
// Retain the public method name for existing callers. Pending checks also need
|
|
355
|
+
// persistence when a turn ends at its limit before consultation can run.
|
|
356
|
+
drainDegradations(): AdvisorCheckpointMetadata[] {
|
|
357
|
+
return [...this.#unavailableTransitions.splice(0), ...(this.#required ? [{ kind: 'advisor_checkpoint' as const, checkpoint: this.#required.checkpoint,
|
|
358
|
+
checkpointKey: this.#required.key, status: 'required' as const, ...this.versions }] : [])];
|
|
331
359
|
}
|
|
332
360
|
|
|
333
361
|
get versions(): AdvisorVersions {
|
|
@@ -343,6 +371,14 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
343
371
|
}
|
|
344
372
|
|
|
345
373
|
setAvailable(available: boolean): void {
|
|
374
|
+
const pending = this.#required ?? this.#softCheckpoint;
|
|
375
|
+
if (!available && this.#advisorAvailable && pending && !this.#completed.has(pending.key)) {
|
|
376
|
+
const event: AdvisorCheckpointMetadata = { kind: 'advisor_checkpoint', checkpoint: pending.checkpoint,
|
|
377
|
+
checkpointKey: pending.key, status: 'attempted-unavailable', ...this.versions };
|
|
378
|
+
this.#applyReviewEvent(event);
|
|
379
|
+
this.#unavailableTransitions.push(event);
|
|
380
|
+
this.#softCheckpoint = undefined;
|
|
381
|
+
}
|
|
346
382
|
this.#advisorAvailable = available;
|
|
347
383
|
}
|
|
348
384
|
|
|
@@ -385,6 +421,13 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
385
421
|
return { allow: true };
|
|
386
422
|
}
|
|
387
423
|
if (call.function.name === 'PresentResult') {
|
|
424
|
+
// Review the actual submission, not an earlier manifest with different
|
|
425
|
+
// files/claims. Whitespace-only JSON changes are not a new revision.
|
|
426
|
+
const manifest = manifestFingerprint(call.function.arguments);
|
|
427
|
+
this.#deliveryManifest = manifest;
|
|
428
|
+
if (this.#deliveryReviewedWork >= 0 && this.#reviewedManifest !== manifest) {
|
|
429
|
+
this.#completed.delete(checkpointKey('pre-delivery', this.versions));
|
|
430
|
+
}
|
|
388
431
|
this.#deliveryDraft = `待提交成果(尚未发布;文件内容必须以读取/渲染证据核实):\n${call.function.arguments}`;
|
|
389
432
|
return this.beforeDelivery(undefined, true);
|
|
390
433
|
}
|
|
@@ -393,9 +436,11 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
393
436
|
if (failure && !failure.allow) return failure;
|
|
394
437
|
}
|
|
395
438
|
if (semantics.mutatesWorkspace) {
|
|
439
|
+
if (this.#required?.reviewReason) return this.#defer(this.#required.checkpoint, this.#required.reason!, this.#required.key);
|
|
440
|
+
if (isCosmeticRequest(this.#currentRequest)) return { allow: true };
|
|
396
441
|
return this.requestCheckpoint(
|
|
397
442
|
'pre-execution',
|
|
398
|
-
'
|
|
443
|
+
'即将执行可能修改工作区的操作,先复核执行方案',
|
|
399
444
|
);
|
|
400
445
|
}
|
|
401
446
|
return { allow: true };
|
|
@@ -404,38 +449,15 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
404
449
|
requestCheckpoint(
|
|
405
450
|
checkpoint: AdvisorCheckpoint,
|
|
406
451
|
reason: string,
|
|
407
|
-
options: {
|
|
452
|
+
options: { signature?: string } = {},
|
|
408
453
|
): CheckpointDecision {
|
|
409
454
|
if (!this.#advisorAvailable || checkpoint === 'high-risk') return { allow: true };
|
|
410
455
|
this.#commitTask();
|
|
411
456
|
const key = checkpointKey(checkpoint, this.versions, options.signature);
|
|
412
457
|
if (this.#completed.has(key)) return { allow: true };
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
this.#required = undefined;
|
|
416
|
-
this.#completed.add(key);
|
|
417
|
-
const notice = '交付前顾问检查已提示一次但未完成咨询,本次有界降级;不得声称结果已获顾问验证。';
|
|
418
|
-
this.#reminders.push(notice);
|
|
419
|
-
this.#degradationWarnings.add(notice);
|
|
420
|
-
return { allow: true };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// A missed consultation must not keep the task suspended.
|
|
424
|
-
const seen = (this.#deferCounts.get(key) ?? 0) + 1;
|
|
425
|
-
this.#deferCounts.set(key, seen);
|
|
426
|
-
if (seen > MAX_CONSECUTIVE_DEFERRALS) {
|
|
427
|
-
this.#recordDegradation(checkpoint, key);
|
|
428
|
-
this.#required = undefined;
|
|
429
|
-
this.#completed.add(key);
|
|
430
|
-
// 绕过了本该经过的检查点就必须留痕:静默放行会让模型以为自己合规了。
|
|
431
|
-
const notice = `Advisor 检查点 ${checkpoint} 已连续暂缓 ${MAX_CONSECUTIVE_DEFERRALS} 次仍未完成咨询,本次降级放行。这一步没有第二意见背书,请自行加倍核实,并在汇报时如实说明。`;
|
|
432
|
-
this.#degradationWarnings.add(notice);
|
|
433
|
-
if (!this.#reminders.includes(notice)) this.#reminders.push(notice);
|
|
434
|
-
return { allow: true };
|
|
435
|
-
}
|
|
436
|
-
|
|
458
|
+
// A reminder is not an attempt. The host must execute the pending review,
|
|
459
|
+
// or record its actual failure/unavailability before releasing this boundary.
|
|
437
460
|
this.#required = { checkpoint, key, reason };
|
|
438
|
-
this.#deferredOnce.add(key);
|
|
439
461
|
return this.#defer(checkpoint, reason, key);
|
|
440
462
|
}
|
|
441
463
|
|
|
@@ -456,16 +478,35 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
456
478
|
|
|
457
479
|
shouldCheckDelivery(): boolean {
|
|
458
480
|
return (
|
|
459
|
-
this.#mutations > 0 ||
|
|
481
|
+
(this.#mutations > 0 && !isCosmeticRequest(this.#currentRequest)) ||
|
|
460
482
|
this.#verifications > 0 ||
|
|
461
483
|
this.#deliveryProduced ||
|
|
484
|
+
needsTextDeliveryReview(this.#currentRequest, this.#deliveryDraft ?? '') ||
|
|
462
485
|
this.#investigationEvidence >= 3
|
|
463
486
|
);
|
|
464
487
|
}
|
|
465
488
|
|
|
489
|
+
/** After one ignored reminder, the host executes the consultation through its
|
|
490
|
+
* normal tool pipeline instead of treating a missing attempt as a failed one. */
|
|
491
|
+
pendingReview(draft?: string): { reason: AdvisorReason; note: string } | undefined {
|
|
492
|
+
if (draft !== undefined) this.#deliveryDraft = draft;
|
|
493
|
+
const active = this.#required ?? this.#softCheckpoint;
|
|
494
|
+
if (!this.#advisorAvailable || !active || this.#completed.has(active.key)) return;
|
|
495
|
+
return { reason: active.reviewReason ?? (active.checkpoint === 'pre-delivery' ? 'delivery_review'
|
|
496
|
+
: active.checkpoint === 'stalled' ? 'stalled' : 'plan_review'),
|
|
497
|
+
note: active.reason ?? '核实当前问题、待执行方案或待交付草稿,主 Agent 读取第二意见后自行判断继续。' };
|
|
498
|
+
}
|
|
499
|
+
|
|
466
500
|
beforeDelivery(draft?: string, force = false): CheckpointDecision {
|
|
467
501
|
if (draft !== undefined) this.#deliveryDraft = draft;
|
|
468
502
|
if (!this.#advisorAvailable) return { allow: true };
|
|
503
|
+
this.#requireSignal(draft ?? '', true);
|
|
504
|
+
if (this.#required?.reviewReason) return this.#defer(this.#required.checkpoint, this.#required.reason!, this.#required.key);
|
|
505
|
+
// A later successful mutation changes the reviewed artifact. Ordinary reads
|
|
506
|
+
// and wording changes do not reopen the checkpoint.
|
|
507
|
+
if (this.#deliveryReviewedWork >= 0 && this.#workVersion > this.#deliveryReviewedWork) {
|
|
508
|
+
this.#completed.delete(checkpointKey('pre-delivery', this.versions));
|
|
509
|
+
}
|
|
469
510
|
const failure = this.#checkFailureReview();
|
|
470
511
|
if (failure && !failure.allow) {
|
|
471
512
|
if (failure.deferred && !failure.deferred.ok) this.#reminders.push(failure.deferred.error);
|
|
@@ -475,7 +516,6 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
475
516
|
const decision = this.requestCheckpoint(
|
|
476
517
|
'pre-delivery',
|
|
477
518
|
'当前回合已产生实质成果或证据,提交最终结论前进行一次第二意见复核',
|
|
478
|
-
{ oneShot: true },
|
|
479
519
|
);
|
|
480
520
|
if (
|
|
481
521
|
decision.deferred &&
|
|
@@ -493,13 +533,12 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
|
|
|
493
533
|
const explicit = reason === 'plan_review' ? 'pre-execution' : reason === 'delivery_review' ? 'pre-delivery' : undefined;
|
|
494
534
|
const active = explicit
|
|
495
535
|
? { checkpoint: explicit as AdvisorCheckpoint, key: checkpointKey(explicit, this.versions) }
|
|
496
|
-
: (reason === 'evidence_conflict' && this.#pending)
|
|
536
|
+
: this.#required ?? ((reason === 'evidence_conflict' && this.#pending)
|
|
497
537
|
? { checkpoint: this.#pending.checkpoint, key: this.#pending.checkpointKey }
|
|
498
|
-
: this.#
|
|
499
|
-
this.#softCheckpoint ?? {
|
|
538
|
+
: this.#softCheckpoint ?? {
|
|
500
539
|
checkpoint: 'pre-execution' as const,
|
|
501
540
|
key: `${this.versions.taskVersion}:voluntary`,
|
|
502
|
-
};
|
|
541
|
+
});
|
|
503
542
|
this.#softCheckpoint = { checkpoint: active.checkpoint, key: active.key };
|
|
504
543
|
return {
|
|
505
544
|
kind: 'advisor_consultation',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Conservative text-delivery signals, not a claim of semantic understanding.
|
|
2
|
+
* Keep tiny acknowledgements/questions cheap; substantive text is an artifact too.
|
|
3
|
+
*/
|
|
4
|
+
export function needsTextDeliveryReview(request, draft) {
|
|
5
|
+
const text = draft.trim();
|
|
6
|
+
if (!text)
|
|
7
|
+
return false;
|
|
8
|
+
const conclusion = /建议|推荐|结论|成本|费用|计费|预算|风险|冲突|估算|recommend|conclusion|cost|pricing|estimate|conflict|risk/i;
|
|
9
|
+
const task = /分析|调研|评估|比较|对比|审查|复核|计算|设计|方案|analy[sz]|research|evaluat|compar|review|calculat|design/i;
|
|
10
|
+
// Numeric estimates and decisions can be short and still deserve review.
|
|
11
|
+
if (text.length >= 20 && conclusion.test(text) && /\d/.test(text))
|
|
12
|
+
return true;
|
|
13
|
+
// Length alone is not substance: long greetings, quotations and clarification
|
|
14
|
+
// questions must not become reviews merely because they exceed a character cap.
|
|
15
|
+
if (/^(请提供|请问|能否提供|please provide|could you provide)/i.test(text))
|
|
16
|
+
return false;
|
|
17
|
+
return (text.length >= 40 && conclusion.test(text))
|
|
18
|
+
|| (task.test(request) && text.length >= 40 && !/[??]$/.test(text));
|
|
19
|
+
}
|
|
20
|
+
export function reviewSignal(text) {
|
|
21
|
+
// Topic discussion and explicit absence of conflict are not incident signals.
|
|
22
|
+
if (/^(?:什么是|解释一下|介绍一下|如何处理|what is|explain)\s*/i.test(text.trim()))
|
|
23
|
+
return;
|
|
24
|
+
text = text.replace(/(?:没有|不存在|未发现|并无).{0,8}(?:冲突|矛盾|不一致)/g, '');
|
|
25
|
+
if (/你.{0,12}(又错了|还是不对|根本没解决|没有解决|答非所问)|我.{0,6}(不满意|不能接受)|\b(?:your (?:answer|solution) is wrong|still not fixed|not what I asked)\b/i.test(text))
|
|
26
|
+
return 'user_dissatisfaction';
|
|
27
|
+
if (/(?:证据|来源|数据|文档|结论).{0,12}(?:冲突|矛盾|不一致)|\bconflicting (?:evidence|sources|data)\b/i.test(text))
|
|
28
|
+
return 'evidence_conflict';
|
|
29
|
+
if (/无法(?:判断|确定|解决)|没有把握|高度不确定|\b(?:cannot determine|unable to resolve)\b/i.test(text))
|
|
30
|
+
return 'uncertainty';
|
|
31
|
+
}
|
|
32
|
+
/** Only explicit cosmetic edits skip implementation review; ambiguous edits do not. */
|
|
33
|
+
export function isCosmeticRequest(request) {
|
|
34
|
+
return /^(?:请)?(?:只|仅)(?:修改|修正|改|调整).{0,30}(?:错别字|拼写|标点|文案|typo|spelling)[。.!!]?$/i.test(request.trim());
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Conservative text-delivery signals, not a claim of semantic understanding.
|
|
2
|
+
* Keep tiny acknowledgements/questions cheap; substantive text is an artifact too.
|
|
3
|
+
*/
|
|
4
|
+
export function needsTextDeliveryReview(request: string, draft: string): boolean {
|
|
5
|
+
const text = draft.trim();
|
|
6
|
+
if (!text) return false;
|
|
7
|
+
const conclusion = /建议|推荐|结论|成本|费用|计费|预算|风险|冲突|估算|recommend|conclusion|cost|pricing|estimate|conflict|risk/i;
|
|
8
|
+
const task = /分析|调研|评估|比较|对比|审查|复核|计算|设计|方案|analy[sz]|research|evaluat|compar|review|calculat|design/i;
|
|
9
|
+
// Numeric estimates and decisions can be short and still deserve review.
|
|
10
|
+
if (text.length >= 20 && conclusion.test(text) && /\d/.test(text)) return true;
|
|
11
|
+
// Length alone is not substance: long greetings, quotations and clarification
|
|
12
|
+
// questions must not become reviews merely because they exceed a character cap.
|
|
13
|
+
if (/^(请提供|请问|能否提供|please provide|could you provide)/i.test(text)) return false;
|
|
14
|
+
return (text.length >= 40 && conclusion.test(text))
|
|
15
|
+
|| (task.test(request) && text.length >= 40 && !/[??]$/.test(text));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function reviewSignal(text: string): 'user_dissatisfaction' | 'evidence_conflict' | 'uncertainty' | undefined {
|
|
19
|
+
// Topic discussion and explicit absence of conflict are not incident signals.
|
|
20
|
+
if (/^(?:什么是|解释一下|介绍一下|如何处理|what is|explain)\s*/i.test(text.trim())) return;
|
|
21
|
+
text = text.replace(/(?:没有|不存在|未发现|并无).{0,8}(?:冲突|矛盾|不一致)/g, '');
|
|
22
|
+
if (/你.{0,12}(又错了|还是不对|根本没解决|没有解决|答非所问)|我.{0,6}(不满意|不能接受)|\b(?:your (?:answer|solution) is wrong|still not fixed|not what I asked)\b/i.test(text)) return 'user_dissatisfaction';
|
|
23
|
+
if (/(?:证据|来源|数据|文档|结论).{0,12}(?:冲突|矛盾|不一致)|\bconflicting (?:evidence|sources|data)\b/i.test(text)) return 'evidence_conflict';
|
|
24
|
+
if (/无法(?:判断|确定|解决)|没有把握|高度不确定|\b(?:cannot determine|unable to resolve)\b/i.test(text)) return 'uncertainty';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Only explicit cosmetic edits skip implementation review; ambiguous edits do not. */
|
|
28
|
+
export function isCosmeticRequest(request: string): boolean {
|
|
29
|
+
return /^(?:请)?(?:只|仅)(?:修改|修正|改|调整).{0,30}(?:错别字|拼写|标点|文案|typo|spelling)[。.!!]?$/i.test(request.trim());
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Display only: never rewrite the model's tool result or persisted audit. */
|
|
2
|
+
export function advisorOpinionText(content) {
|
|
3
|
+
return content.replace(/^consultationId: [^\r\n]+\r?\n调用 AdvisorDecision 时必须原样使用上面的完整 consultationId。\r?\n(?:\r?\n)?/, '');
|
|
4
|
+
}
|
|
5
|
+
export function advisorDecisionLabel(decision) {
|
|
6
|
+
return { accept: '已采纳', partial: '部分采纳', reject: '未采纳' }[decision] ?? '已记录';
|
|
7
|
+
}
|
|
8
|
+
export function advisorEvidenceLabel(status) {
|
|
9
|
+
return status === 'evidence_linked' ? '已关联证据 · 不代表核验通过' : '待验证 · 未关联证据';
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Display only: never rewrite the model's tool result or persisted audit. */
|
|
2
|
+
export function advisorOpinionText(content: string): string {
|
|
3
|
+
return content.replace(/^consultationId: [^\r\n]+\r?\n调用 AdvisorDecision 时必须原样使用上面的完整 consultationId。\r?\n(?:\r?\n)?/, '');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function advisorDecisionLabel(decision: string): string {
|
|
7
|
+
return ({ accept: '已采纳', partial: '部分采纳', reject: '未采纳' } as Record<string, string>)[decision] ?? '已记录';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function advisorEvidenceLabel(status?: string): string {
|
|
11
|
+
return status === 'evidence_linked' ? '已关联证据 · 不代表核验通过' : '待验证 · 未关联证据';
|
|
12
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { provenanceLabel } from '../llm/messageProvenance.js';
|
|
2
|
+
export function collectReviewContext(bundle) {
|
|
3
|
+
const supported = new Set(['project', 'memory', 'persona', 'task', 'runtime']);
|
|
4
|
+
return {
|
|
5
|
+
constraints: (bundle?.fragments ?? []).filter(f => supported.has(f.kind)).map(({ id, authority, content }) => ({ id, authority, content })),
|
|
6
|
+
omittedIds: bundle?.diagnostics.omittedIds ?? [],
|
|
7
|
+
clippedIds: bundle?.diagnostics.clippedIds ?? [],
|
|
8
|
+
notForwardedIds: (bundle?.fragments ?? []).filter(f => !supported.has(f.kind)).map(f => f.id),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function partitionReviewContext(host) {
|
|
12
|
+
return { ...host,
|
|
13
|
+
constraints: host.constraints.filter(f => f.authority === 'constraint' || f.authority === 'host'),
|
|
14
|
+
evidence: host.constraints.filter(f => f.authority === 'user-evidence'),
|
|
15
|
+
untrustedBackground: host.constraints.filter(f => !['constraint', 'host', 'user-evidence'].includes(f.authority)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Keep source locations and relevant middle passages, not just the head/tail.
|
|
19
|
+
* Selection is lexical, not a claim that the omitted content is irrelevant. */
|
|
20
|
+
export function reviewExcerpt(text, maxChars, query) {
|
|
21
|
+
if (text.length <= maxChars)
|
|
22
|
+
return text;
|
|
23
|
+
const limit = Math.max(0, maxChars);
|
|
24
|
+
if (limit < 180)
|
|
25
|
+
return `[材料过长,已省略 ${text.length} 字符;无法核实]`.slice(0, limit);
|
|
26
|
+
const terms = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}_./-]{2,}/gu) ?? [])]
|
|
27
|
+
.filter(term => term.length < 100).slice(0, 64);
|
|
28
|
+
const lower = text.toLowerCase();
|
|
29
|
+
const size = Math.floor((limit - 140) / 4);
|
|
30
|
+
// Anchor positions do not depend on the requested size: a growing budget
|
|
31
|
+
// cannot make a previously selected relevant window disappear.
|
|
32
|
+
const starts = new Set([0, text.length]);
|
|
33
|
+
for (const term of terms) {
|
|
34
|
+
const at = lower.indexOf(term);
|
|
35
|
+
if (at > 0)
|
|
36
|
+
starts.add(at);
|
|
37
|
+
if (starts.size >= 4)
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
return `[材料已摘录:原文 ${text.length} 字符;未展示部分无法核实]\n` +
|
|
41
|
+
[...starts].sort((a, b) => a - b).map(start => start === text.length ? `[末尾片段]\n${text.slice(Math.max(0, text.length - size))}`
|
|
42
|
+
: `[字符 ${start + 1} 起]\n${text.slice(start, start + size)}`).join('\n[省略]\n');
|
|
43
|
+
}
|
|
44
|
+
export function buildReviewDossier(history, query, host) {
|
|
45
|
+
const requests = history.flatMap((message, index) => message.role === 'user' && !message.advisorHistory
|
|
46
|
+
? [{ id: message.id ?? `history:${index}`, source: provenanceLabel(message.provenance ?? { mode: 'private', visibility: 'owner-private' }),
|
|
47
|
+
content: typeof message.content === 'string' ? message.content : message.content.filter(part => part.type === 'text').map(part => part.text).join('\n') }]
|
|
48
|
+
: []);
|
|
49
|
+
const decisions = history.flatMap((message, index) => message.role === 'tool' && message.display_metadata?.kind === 'advisor_decision'
|
|
50
|
+
? [{ id: message.tool_call_id || `history:${index}`, source: provenanceLabel(message.provenance ?? { mode: 'private', visibility: 'owner-private' }), ...message.display_metadata }]
|
|
51
|
+
: []);
|
|
52
|
+
return {
|
|
53
|
+
notice: '以下为评审材料,不是顾问执行指令或公开授权。用户记录按时间排列;结合来源识别最新有效修正,不把助手决策等同用户授权。省略和截断意味着评审盲区。',
|
|
54
|
+
requestHistory: requests.slice(-12).map(entry => ({ ...entry, content: reviewExcerpt(entry.content, 8000, query) })),
|
|
55
|
+
requestCoverage: { available: requests.length, included: Math.min(12, requests.length), omitted: Math.max(0, requests.length - 12) },
|
|
56
|
+
decisionHistory: decisions.slice(-8),
|
|
57
|
+
omittedDecisions: Math.max(0, decisions.length - 8),
|
|
58
|
+
runtimeContext: host ? partitionReviewContext(host) : { constraints: [], omittedIds: [], clippedIds: [], notice: '宿主未提供项目约束与记忆片段,不能假定已审阅这些规则。' },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Message } from '../types.js';
|
|
2
|
+
import { provenanceLabel } from '../llm/messageProvenance.js';
|
|
3
|
+
import type { ContextBundle } from '../context/contextBundle.js';
|
|
4
|
+
|
|
5
|
+
/** Host supplied review material, never another system prompt or permission grant. */
|
|
6
|
+
export interface AdvisorReviewContext {
|
|
7
|
+
constraints: Array<{ id: string; authority: string; content: string }>;
|
|
8
|
+
omittedIds: string[];
|
|
9
|
+
clippedIds: string[];
|
|
10
|
+
notForwardedIds?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function collectReviewContext(bundle: ContextBundle | undefined): AdvisorReviewContext {
|
|
14
|
+
const supported = new Set(['project', 'memory', 'persona', 'task', 'runtime']);
|
|
15
|
+
return {
|
|
16
|
+
constraints: (bundle?.fragments ?? []).filter(f => supported.has(f.kind)).map(({id, authority, content}) => ({id, authority, content})),
|
|
17
|
+
omittedIds: bundle?.diagnostics.omittedIds ?? [],
|
|
18
|
+
clippedIds: bundle?.diagnostics.clippedIds ?? [],
|
|
19
|
+
notForwardedIds: (bundle?.fragments ?? []).filter(f => !supported.has(f.kind)).map(f => f.id),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function partitionReviewContext(host: AdvisorReviewContext) {
|
|
24
|
+
return { ...host,
|
|
25
|
+
constraints: host.constraints.filter(f => f.authority === 'constraint' || f.authority === 'host'),
|
|
26
|
+
evidence: host.constraints.filter(f => f.authority === 'user-evidence'),
|
|
27
|
+
untrustedBackground: host.constraints.filter(f => !['constraint', 'host', 'user-evidence'].includes(f.authority)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Keep source locations and relevant middle passages, not just the head/tail.
|
|
32
|
+
* Selection is lexical, not a claim that the omitted content is irrelevant. */
|
|
33
|
+
export function reviewExcerpt(text: string, maxChars: number, query: string): string {
|
|
34
|
+
if (text.length <= maxChars) return text;
|
|
35
|
+
const limit = Math.max(0, maxChars);
|
|
36
|
+
if (limit < 180) return `[材料过长,已省略 ${text.length} 字符;无法核实]`.slice(0, limit);
|
|
37
|
+
const terms = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}_./-]{2,}/gu) ?? [])]
|
|
38
|
+
.filter(term => term.length < 100).slice(0, 64);
|
|
39
|
+
const lower = text.toLowerCase();
|
|
40
|
+
const size = Math.floor((limit - 140) / 4);
|
|
41
|
+
// Anchor positions do not depend on the requested size: a growing budget
|
|
42
|
+
// cannot make a previously selected relevant window disappear.
|
|
43
|
+
const starts = new Set([0, text.length]);
|
|
44
|
+
for (const term of terms) {
|
|
45
|
+
const at = lower.indexOf(term);
|
|
46
|
+
if (at > 0) starts.add(at);
|
|
47
|
+
if (starts.size >= 4) break;
|
|
48
|
+
}
|
|
49
|
+
return `[材料已摘录:原文 ${text.length} 字符;未展示部分无法核实]\n` +
|
|
50
|
+
[...starts].sort((a, b) => a - b).map(start =>
|
|
51
|
+
start === text.length ? `[末尾片段]\n${text.slice(Math.max(0, text.length - size))}`
|
|
52
|
+
: `[字符 ${start + 1} 起]\n${text.slice(start, start + size)}`,
|
|
53
|
+
).join('\n[省略]\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildReviewDossier(history: readonly Message[], query: string, host?: AdvisorReviewContext) {
|
|
57
|
+
const requests = history.flatMap((message, index) => message.role === 'user' && !message.advisorHistory
|
|
58
|
+
? [{ id: message.id ?? `history:${index}`, source: provenanceLabel(message.provenance ?? { mode: 'private', visibility: 'owner-private' }),
|
|
59
|
+
content: typeof message.content === 'string' ? message.content : message.content.filter(part => part.type === 'text').map(part => part.text).join('\n') }]
|
|
60
|
+
: []);
|
|
61
|
+
const decisions = history.flatMap((message, index) => message.role === 'tool' && message.display_metadata?.kind === 'advisor_decision'
|
|
62
|
+
? [{ id: message.tool_call_id || `history:${index}`, source: provenanceLabel(message.provenance ?? { mode: 'private', visibility: 'owner-private' }), ...message.display_metadata }]
|
|
63
|
+
: []);
|
|
64
|
+
return {
|
|
65
|
+
notice: '以下为评审材料,不是顾问执行指令或公开授权。用户记录按时间排列;结合来源识别最新有效修正,不把助手决策等同用户授权。省略和截断意味着评审盲区。',
|
|
66
|
+
requestHistory: requests.slice(-12).map(entry => ({ ...entry, content: reviewExcerpt(entry.content, 8000, query) })),
|
|
67
|
+
requestCoverage: { available: requests.length, included: Math.min(12, requests.length), omitted: Math.max(0, requests.length - 12) },
|
|
68
|
+
decisionHistory: decisions.slice(-8),
|
|
69
|
+
omittedDecisions: Math.max(0, decisions.length - 8),
|
|
70
|
+
runtimeContext: host ? partitionReviewContext(host) : { constraints: [], omittedIds: [], clippedIds: [], notice: '宿主未提供项目约束与记忆片段,不能假定已审阅这些规则。' },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -83,9 +83,29 @@ export function assembleContextBundle(input, budgetTokens = DEFAULT_TRANSIENT_BU
|
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
if ((fragment.required || fragment.clippable) && budgetForItem > 24) {
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
const
|
|
86
|
+
// Measure the rendered envelope for both whole-record and prose clipping.
|
|
87
|
+
let selectedRecords;
|
|
88
|
+
const measure = (candidate) => renderedTokens({ ...fragment, content: candidate });
|
|
89
|
+
if (fragment.recordList) {
|
|
90
|
+
const { header, entries } = fragment.recordList;
|
|
91
|
+
const kept = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const candidate = `${header}\n${[...kept, entry].map(record => record.text).join('\n')}${CLIP_MARKER}`;
|
|
94
|
+
if (measure(candidate) <= budgetForItem)
|
|
95
|
+
kept.push(entry);
|
|
96
|
+
}
|
|
97
|
+
selectedRecords = { header, entries: kept };
|
|
98
|
+
}
|
|
99
|
+
const content = selectedRecords
|
|
100
|
+
? selectedRecords.entries.length
|
|
101
|
+
? `${selectedRecords.header}\n${selectedRecords.entries.map(record => record.text).join('\n')}${CLIP_MARKER}`
|
|
102
|
+
: ''
|
|
103
|
+
: clipToRenderedBudget(fragment.content, budgetForItem, measure);
|
|
104
|
+
const clipped = contextFragment({
|
|
105
|
+
...fragment,
|
|
106
|
+
content,
|
|
107
|
+
...(selectedRecords ? { recordList: selectedRecords } : {}),
|
|
108
|
+
});
|
|
89
109
|
const clippedTokens = renderedTokens(clipped);
|
|
90
110
|
if (clipped.content && clippedTokens <= budgetForItem) {
|
|
91
111
|
included.push({ fragment: clipped, index: item.index });
|
|
@@ -22,6 +22,8 @@ export interface ContextFragment {
|
|
|
22
22
|
content: string;
|
|
23
23
|
required?: boolean;
|
|
24
24
|
clippable?: boolean;
|
|
25
|
+
/** Whole evidence records: never detach a statement from its qualifications. */
|
|
26
|
+
recordList?: { header: string; entries: Array<{ id: string; text: string }> };
|
|
25
27
|
/** Constraints that must remain whole, even when this exceeds the soft budget. */
|
|
26
28
|
atomic?: boolean;
|
|
27
29
|
}
|
|
@@ -137,13 +139,32 @@ export function assembleContextBundle(
|
|
|
137
139
|
continue;
|
|
138
140
|
}
|
|
139
141
|
if ((fragment.required || fragment.clippable) && budgetForItem > 24) {
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
// Measure the rendered envelope for both whole-record and prose clipping.
|
|
143
|
+
let selectedRecords: ContextFragment['recordList'];
|
|
144
|
+
const measure = (candidate: string) => renderedTokens({ ...fragment, content: candidate });
|
|
145
|
+
if (fragment.recordList) {
|
|
146
|
+
const { header, entries } = fragment.recordList;
|
|
147
|
+
const kept: typeof entries = [];
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const candidate = `${header}\n${[...kept, entry].map(record => record.text).join('\n')}${CLIP_MARKER}`;
|
|
150
|
+
if (measure(candidate) <= budgetForItem) kept.push(entry);
|
|
151
|
+
}
|
|
152
|
+
selectedRecords = { header, entries: kept };
|
|
153
|
+
}
|
|
154
|
+
const content = selectedRecords
|
|
155
|
+
? selectedRecords.entries.length
|
|
156
|
+
? `${selectedRecords.header}\n${selectedRecords.entries.map(record => record.text).join('\n')}${CLIP_MARKER}`
|
|
157
|
+
: ''
|
|
158
|
+
: clipToRenderedBudget(
|
|
159
|
+
fragment.content,
|
|
160
|
+
budgetForItem,
|
|
161
|
+
measure,
|
|
162
|
+
);
|
|
163
|
+
const clipped = contextFragment({
|
|
164
|
+
...fragment,
|
|
165
|
+
content,
|
|
166
|
+
...(selectedRecords ? { recordList: selectedRecords } : {}),
|
|
167
|
+
});
|
|
147
168
|
const clippedTokens = renderedTokens(clipped);
|
|
148
169
|
if (clipped.content && clippedTokens <= budgetForItem) {
|
|
149
170
|
included.push({ fragment: clipped, index: item.index });
|
|
@@ -80,6 +80,9 @@ function estimateContentPartTokens(part) {
|
|
|
80
80
|
export function countMessagesTokens(messages) {
|
|
81
81
|
let total = 0;
|
|
82
82
|
for (const m of messages) {
|
|
83
|
+
// Display-only history is never sent to the provider and consumes no window.
|
|
84
|
+
if (m.role === 'system' && m._ephemeral)
|
|
85
|
+
continue;
|
|
83
86
|
// 每条消息至少有 ~4 token 的固定开销(role + 边界标记)
|
|
84
87
|
total += 4;
|
|
85
88
|
if (typeof m.content === 'string') {
|
|
@@ -80,6 +80,8 @@ function estimateContentPartTokens(part: ChatContentPart): number {
|
|
|
80
80
|
export function countMessagesTokens(messages: Message[]): number {
|
|
81
81
|
let total = 0;
|
|
82
82
|
for (const m of messages) {
|
|
83
|
+
// Display-only history is never sent to the provider and consumes no window.
|
|
84
|
+
if (m.role === 'system' && m._ephemeral) continue;
|
|
83
85
|
// 每条消息至少有 ~4 token 的固定开销(role + 边界标记)
|
|
84
86
|
total += 4;
|
|
85
87
|
|
|
@@ -37,7 +37,7 @@ export function prepareMessagesForProvider(messages, conversationMode = 'private
|
|
|
37
37
|
return messages
|
|
38
38
|
.filter((message) => !(message.role === 'system' && message._ephemeral === true))
|
|
39
39
|
.map((message) => {
|
|
40
|
-
const { id, timestamp, attachments, browserReferences, display_metadata, display_ok, tool_result_policy, provenance, advisorTaskVersion, advisorCheckpoints, advisorMutatesWorkspace, advisorHistory, toolResultDisposition, ...rest } = message;
|
|
40
|
+
const { id, timestamp, presentation, attachments, browserReferences, display_metadata, display_ok, tool_result_policy, provenance, advisorTaskVersion, advisorCheckpoints, advisorMutatesWorkspace, advisorHistory, toolResultDisposition, ...rest } = message;
|
|
41
41
|
if (rest.role === 'user')
|
|
42
42
|
turnProvenance = provenance;
|
|
43
43
|
if (rest.role === 'user' || rest.role === 'assistant' || rest.role === 'tool') {
|
|
@@ -46,7 +46,7 @@ export function prepareMessagesForProvider(
|
|
|
46
46
|
.filter((message) => !(message.role === 'system' && message._ephemeral === true))
|
|
47
47
|
.map((message) => {
|
|
48
48
|
const {
|
|
49
|
-
id, timestamp, attachments, browserReferences, display_metadata, display_ok, tool_result_policy, provenance, advisorTaskVersion, advisorCheckpoints, advisorMutatesWorkspace, advisorHistory, toolResultDisposition, ...rest
|
|
49
|
+
id, timestamp, presentation, attachments, browserReferences, display_metadata, display_ok, tool_result_policy, provenance, advisorTaskVersion, advisorCheckpoints, advisorMutatesWorkspace, advisorHistory, toolResultDisposition, ...rest
|
|
50
50
|
} = message as Message & Record<string, unknown>;
|
|
51
51
|
if (rest.role === 'user') turnProvenance = provenance;
|
|
52
52
|
if (rest.role === 'user' || rest.role === 'assistant' || rest.role === 'tool') {
|