@yeaft/webchat-agent 1.0.639 → 1.0.641
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +102 -102
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/attachments.js +22 -9
- package/yeaft/engine.js +95 -574
- package/yeaft/history-window.js +132 -5
- package/yeaft/query-progress.js +15 -2
- package/yeaft/response-presentation.js +27 -5
- package/yeaft/sessions/project-doc.js +29 -6
- package/yeaft/stats/tool-usage.js +90 -22
- package/yeaft/tools/git-read.js +7 -5
- package/yeaft/tools/view-image.js +54 -33
- package/yeaft/tools/web-fetch.js +65 -57
- package/yeaft/tools/web-search.js +116 -124
- package/yeaft/web-research.js +169 -0
package/yeaft/engine.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* Reference: yeaft-yeaft-implementation-plan.md §3.1, §4 (Phase 2)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { randomUUID
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
21
|
import { QueryProgress, progressReminder } from './query-progress.js';
|
|
22
|
+
import { createWebResearchState } from './web-research.js';
|
|
22
23
|
import { promises as fsp } from 'fs';
|
|
23
24
|
import { join, resolve as resolvePath } from 'path';
|
|
24
25
|
import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
pickProjectDocFile,
|
|
31
32
|
selectProjectDocContext,
|
|
32
33
|
projectDocPathHintsFromToolCall,
|
|
34
|
+
projectDocToolCallNeedsWriteReview,
|
|
33
35
|
projectDocWriteScopesNeedingReload,
|
|
34
36
|
DEFAULT_PROJECT_DOC_MAX_BYTES,
|
|
35
37
|
} from './sessions/project-doc.js';
|
|
@@ -53,12 +55,6 @@ import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
|
53
55
|
// pass a real threadId per (sessionId, vpId, threadId) engine instance.
|
|
54
56
|
const MAIN_THREAD_ID = 'main';
|
|
55
57
|
|
|
56
|
-
function foldingMessageKey(message) {
|
|
57
|
-
return message?._persistedMessageId || message?.id || createHash('sha256').update(JSON.stringify([
|
|
58
|
-
message?.role, message?.toolCallId, message?.toolCalls, message?.content,
|
|
59
|
-
])).digest('hex');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
58
|
import { pickEffort, parseEffortPrefix, snapshotEffortDecision } from './effort.js';
|
|
63
59
|
import { bindProviderState } from './llm/provider-state.js';
|
|
64
60
|
import {
|
|
@@ -69,8 +65,7 @@ import {
|
|
|
69
65
|
removePostCompactIfSource,
|
|
70
66
|
savePostCompact,
|
|
71
67
|
} from './post-compact.js';
|
|
72
|
-
import {
|
|
73
|
-
import { lookupModelLimitSync } from './llm/models-dev.js';
|
|
68
|
+
import { getModelInfo, normalizeEffort, parseModelRef, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
74
69
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
75
70
|
import { resolveThinking } from './router/thinking.js';
|
|
76
71
|
import { approxTokens, computeBudget } from './memory/budget.js';
|
|
@@ -82,18 +77,11 @@ import { createPluginSkillManager } from './plugins.js';
|
|
|
82
77
|
import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
|
|
83
78
|
import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
|
|
84
79
|
import {
|
|
85
|
-
TOOL_LOOP_REFLECTION_INTERVAL,
|
|
86
|
-
TURN_SUMMARY_THRESHOLD,
|
|
87
80
|
DUP_TOOL_THRESHOLD,
|
|
88
81
|
ExecLog,
|
|
89
82
|
buildEntry as buildExecLogEntry,
|
|
90
83
|
argsHashOf,
|
|
91
|
-
runT1Reflection,
|
|
92
|
-
runT2Reflection,
|
|
93
|
-
buildFallbackStub,
|
|
94
|
-
collapseRangeToReflection,
|
|
95
84
|
buildDuplicateReminder,
|
|
96
|
-
extractToolPairsFromRange,
|
|
97
85
|
} from './tool-folding/index.js';
|
|
98
86
|
|
|
99
87
|
/**
|
|
@@ -463,47 +451,6 @@ export function estimateProviderInputBreakdown({
|
|
|
463
451
|
};
|
|
464
452
|
}
|
|
465
453
|
|
|
466
|
-
export const GROUP_CONTEXT_PRESSURE_RATIO = 0.8;
|
|
467
|
-
|
|
468
|
-
export function shouldAllowGroupReflection({
|
|
469
|
-
system = '',
|
|
470
|
-
messages = [],
|
|
471
|
-
model = null,
|
|
472
|
-
config = {},
|
|
473
|
-
sessionId = null,
|
|
474
|
-
} = {}) {
|
|
475
|
-
if (!sessionId) {
|
|
476
|
-
return {
|
|
477
|
-
allowed: true,
|
|
478
|
-
tokenEstimate: estimateMessagesTokens(system, messages),
|
|
479
|
-
threshold: 0,
|
|
480
|
-
contextWindow: null,
|
|
481
|
-
ratio: GROUP_CONTEXT_PRESSURE_RATIO,
|
|
482
|
-
usedFallbackContextWindow: false,
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
const contextWindow = resolveContextWindow(model, config);
|
|
486
|
-
// Telemetry: did the resolver hit either of its top non-default rungs?
|
|
487
|
-
// Used by `usedFallbackContextWindow` below — if neither models.dev nor
|
|
488
|
-
// the global config provided a number, we fell through to DEFAULT and
|
|
489
|
-
// callers may want to surface that to the user.
|
|
490
|
-
const hasModelsDevContext = !!lookupModelLimitSync(model, resolveModel(model)?.provider || null)?.context;
|
|
491
|
-
const hasConfigContext = Number.isFinite(config?.maxContextTokens) && config.maxContextTokens > 0;
|
|
492
|
-
const threshold = Math.floor(contextWindow * GROUP_CONTEXT_PRESSURE_RATIO);
|
|
493
|
-
const tokenEstimate = estimateMessagesTokens(system, messages);
|
|
494
|
-
const overThreshold = tokenEstimate >= threshold;
|
|
495
|
-
return {
|
|
496
|
-
// Group send defaults to no reflection. Trust the model until context
|
|
497
|
-
// pressure says we are near the model window.
|
|
498
|
-
allowed: overThreshold,
|
|
499
|
-
tokenEstimate,
|
|
500
|
-
threshold,
|
|
501
|
-
contextWindow,
|
|
502
|
-
ratio: GROUP_CONTEXT_PRESSURE_RATIO,
|
|
503
|
-
usedFallbackContextWindow: !hasModelsDevContext && !hasConfigContext && contextWindow === DEFAULT_CONTEXT_WINDOW,
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
454
|
// ─── Engine Events (superset of adapter events) ──────────────────
|
|
508
455
|
|
|
509
456
|
/**
|
|
@@ -767,24 +714,8 @@ export class Engine {
|
|
|
767
714
|
*/
|
|
768
715
|
#currentAbortCtrl = null;
|
|
769
716
|
|
|
770
|
-
/**
|
|
771
|
-
* PR-L — V7 Tool History Reflection state. Owned per Engine instance.
|
|
772
|
-
*
|
|
773
|
-
* • `#execLog` — append-only log of every tool execution, used for
|
|
774
|
-
* fallback-stub generation and duplicate-call detection. Persists
|
|
775
|
-
* to <yeaftDir>/tool-log/<traceId>/<turnIdx>.jsonl when yeaftDir
|
|
776
|
-
* is set; in-memory only otherwise.
|
|
777
|
-
* • `#pendingT2` — Map<turnNumber, { promise, loopRange, count, ... }>
|
|
778
|
-
* keyed by the turn number that triggered T2. The next query() call
|
|
779
|
-
* non-blocking-checks this map; if the promise has resolved, the
|
|
780
|
-
* prior turn's history is rewritten with the reflection. If still
|
|
781
|
-
* pending, the engine falls back to the exec-log stub.
|
|
782
|
-
* • `#reflectedTurns` — Set<turnNumber>; ensures T1 fires at most
|
|
783
|
-
* once per reflection interval measured in tool loops.
|
|
784
|
-
*/
|
|
717
|
+
/** Append-only tool execution diagnostics and query-local duplicate lookup. */
|
|
785
718
|
#execLog = null;
|
|
786
|
-
#pendingT2 = new Map();
|
|
787
|
-
#reflectedTurns = new Set();
|
|
788
719
|
#__queryCounter = 0;
|
|
789
720
|
|
|
790
721
|
/** Derived post-response summaries, keyed by Session/VP/thread scope. */
|
|
@@ -1444,6 +1375,7 @@ export class Engine {
|
|
|
1444
1375
|
effortDecision: snapshotEffortDecision(vpCtx?.effortDecision),
|
|
1445
1376
|
requestIdentity: vpCtx?.requestIdentity,
|
|
1446
1377
|
queryTurnId: vpCtx?.queryTurnId,
|
|
1378
|
+
webResearch: vpCtx?.webResearch,
|
|
1447
1379
|
historyBeforeSeq: vpCtx?.historyBeforeSeq,
|
|
1448
1380
|
yeaftDir: this.#yeaftDir,
|
|
1449
1381
|
managedCliReady: this.#managedCliReady,
|
|
@@ -1680,17 +1612,6 @@ export class Engine {
|
|
|
1680
1612
|
return this.#conversationStore.append(this.#conversationRecord(message, context), { strict: context.strict === true });
|
|
1681
1613
|
}
|
|
1682
1614
|
|
|
1683
|
-
#persistFoldedRange(foldedMessages, reflection, context = {}) {
|
|
1684
|
-
if (!this.#canPersistConversation() || typeof this.#conversationStore?.foldMessages !== 'function') return null;
|
|
1685
|
-
const persistedRows = (foldedMessages || [])
|
|
1686
|
-
.map(message => message?._persistedMessageId || message?.id)
|
|
1687
|
-
.filter(id => typeof id === 'string' && id)
|
|
1688
|
-
.map(id => ({ id }));
|
|
1689
|
-
if (persistedRows.length === 0) return null;
|
|
1690
|
-
const record = this.#conversationRecord(reflection, context);
|
|
1691
|
-
return this.#conversationStore.foldMessages(persistedRows, record);
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
1615
|
#formatTaskResultUpdateContent(content) {
|
|
1695
1616
|
if (typeof content === 'string') return content;
|
|
1696
1617
|
try { return JSON.stringify(content); } catch { return String(content); }
|
|
@@ -1710,10 +1631,9 @@ export class Engine {
|
|
|
1710
1631
|
msg && msg.role === 'tool' && msg.toolCallId === update.toolCallId
|
|
1711
1632
|
));
|
|
1712
1633
|
if (!toolMsg) {
|
|
1713
|
-
//
|
|
1714
|
-
// completed.
|
|
1715
|
-
//
|
|
1716
|
-
// provider transcript. This is engine control context, not a fresh
|
|
1634
|
+
// Legacy histories may have folded the original tool row before this
|
|
1635
|
+
// task completed. A late completion must not recreate the hidden tool arc
|
|
1636
|
+
// on disk or in the provider transcript. This is engine control context, not a fresh
|
|
1717
1637
|
// user-authored message; ConversationStore keeps it off the visible
|
|
1718
1638
|
// transcript while retaining it for the next provider boundary.
|
|
1719
1639
|
const contextContent = truncateToolResultIfNeeded(appendText, {
|
|
@@ -2532,70 +2452,19 @@ export class Engine {
|
|
|
2532
2452
|
},
|
|
2533
2453
|
];
|
|
2534
2454
|
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
messages: conversationMessages,
|
|
2538
|
-
model: this.#config.model,
|
|
2539
|
-
config: this.#config,
|
|
2540
|
-
sessionId,
|
|
2541
|
-
});
|
|
2542
|
-
const groupReflectionAllowed = groupReflectionGate.allowed === true;
|
|
2543
|
-
if (sessionId && groupReflectionGate?.usedFallbackContextWindow) {
|
|
2544
|
-
this.#trace.log?.('group_context_window_fallback', {
|
|
2545
|
-
sessionId,
|
|
2546
|
-
model: this.#config.model,
|
|
2547
|
-
contextWindow: groupReflectionGate.contextWindow,
|
|
2548
|
-
threshold: groupReflectionGate.threshold,
|
|
2549
|
-
});
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
// PR-L: T2 carry-forward. If a previous query()'s end-of-turn
|
|
2553
|
-
// reflection has resolved, rewrite that turn's range in
|
|
2554
|
-
// `conversationMessages` to a single assistant reflection message.
|
|
2555
|
-
// If still pending, fall back to the exec-log stub — non-blocking,
|
|
2556
|
-
// never wait. This runs BEFORE the first adapter.stream so the
|
|
2557
|
-
// upcoming call sees the rewritten history. Group send defaults to no
|
|
2558
|
-
// reflection; only high context pressure (>=80% of model window)
|
|
2559
|
-
// enables the carry-forward rewrite.
|
|
2560
|
-
if (groupReflectionAllowed) {
|
|
2561
|
-
yield* this.#applyPendingT2Reflections(conversationMessages, prompt, {
|
|
2562
|
-
sessionId: runtimeSessionId,
|
|
2563
|
-
model: this.#config.model,
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
// PR-L: track this query()'s tool-arc for reflection.
|
|
2568
|
-
// `turnStartIdx` is where the current user message lives; the arc
|
|
2569
|
-
// we may collapse spans (arcStartIdx .. last assistant/tool).
|
|
2570
|
-
//
|
|
2571
|
-
// Periodic T1 fires every 30 tool loops, not every 30 calls. A single
|
|
2572
|
-
// provider batch can contain many parallel calls but is still one loop.
|
|
2573
|
-
// track:
|
|
2574
|
-
// • `lastT1AtLoopCount` — tool-loop snapshot at the last T1
|
|
2575
|
-
// ATTEMPT (success OR error). Trigger when
|
|
2576
|
-
// `completedToolLoops - lastT1AtLoopCount >= interval`.
|
|
2577
|
-
// • `arcStartIdx` — first index of the current (uncollapsed)
|
|
2578
|
-
// tool arc. Initialised to turnStartIdx + 1; reset after each
|
|
2579
|
-
// successful T1 collapse to `conversationMessages.length`
|
|
2580
|
-
// (i.e. the slot the next assistant message will land in).
|
|
2581
|
-
// • `t1CollapsesDone` — count of T1 firings that ACTUALLY
|
|
2582
|
-
// rewrote history. Distinct from `lastT1AtLoopCount` because
|
|
2583
|
-
// the catch block bumps the latter to back off after a
|
|
2584
|
-
// transient reflector error WITHOUT having collapsed
|
|
2585
|
-
// anything. The T2 schedule check below is gated on this
|
|
2586
|
-
// counter (==0 means "no T1 ever rewrote the arc, T2 may
|
|
2587
|
-
// fall back at end_turn").
|
|
2455
|
+
// Current-turn protocol stays on the live tape. Only the disposable
|
|
2456
|
+
// provider copy is compacted when its actual model-window budget is full.
|
|
2588
2457
|
const turnStartIdx = conversationMessages.length - 1;
|
|
2589
2458
|
let queryToolCount = 0;
|
|
2590
|
-
let lastT1AtLoopCount = 0;
|
|
2591
|
-
let arcStartIdx = turnStartIdx + 1;
|
|
2592
|
-
let t1CollapsesDone = 0;
|
|
2593
2459
|
// Duplicate policy is scoped to one user query. Only successful, real
|
|
2594
2460
|
// executions increment these counters; errors and cache reuse do not.
|
|
2595
2461
|
const queryDuplicateCounts = new Map();
|
|
2596
2462
|
const queryDuplicateSuppressions = new Map();
|
|
2597
2463
|
const queryValidationFailures = new Map();
|
|
2598
2464
|
const fileReadObservations = new Map();
|
|
2465
|
+
// Success reuse and in-flight sharing must not cross user turns, Sessions,
|
|
2466
|
+
// or child Engines. Never copy this ledger into parentEngineDeps.
|
|
2467
|
+
const webResearch = createWebResearchState();
|
|
2599
2468
|
let duplicateReminderAwaitingResponse = false;
|
|
2600
2469
|
const queryNumber = (this.#__queryCounter = (this.#__queryCounter || 0) + 1);
|
|
2601
2470
|
|
|
@@ -3057,7 +2926,7 @@ export class Engine {
|
|
|
3057
2926
|
));
|
|
3058
2927
|
const toolSchemaTokens = toolDefs.length > 0
|
|
3059
2928
|
? approxTokens(JSON.stringify(toolDefs)) : 0;
|
|
3060
|
-
const
|
|
2929
|
+
const fitOptions = {
|
|
3061
2930
|
contextWindow: Math.max(1, Math.floor(currentContextWindow * providerContextScale)),
|
|
3062
2931
|
systemTokens: estimateMessagesTokens(systemPrompt, []),
|
|
3063
2932
|
toolSchemaTokens,
|
|
@@ -3065,9 +2934,50 @@ export class Engine {
|
|
|
3065
2934
|
historyMessageCount,
|
|
3066
2935
|
historyTokenBudget: historyBudget,
|
|
3067
2936
|
language: requestConfig.language,
|
|
2937
|
+
};
|
|
2938
|
+
let fittedRequest = fitProviderRequestToContext(wireMessages, fitOptions);
|
|
2939
|
+
// A task can complete on an old tool row. Sampling or evicting that row
|
|
2940
|
+
// must not acknowledge unseen evidence. Re-inject only missing pending
|
|
2941
|
+
// deliveries as fresh provider-only notes, then check the final copy.
|
|
2942
|
+
const taskDeliveries = Array.from(this.#pendingAsyncTaskConfirmIds).flatMap(taskId => {
|
|
2943
|
+
const delivery = this.#acceptedAsyncTaskResults.get(taskId);
|
|
2944
|
+
if (!delivery) return [];
|
|
2945
|
+
const text = truncateToolResultIfNeeded(this.#formatTaskResultUpdateContent(delivery.content), {
|
|
2946
|
+
toolName: 'async task result', language: requestConfig.language,
|
|
2947
|
+
});
|
|
2948
|
+
return [{ taskId, text }];
|
|
3068
2949
|
});
|
|
2950
|
+
const includesDelivery = (messages, text) => messages.some(message => (
|
|
2951
|
+
this.#formatTaskResultUpdateContent(message.content).includes(text)
|
|
2952
|
+
));
|
|
2953
|
+
const missingDeliveries = taskDeliveries.filter(({ text }) => !includesDelivery(fittedRequest.messages, text));
|
|
2954
|
+
if (missingDeliveries.length > 0) {
|
|
2955
|
+
fittedRequest = fitProviderRequestToContext([...wireMessages, ...missingDeliveries.map(({ text }) => ({
|
|
2956
|
+
role: 'user', content: `[system note] Async task completion:\n${text}`,
|
|
2957
|
+
}))], fitOptions);
|
|
2958
|
+
}
|
|
3069
2959
|
wireMessages = fittedRequest.messages;
|
|
2960
|
+
const deliveredTaskIds = taskDeliveries.filter(({ text }) => includesDelivery(wireMessages, text))
|
|
2961
|
+
.map(({ taskId }) => taskId);
|
|
3070
2962
|
historyMessageCount = fittedRequest.meta.historyMessagesAfter;
|
|
2963
|
+
if (fittedRequest.meta.compactedToolResults > 0) {
|
|
2964
|
+
// Compaction invalidates file-read hints, not execution/reuse safety
|
|
2965
|
+
// ledgers. The complete result remains in transcript/debug storage.
|
|
2966
|
+
fileReadObservations.clear();
|
|
2967
|
+
recordAgentPerfTrace(requestConfig, {
|
|
2968
|
+
traceId: perfTraceId || queryTurnId,
|
|
2969
|
+
phase: 'tool_result_compact',
|
|
2970
|
+
sessionId: runtimeSessionId,
|
|
2971
|
+
vpId: queryVpId,
|
|
2972
|
+
turnId: queryTurnId,
|
|
2973
|
+
threadId: runtimeThreadId,
|
|
2974
|
+
detail: {
|
|
2975
|
+
model: currentModel,
|
|
2976
|
+
recoveryAttempt: contextOverflowRecoveryAttempts,
|
|
2977
|
+
...fittedRequest.meta,
|
|
2978
|
+
},
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
3071
2981
|
if (fittedRequest.meta.droppedHistoryMessages > 0
|
|
3072
2982
|
|| fittedRequest.meta.droppedCurrentMessages > 0
|
|
3073
2983
|
|| providerContextScale < 1) {
|
|
@@ -3080,49 +2990,6 @@ export class Engine {
|
|
|
3080
2990
|
});
|
|
3081
2991
|
}
|
|
3082
2992
|
|
|
3083
|
-
// task-704b: pre-flight total-token guard. Even with the per-tool
|
|
3084
|
-
// cap (registry.js: 10% of contextWindow per result), N tool
|
|
3085
|
-
// results plus history can still breach the wire limit before we
|
|
3086
|
-
// ever call adapter.stream(). Estimate the total token cost; if
|
|
3087
|
-
// it exceeds PREFLIGHT_RATIO of the live context window, run an
|
|
3088
|
-
// emergency archive sweep with `turnAgeMin: 0` so even
|
|
3089
|
-
// current-turn-but-not-this-call bulky results get stubbed. The
|
|
3090
|
-
// normal sweep above only stubs results older than 5 user turns
|
|
3091
|
-
// — that's the wrong cadence when the *current* turn already has
|
|
3092
|
-
// 4 large grep results.
|
|
3093
|
-
//
|
|
3094
|
-
// PREFLIGHT_RATIO = 0.85 leaves ~15% of the window for the model's
|
|
3095
|
-
// own output tokens + tools metadata + light future history.
|
|
3096
|
-
// The estimator (`estimateMessagesTokens`) is approxTokens
|
|
3097
|
-
// (char/4 with CJK weighting) — good enough for a guard rail; a
|
|
3098
|
-
// real tokenizer would be exact but adds a heavy dep.
|
|
3099
|
-
if (scenario !== 'work-item' && this.#yeaftDir && (this.#config?.archive?.toolResults !== false)) {
|
|
3100
|
-
const PREFLIGHT_RATIO = 0.85;
|
|
3101
|
-
const threshold = Math.floor(currentContextWindow * PREFLIGHT_RATIO);
|
|
3102
|
-
const estimate = estimateMessagesTokens(systemPrompt, wireMessages);
|
|
3103
|
-
if (estimate > threshold) {
|
|
3104
|
-
try {
|
|
3105
|
-
const sweep = await archiveToolResults({
|
|
3106
|
-
root: `${this.#yeaftDir}/memory`,
|
|
3107
|
-
scopeDir: 'user',
|
|
3108
|
-
messages: wireMessages,
|
|
3109
|
-
turnAgeMin: 0,
|
|
3110
|
-
lengthMin: this.#config?.archive?.lengthMin ?? 2000,
|
|
3111
|
-
});
|
|
3112
|
-
wireMessages = sweep.nextMessages;
|
|
3113
|
-
if (sweep.archivedCount > 0) {
|
|
3114
|
-
this.#trace.log?.('preflight_sweep', {
|
|
3115
|
-
archivedCount: sweep.archivedCount,
|
|
3116
|
-
archivedBytes: sweep.archivedBytes,
|
|
3117
|
-
estimateBefore: estimate,
|
|
3118
|
-
threshold,
|
|
3119
|
-
contextWindow: currentContextWindow,
|
|
3120
|
-
});
|
|
3121
|
-
}
|
|
3122
|
-
} catch { /* best-effort */ }
|
|
3123
|
-
}
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
2993
|
// Freeze diagnostics only after every provider-only trim/sweep has
|
|
3127
2994
|
// completed, so debug describes the exact request copy rather than the
|
|
3128
2995
|
// durable transcript or an earlier preflight candidate.
|
|
@@ -3218,7 +3085,7 @@ export class Engine {
|
|
|
3218
3085
|
// is not delivery: fetch may remain pending and then be aborted before
|
|
3219
3086
|
// the provider processes anything. Ack only after a normal stream end
|
|
3220
3087
|
// that included the provider's terminal stop event.
|
|
3221
|
-
const requestAsyncTaskIds =
|
|
3088
|
+
const requestAsyncTaskIds = deliveredTaskIds;
|
|
3222
3089
|
let sawProviderStop = false;
|
|
3223
3090
|
for await (const event of providerStream) {
|
|
3224
3091
|
// task-325a (abort-stop fix): per-event abort short-circuit.
|
|
@@ -4204,80 +4071,8 @@ export class Engine {
|
|
|
4204
4071
|
yield { type: 'turn_end', turnNumber, stopReason, threadId, terminal: true };
|
|
4205
4072
|
|
|
4206
4073
|
// Message durability is handled incrementally before this terminal
|
|
4207
|
-
// branch.
|
|
4208
|
-
|
|
4209
|
-
// PR-L: T2 end-of-turn (asynchronous) reflection. Fires when the
|
|
4210
|
-
// total tool count for this query() exceeds TURN_SUMMARY_THRESHOLD
|
|
4211
|
-
// (8) AND no T1 has actually rewritten the arc yet. Kicks off the
|
|
4212
|
-
// primary-model call without await; the next query()'s
|
|
4213
|
-
// `#applyPendingT2Reflections` carries the result forward.
|
|
4214
|
-
//
|
|
4215
|
-
// Periodic-T1 fix: gate on `t1CollapsesDone === 0`, NOT
|
|
4216
|
-
// `lastT1AtLoopCount === 0`. The catch block of T1 advances
|
|
4217
|
-
// `lastT1AtLoopCount` after a reflector error to avoid
|
|
4218
|
-
// tight-loop retries — but no collapse happened, so T2 should
|
|
4219
|
-
// still be allowed to fall back at end_turn. Fowler-review
|
|
4220
|
-
// critical finding.
|
|
4221
|
-
if (groupReflectionAllowed && queryToolCount > TURN_SUMMARY_THRESHOLD && t1CollapsesDone === 0) {
|
|
4222
|
-
const arcStart = turnStartIdx + 1;
|
|
4223
|
-
const arcEnd = conversationMessages.length - 1;
|
|
4224
|
-
if (arcEnd > arcStart) {
|
|
4225
|
-
const { pairs, assistantText } = extractToolPairsFromRange(
|
|
4226
|
-
conversationMessages, arcStart, arcEnd,
|
|
4227
|
-
);
|
|
4228
|
-
yield {
|
|
4229
|
-
type: 'reflection',
|
|
4230
|
-
turnId: queryTurnId,
|
|
4231
|
-
loopNumber: turnNumber,
|
|
4232
|
-
trigger: 't2',
|
|
4233
|
-
status: 'pending',
|
|
4234
|
-
loopRange: [arcStart, arcEnd],
|
|
4235
|
-
toolCount: pairs.length,
|
|
4236
|
-
};
|
|
4237
|
-
const promise = runT2Reflection({
|
|
4238
|
-
adapter: this.#adapter,
|
|
4239
|
-
model: this.#config.model,
|
|
4240
|
-
originalUserMsg: prompt,
|
|
4241
|
-
toolPairs: pairs,
|
|
4242
|
-
assistantText,
|
|
4243
|
-
language: this.#config.language,
|
|
4244
|
-
signal,
|
|
4245
|
-
onComplete: diagnostic => this.#trace.log?.('tool_reflection', {
|
|
4246
|
-
sessionId: runtimeSessionId, turnId: queryTurnId,
|
|
4247
|
-
trigger: 't2', model: this.#config.model, ...diagnostic,
|
|
4248
|
-
}),
|
|
4249
|
-
});
|
|
4250
|
-
// Detach: never await. The promise outlives this query() and
|
|
4251
|
-
// the next call will pick it up (or use the fallback stub if
|
|
4252
|
-
// it hasn't resolved by then).
|
|
4253
|
-
// PR-L follow-up: latch a synchronously-readable ready flag
|
|
4254
|
-
// and result on the info record so `#applyPendingT2Reflections`
|
|
4255
|
-
// can decide ready-vs-pending without racing microtasks.
|
|
4256
|
-
const info = {
|
|
4257
|
-
promise,
|
|
4258
|
-
loopRange: [arcStart, arcEnd],
|
|
4259
|
-
// Absolute indexes do not survive history-window reconstruction.
|
|
4260
|
-
arcKeys: conversationMessages.slice(arcStart, arcEnd + 1).map(foldingMessageKey),
|
|
4261
|
-
arcRows: conversationMessages.slice(arcStart, arcEnd + 1).map(message => ({
|
|
4262
|
-
role: message.role, id: message.id, _persistedMessageId: message._persistedMessageId,
|
|
4263
|
-
toolCallId: message.toolCallId, toolCalls: message.toolCalls?.map(call => ({ id: call.id })),
|
|
4264
|
-
})),
|
|
4265
|
-
count: pairs.length,
|
|
4266
|
-
originalUserMsg: prompt,
|
|
4267
|
-
originatingTurnId: queryTurnId,
|
|
4268
|
-
causalRootId,
|
|
4269
|
-
executionOrigin,
|
|
4270
|
-
ready: false,
|
|
4271
|
-
result: null,
|
|
4272
|
-
error: null,
|
|
4273
|
-
};
|
|
4274
|
-
promise.then(
|
|
4275
|
-
(v) => { info.ready = true; info.result = v; },
|
|
4276
|
-
(err) => { info.ready = true; info.error = err; },
|
|
4277
|
-
);
|
|
4278
|
-
this.#pendingT2.set(queryNumber, info);
|
|
4279
|
-
}
|
|
4280
|
-
}
|
|
4074
|
+
// branch. No reflection or durable history fold runs here; the separate
|
|
4075
|
+
// post-response compact is scheduled only after turn_close delivery.
|
|
4281
4076
|
|
|
4282
4077
|
break;
|
|
4283
4078
|
}
|
|
@@ -4295,6 +4090,7 @@ export class Engine {
|
|
|
4295
4090
|
effortDecision: snapshotEffortDecision(requestEffortDecision),
|
|
4296
4091
|
requestIdentity,
|
|
4297
4092
|
queryTurnId,
|
|
4093
|
+
webResearch,
|
|
4298
4094
|
historyBeforeSeq: Number.isFinite(persistedQueryUser?.seq) ? persistedQueryUser.seq : parseSeqFromId(persistedQueryUser?.id),
|
|
4299
4095
|
router,
|
|
4300
4096
|
senderVpId,
|
|
@@ -4406,7 +4202,6 @@ export class Engine {
|
|
|
4406
4202
|
// break out of the outer while-loop cleanly once the current
|
|
4407
4203
|
// tool batch finishes reporting.
|
|
4408
4204
|
let abortedDuringTools = false;
|
|
4409
|
-
let batchHasUnseenImages = false;
|
|
4410
4205
|
/** @type {string[]} */
|
|
4411
4206
|
const pendingDupReminders = [];
|
|
4412
4207
|
let terminateAfterDuplicateBatch = false;
|
|
@@ -4602,15 +4397,31 @@ export class Engine {
|
|
|
4602
4397
|
: this.#tools.has(tc.name);
|
|
4603
4398
|
const toolProjectDocPathHints = projectDocPathHintsFromToolCall(tc.name, tc.input);
|
|
4604
4399
|
const readOnlyTool = hasTool ? isReadOnlyTool(this, tc.name, tc.input) : false;
|
|
4605
|
-
const
|
|
4606
|
-
|
|
4400
|
+
const projectDocWrite = hasTool
|
|
4401
|
+
&& projectDocToolCallNeedsWriteReview(tc.name, tc.input, readOnlyTool);
|
|
4402
|
+
const missingProjectDocScopes = projectDocWrite
|
|
4403
|
+
// Shell code is opaque: cwd alone cannot identify its write targets.
|
|
4404
|
+
// Load all omitted rules rather than interpreting command words as paths.
|
|
4405
|
+
? projectDocWriteScopesNeedingReload(projectDocContext, tc.name === 'Bash' ? [] : toolProjectDocPathHints)
|
|
4607
4406
|
: new Set();
|
|
4608
4407
|
// A changed rule source was not present in the request that generated
|
|
4609
4408
|
// this write, even when its scope label is unchanged. Never replay it.
|
|
4610
|
-
const freshProjectDocSource =
|
|
4409
|
+
const freshProjectDocSource = projectDocWrite
|
|
4611
4410
|
? this.#getProjectDocBlock(workDir) : projectDocSource;
|
|
4612
4411
|
const projectDocSourceChanged = freshProjectDocSource !== projectDocSource;
|
|
4613
|
-
const
|
|
4412
|
+
const nextProjectDocPathHints = [...new Set([
|
|
4413
|
+
...projectDocLoadedPathHints, ...toolProjectDocPathHints,
|
|
4414
|
+
])];
|
|
4415
|
+
const reloadedProjectDocContext = missingProjectDocScopes.size > 0 || projectDocSourceChanged
|
|
4416
|
+
? selectProjectDocContext(freshProjectDocSource, {
|
|
4417
|
+
prompt, messages, pathHints: nextProjectDocPathHints,
|
|
4418
|
+
language: this.#config.language || 'en',
|
|
4419
|
+
forcedScopes: [...projectDocContext.selectedScopes, ...missingProjectDocScopes],
|
|
4420
|
+
})
|
|
4421
|
+
: projectDocContext;
|
|
4422
|
+
// Scope bookkeeping (including core sections already shown) is not a
|
|
4423
|
+
// new instruction. Only stop a write when its model-visible rules change.
|
|
4424
|
+
const needsProjectDocReload = reloadedProjectDocContext.text !== projectDocContext.text;
|
|
4614
4425
|
|
|
4615
4426
|
if (abortSkipped) {
|
|
4616
4427
|
output = `Skipped ${tc.name} because the turn was aborted before this tool started.`;
|
|
@@ -4659,22 +4470,15 @@ export class Engine {
|
|
|
4659
4470
|
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, suppressed: true, threadId: this.currentThreadId };
|
|
4660
4471
|
} else if (needsProjectDocReload) {
|
|
4661
4472
|
projectDocSource = freshProjectDocSource;
|
|
4662
|
-
projectDocLoadedPathHints =
|
|
4663
|
-
|
|
4664
|
-
...toolProjectDocPathHints,
|
|
4665
|
-
])];
|
|
4666
|
-
const previouslySelectedProjectDocScopes = new Set(projectDocContext.selectedScopes);
|
|
4667
|
-
projectDocContext = selectProjectDocContext(projectDocSource, {
|
|
4668
|
-
prompt,
|
|
4669
|
-
messages,
|
|
4670
|
-
pathHints: projectDocLoadedPathHints,
|
|
4671
|
-
language: this.#config.language || 'en',
|
|
4672
|
-
forcedScopes: [...previouslySelectedProjectDocScopes, ...missingProjectDocScopes],
|
|
4673
|
-
});
|
|
4473
|
+
projectDocLoadedPathHints = nextProjectDocPathHints;
|
|
4474
|
+
projectDocContext = reloadedProjectDocContext;
|
|
4674
4475
|
const zh = String(this.#config?.language || '').toLowerCase().startsWith('zh');
|
|
4476
|
+
const target = toolProjectDocPathHints.length > 0
|
|
4477
|
+
? (zh ? `路径 ${toolProjectDocPathHints.join(', ')}` : toolProjectDocPathHints.join(', '))
|
|
4478
|
+
: (zh ? `${tc.name} 写操作` : `the ${tc.name} write`);
|
|
4675
4479
|
const notice = zh
|
|
4676
|
-
?
|
|
4677
|
-
: `Project rules were reloaded for ${
|
|
4480
|
+
? `项目规则已针对${target}重新加载。先复核新载入的规则,再重新提交写操作;本次调用未执行。`
|
|
4481
|
+
: `Project rules were reloaded for ${target}. Review the newly loaded rules before resubmitting the write; this call was not executed.`;
|
|
4678
4482
|
promptNotices = [notice];
|
|
4679
4483
|
systemPrompt = buildCurrentSystemPrompt();
|
|
4680
4484
|
output = notice;
|
|
@@ -4930,7 +4734,6 @@ export class Engine {
|
|
|
4930
4734
|
toolName: tc.name,
|
|
4931
4735
|
language: this.#config?.language,
|
|
4932
4736
|
});
|
|
4933
|
-
if (modelImages.length > 0) batchHasUnseenImages = true;
|
|
4934
4737
|
const toolMessage = {
|
|
4935
4738
|
role: 'tool',
|
|
4936
4739
|
toolCallId: tc.id,
|
|
@@ -5072,10 +4875,8 @@ export class Engine {
|
|
|
5072
4875
|
// clean wire shape. Used by `route_forward` to hand off control
|
|
5073
4876
|
// to other VPs without continuing to generate.
|
|
5074
4877
|
//
|
|
5075
|
-
//
|
|
5076
|
-
//
|
|
5077
|
-
// multi-iteration tool loops) and BEFORE the abortedDuringTools
|
|
5078
|
-
// check (so a clean handoff doesn't get reported as 'aborted').
|
|
4878
|
+
// A clean handoff must precede the abortedDuringTools check so it
|
|
4879
|
+
// does not get reported as 'aborted'.
|
|
5079
4880
|
if (endTurnRequested && !toolBatchBarrier) {
|
|
5080
4881
|
if (pendingSubAgentNotifs.length > 0) {
|
|
5081
4882
|
acknowledgePendingNotifications(notifScope, pendingSubAgentNotifs.map(n => n.id));
|
|
@@ -5094,180 +4895,6 @@ export class Engine {
|
|
|
5094
4895
|
break;
|
|
5095
4896
|
}
|
|
5096
4897
|
|
|
5097
|
-
// PR-L: T1 in-turn (synchronous) reflection. Fires every 30 completed
|
|
5098
|
-
// tool loops. A provider response containing many parallel tool calls is
|
|
5099
|
-
// one loop, not many. Generates a markdown reflection
|
|
5100
|
-
// over the assistant+tool arc since the last T1 firing (or
|
|
5101
|
-
// since the user prompt for the first batch) and rewrites that
|
|
5102
|
-
// range to a SINGLE synthetic user message before the next
|
|
5103
|
-
// adapter.stream() runs.
|
|
5104
|
-
//
|
|
5105
|
-
// Loop semantics:
|
|
5106
|
-
// - First interval: arcStartIdx = turnStartIdx + 1.
|
|
5107
|
-
// - Each subsequent interval: arcStartIdx is updated to the slot
|
|
5108
|
-
// right after the just-inserted reflection message; fires
|
|
5109
|
-
// again whenever 30 more tool loops have completed.
|
|
5110
|
-
// - The dedup Set key includes the loop count so each interval
|
|
5111
|
-
// within the same query gets a distinct entry — without
|
|
5112
|
-
// this the second batch would be silently skipped.
|
|
5113
|
-
const completedToolLoops = toolLoopTurns + 1;
|
|
5114
|
-
const t1BatchDue = completedToolLoops - lastT1AtLoopCount
|
|
5115
|
-
>= TOOL_LOOP_REFLECTION_INTERVAL;
|
|
5116
|
-
// Text-only reflection cannot consume visual input. Defer folding until
|
|
5117
|
-
// the primary provider has received this image batch at least once.
|
|
5118
|
-
if (t1BatchDue && !batchHasUnseenImages && !toolBatchBarrier
|
|
5119
|
-
&& !abortedDuringTools && !signal?.aborted) {
|
|
5120
|
-
const t1DedupKey = `${queryNumber}:t1-loop:${completedToolLoops}`;
|
|
5121
|
-
if (this.#reflectedTurns.has(t1DedupKey)) {
|
|
5122
|
-
// Defensive: should never hit since t1BatchDue gates re-entry
|
|
5123
|
-
// and queryNumber namespaces queries. Kept as belt-and-
|
|
5124
|
-
// suspenders against any future external mutation of the
|
|
5125
|
-
// cursor (or a re-entrant query() that this code doesn't
|
|
5126
|
-
// anticipate).
|
|
5127
|
-
} else {
|
|
5128
|
-
this.#reflectedTurns.add(t1DedupKey);
|
|
5129
|
-
const batchStart = arcStartIdx;
|
|
5130
|
-
const batchEnd = conversationMessages.length - 1;
|
|
5131
|
-
try {
|
|
5132
|
-
const { pairs, assistantText } = extractToolPairsFromRange(
|
|
5133
|
-
conversationMessages, batchStart, batchEnd,
|
|
5134
|
-
);
|
|
5135
|
-
yield {
|
|
5136
|
-
type: 'reflection',
|
|
5137
|
-
turnId: queryTurnId,
|
|
5138
|
-
threadId,
|
|
5139
|
-
loopNumber: turnNumber,
|
|
5140
|
-
trigger: 't1',
|
|
5141
|
-
status: 'pending',
|
|
5142
|
-
loopRange: [batchStart, batchEnd],
|
|
5143
|
-
toolCount: pairs.length,
|
|
5144
|
-
};
|
|
5145
|
-
// Observe only this synchronous query-owned call. Do not tap the
|
|
5146
|
-
// shared accounting adapter (also used by children), or feed these
|
|
5147
|
-
// costs into the main loop's token/dispatch budget. Capture raw usage
|
|
5148
|
-
// before reflection diagnostics normalize away cache-inclusion flags.
|
|
5149
|
-
const reflectionAdapter = typeof this.#adapter.call === 'function' ? {
|
|
5150
|
-
call: async params => {
|
|
5151
|
-
let dispatches = 0;
|
|
5152
|
-
let result;
|
|
5153
|
-
let failure;
|
|
5154
|
-
try {
|
|
5155
|
-
result = await this.#adapter.call({
|
|
5156
|
-
...params,
|
|
5157
|
-
onRequestStart: (...args) => {
|
|
5158
|
-
dispatches++;
|
|
5159
|
-
queryProgress.requestStarted();
|
|
5160
|
-
params.onRequestStart?.(...args);
|
|
5161
|
-
},
|
|
5162
|
-
});
|
|
5163
|
-
return result;
|
|
5164
|
-
} catch (error) {
|
|
5165
|
-
failure = error;
|
|
5166
|
-
throw error;
|
|
5167
|
-
} finally {
|
|
5168
|
-
// Native adapters report every dispatch, including a refreshed
|
|
5169
|
-
// credential retry. Legacy adapters without the hook count once.
|
|
5170
|
-
if (dispatches === 0) queryProgress.requestStarted();
|
|
5171
|
-
const usage = result?.usage || failure?.usage;
|
|
5172
|
-
if (usage) queryProgress.usageObserved(usage);
|
|
5173
|
-
}
|
|
5174
|
-
},
|
|
5175
|
-
} : this.#adapter;
|
|
5176
|
-
const { content, durationMs } = await runT1Reflection({
|
|
5177
|
-
adapter: reflectionAdapter,
|
|
5178
|
-
model: this.#config.model,
|
|
5179
|
-
originalUserMsg: prompt,
|
|
5180
|
-
toolPairs: pairs,
|
|
5181
|
-
assistantText,
|
|
5182
|
-
language: this.#config.language,
|
|
5183
|
-
signal,
|
|
5184
|
-
onComplete: diagnostic => this.#trace.log?.('tool_reflection', {
|
|
5185
|
-
sessionId: runtimeSessionId, turnId: queryTurnId,
|
|
5186
|
-
trigger: 't1', model: this.#config.model, ...diagnostic,
|
|
5187
|
-
}),
|
|
5188
|
-
});
|
|
5189
|
-
const { messages: next, reflection: reflectionMessage, foldedMessages } = collapseRangeToReflection(
|
|
5190
|
-
conversationMessages, batchStart, batchEnd, content,
|
|
5191
|
-
);
|
|
5192
|
-
const durableRowsInRange = foldedMessages
|
|
5193
|
-
.some(message => message?._persistedMessageId || message?.id);
|
|
5194
|
-
const persistedReflection = this.#persistFoldedRange(
|
|
5195
|
-
foldedMessages,
|
|
5196
|
-
reflectionMessage,
|
|
5197
|
-
{ sessionId: runtimeSessionId, model: currentModel, executionOrigin },
|
|
5198
|
-
);
|
|
5199
|
-
if (durableRowsInRange && !persistedReflection) {
|
|
5200
|
-
throw new Error('T1 reflection could not publish its durable range replacement');
|
|
5201
|
-
}
|
|
5202
|
-
if (persistedReflection?.id) reflectionMessage._persistedMessageId = persistedReflection.id;
|
|
5203
|
-
// Raw tool rows inside the replacement range are now tombstoned in
|
|
5204
|
-
// durable history. Late async completions must follow the folded
|
|
5205
|
-
// continuation path rather than appending to those stale rows.
|
|
5206
|
-
const foldedToolCallIds = conversationMessages
|
|
5207
|
-
.slice(batchStart, batchEnd + 1)
|
|
5208
|
-
.filter(message => message?.role === 'tool' && message.toolCallId)
|
|
5209
|
-
.map(message => message.toolCallId);
|
|
5210
|
-
for (const toolCallId of foldedToolCallIds) {
|
|
5211
|
-
this.#persistedToolMessages.delete(toolCallId);
|
|
5212
|
-
}
|
|
5213
|
-
conversationMessages.length = 0;
|
|
5214
|
-
for (const m of next) conversationMessages.push(m);
|
|
5215
|
-
// The preserved users precede the reflection. The next arc starts
|
|
5216
|
-
// immediately after it, i.e. at conversationMessages.length
|
|
5217
|
-
// (the next assistant message will land here).
|
|
5218
|
-
arcStartIdx = conversationMessages.length;
|
|
5219
|
-
// Read hints must not imply raw ranges remain in model context.
|
|
5220
|
-
fileReadObservations.clear();
|
|
5221
|
-
lastT1AtLoopCount = completedToolLoops;
|
|
5222
|
-
// Bump the success counter — used by the T2 schedule check
|
|
5223
|
-
// to decide whether T2 still has work to do at end_turn.
|
|
5224
|
-
// Distinct from lastT1AtLoopCount which the catch block
|
|
5225
|
-
// also bumps (but without rewriting history).
|
|
5226
|
-
t1CollapsesDone += 1;
|
|
5227
|
-
yield {
|
|
5228
|
-
type: 'reflection',
|
|
5229
|
-
turnId: queryTurnId,
|
|
5230
|
-
threadId,
|
|
5231
|
-
loopNumber: turnNumber,
|
|
5232
|
-
trigger: 't1',
|
|
5233
|
-
// PR-L bug fix: keep the same loopRange as the `pending` event
|
|
5234
|
-
// so the frontend key stays stable across pending → ready and
|
|
5235
|
-
// the spinner card is replaced in place (no orphan).
|
|
5236
|
-
status: 'ready',
|
|
5237
|
-
loopRange: [batchStart, batchEnd],
|
|
5238
|
-
toolCount: pairs.length,
|
|
5239
|
-
content,
|
|
5240
|
-
durationMs,
|
|
5241
|
-
};
|
|
5242
|
-
} catch (err) {
|
|
5243
|
-
// Best-effort. On failure leave history unchanged so the loop
|
|
5244
|
-
// continues normally — never block the turn.
|
|
5245
|
-
yield {
|
|
5246
|
-
type: 'reflection',
|
|
5247
|
-
turnId: queryTurnId,
|
|
5248
|
-
threadId,
|
|
5249
|
-
loopNumber: turnNumber,
|
|
5250
|
-
trigger: 't1',
|
|
5251
|
-
status: 'error',
|
|
5252
|
-
error: err && err.message || String(err),
|
|
5253
|
-
};
|
|
5254
|
-
// Advance lastT1AtLoopCount past this interval so we don't
|
|
5255
|
-
// tight-loop on a hiccuping reflector. The next attempt is
|
|
5256
|
-
// another 30 tool loops from now, not immediately. arcStartIdx is
|
|
5257
|
-
// left alone because history wasn't rewritten — the tail still
|
|
5258
|
-
// begins where it did. The trade-off: the next batch's
|
|
5259
|
-
// reflection will cover the tools that just failed too,
|
|
5260
|
-
// which is fine (they're still in conversationMessages).
|
|
5261
|
-
//
|
|
5262
|
-
// We do NOT bump t1CollapsesDone — see the variable's
|
|
5263
|
-
// declaration comment. This keeps the T2 fallback path live
|
|
5264
|
-
// when every T1 attempt has errored.
|
|
5265
|
-
lastT1AtLoopCount = completedToolLoops;
|
|
5266
|
-
}
|
|
5267
|
-
yield queryProgress.snapshot(progressIdentity);
|
|
5268
|
-
}
|
|
5269
|
-
}
|
|
5270
|
-
|
|
5271
4898
|
// task-325a: if abort fired between tools, converge now — emit
|
|
5272
4899
|
// the typed `aborted` event + a final turn_end with stopReason
|
|
5273
4900
|
// 'aborted' instead of looping back to a new adapter call.
|
|
@@ -5439,109 +5066,6 @@ export class Engine {
|
|
|
5439
5066
|
/** @returns {import('./mcp.js').MCPManager|null} */
|
|
5440
5067
|
get mcpManager() { return this.#mcpManager; }
|
|
5441
5068
|
|
|
5442
|
-
/**
|
|
5443
|
-
* PR-L — V7 Tool History Reflection helpers.
|
|
5444
|
-
*
|
|
5445
|
-
* `#applyPendingT2Reflections` is called at the start of every
|
|
5446
|
-
* `#runQuery` to carry forward any prior turn's async reflection. It is
|
|
5447
|
-
* a generator so it can yield reflection events to the engine consumer.
|
|
5448
|
-
* Non-blocking: never awaits a pending promise.
|
|
5449
|
-
*
|
|
5450
|
-
* @param {Array} conversationMessages
|
|
5451
|
-
* @param {string} originalUserMsg
|
|
5452
|
-
* @param {{sessionId?: string, model?: string}} context
|
|
5453
|
-
*/
|
|
5454
|
-
async *#applyPendingT2Reflections(conversationMessages, originalUserMsg, context = {}) {
|
|
5455
|
-
if (this.#pendingT2.size === 0) return;
|
|
5456
|
-
// Drain in insertion order (Map preserves it). We process all entries
|
|
5457
|
-
// because the user could send multiple prompts back-to-back before
|
|
5458
|
-
// the engine resumes — each historical turn gets its rewrite.
|
|
5459
|
-
const drained = [...this.#pendingT2.entries()];
|
|
5460
|
-
this.#pendingT2.clear();
|
|
5461
|
-
|
|
5462
|
-
for (const [turnNumber, info] of drained) {
|
|
5463
|
-
const keys = info.arcKeys;
|
|
5464
|
-
if (!Array.isArray(keys) || keys.length === 0) continue;
|
|
5465
|
-
const currentKeys = conversationMessages.map(foldingMessageKey);
|
|
5466
|
-
const startIdx = currentKeys.findIndex((key, index) => key === keys[0]
|
|
5467
|
-
&& keys.every((expected, offset) => currentKeys[index + offset] === expected));
|
|
5468
|
-
const endIdx = startIdx + keys.length - 1;
|
|
5469
|
-
const rangePresent = startIdx >= 0;
|
|
5470
|
-
|
|
5471
|
-
// PR-L follow-up: deterministic readiness check. The info record
|
|
5472
|
-
// carries `ready / result / error` flags that are flipped from the
|
|
5473
|
-
// promise's then/catch handler; reading them here is purely
|
|
5474
|
-
// synchronous bookkeeping — no microtask race.
|
|
5475
|
-
let content;
|
|
5476
|
-
let trigger;
|
|
5477
|
-
let durationMs = 0;
|
|
5478
|
-
if (!info.ready) {
|
|
5479
|
-
// Still in flight — fall back to the exec-log stub. Detach the
|
|
5480
|
-
// unresolved promise so we don't leak it (handlers above already
|
|
5481
|
-
// swallow rejection by routing into info.error).
|
|
5482
|
-
const entries = this.#execLog ? this.#execLog.readTurn(turnNumber) : [];
|
|
5483
|
-
content = buildFallbackStub({ execLogEntries: entries, originalUserMsg: info.originalUserMsg || originalUserMsg });
|
|
5484
|
-
trigger = 't2-fallback';
|
|
5485
|
-
} else if (info.error) {
|
|
5486
|
-
// Promise rejected — leave history unchanged, no event.
|
|
5487
|
-
continue;
|
|
5488
|
-
} else if (info.result && typeof info.result.content === 'string' && info.result.content) {
|
|
5489
|
-
content = info.result.content;
|
|
5490
|
-
trigger = 't2';
|
|
5491
|
-
durationMs = info.result.durationMs || 0;
|
|
5492
|
-
} else {
|
|
5493
|
-
// Resolved but with no usable content — defensively skip.
|
|
5494
|
-
continue;
|
|
5495
|
-
}
|
|
5496
|
-
|
|
5497
|
-
// Rewrite history and publish the same logical replacement to disk.
|
|
5498
|
-
const { messages: next, reflection: reflectionMessage, foldedMessages } = rangePresent
|
|
5499
|
-
? collapseRangeToReflection(conversationMessages, startIdx, endIdx, content)
|
|
5500
|
-
: collapseRangeToReflection(info.arcRows, 0, info.arcRows.length - 1, content);
|
|
5501
|
-
const durableRowsInRange = foldedMessages
|
|
5502
|
-
.some(message => message?._persistedMessageId || message?.id);
|
|
5503
|
-
const persistedReflection = this.#persistFoldedRange(
|
|
5504
|
-
foldedMessages,
|
|
5505
|
-
reflectionMessage,
|
|
5506
|
-
{
|
|
5507
|
-
...context,
|
|
5508
|
-
causalRootId: info.causalRootId || null,
|
|
5509
|
-
executionOrigin: info.executionOrigin === 'route_forward' ? 'route_forward' : null,
|
|
5510
|
-
},
|
|
5511
|
-
);
|
|
5512
|
-
if (durableRowsInRange && !persistedReflection) continue;
|
|
5513
|
-
if (persistedReflection?.id) reflectionMessage._persistedMessageId = persistedReflection.id;
|
|
5514
|
-
// T2 replaces this arc in durable and in-memory history just like T1.
|
|
5515
|
-
// Forget its raw result handles before a late async task can append to a
|
|
5516
|
-
// tombstoned tool row at the next provider boundary.
|
|
5517
|
-
const foldedToolCallIds = foldedMessages
|
|
5518
|
-
.filter(message => message?.role === 'tool' && message.toolCallId)
|
|
5519
|
-
.map(message => message.toolCallId);
|
|
5520
|
-
for (const toolCallId of foldedToolCallIds) {
|
|
5521
|
-
this.#persistedToolMessages.delete(toolCallId);
|
|
5522
|
-
}
|
|
5523
|
-
// History may have evicted the old arc. Publish only its original
|
|
5524
|
-
// durable row IDs; never replace unrelated rows at stale indexes.
|
|
5525
|
-
if (rangePresent) {
|
|
5526
|
-
conversationMessages.length = 0;
|
|
5527
|
-
for (const m of next) conversationMessages.push(m);
|
|
5528
|
-
}
|
|
5529
|
-
|
|
5530
|
-
yield {
|
|
5531
|
-
type: 'reflection',
|
|
5532
|
-
turnId: info.originatingTurnId || null,
|
|
5533
|
-
trigger,
|
|
5534
|
-
status: 'ready',
|
|
5535
|
-
// Wire identity must match the original pending card, even if the
|
|
5536
|
-
// history window shifted or evicted its internal replacement range.
|
|
5537
|
-
loopRange: info.loopRange,
|
|
5538
|
-
toolCount: info.count || 0,
|
|
5539
|
-
content,
|
|
5540
|
-
durationMs,
|
|
5541
|
-
};
|
|
5542
|
-
}
|
|
5543
|
-
}
|
|
5544
|
-
|
|
5545
5069
|
/**
|
|
5546
5070
|
* PR-L — read-only accessor for tests.
|
|
5547
5071
|
*/
|
|
@@ -5696,10 +5220,9 @@ export class Engine {
|
|
|
5696
5220
|
? opts.preview
|
|
5697
5221
|
: (typeof content === 'string' ? content.slice(0, 200) : '[task result]');
|
|
5698
5222
|
const meta = this.#asyncTaskToolMeta.get(taskId) || {};
|
|
5699
|
-
// A completion
|
|
5700
|
-
//
|
|
5701
|
-
//
|
|
5702
|
-
// a raw tool row after the reflection and invalidate the fold.
|
|
5223
|
+
// A completion updates its original tool row when available. Legacy folded
|
|
5224
|
+
// histories instead receive a continuation note; provider-only compaction
|
|
5225
|
+
// never clears this durable handle or changes result ownership.
|
|
5703
5226
|
const hasLiveToolRow = typeof meta.toolCallId === 'string'
|
|
5704
5227
|
&& meta.toolCallId
|
|
5705
5228
|
&& Boolean(this.#persistedToolMessages.get(meta.toolCallId));
|
|
@@ -5725,9 +5248,8 @@ export class Engine {
|
|
|
5725
5248
|
sessionId: delivery.sessionId,
|
|
5726
5249
|
vpId: delivery.vpId,
|
|
5727
5250
|
turnId: delivery.turnId,
|
|
5728
|
-
//
|
|
5729
|
-
//
|
|
5730
|
-
// later drain cannot infer stale liveness from an unrelated row.
|
|
5251
|
+
// Capture durable row ownership at completion time, independently of
|
|
5252
|
+
// any provider-only compacted request copy.
|
|
5731
5253
|
folded: !hasLiveToolRow,
|
|
5732
5254
|
});
|
|
5733
5255
|
} else {
|
|
@@ -5765,8 +5287,7 @@ export class Engine {
|
|
|
5765
5287
|
: (this.#sessionId || null),
|
|
5766
5288
|
vpId: typeof meta.vpId === 'string' && meta.vpId ? meta.vpId : (this.#vpId || null),
|
|
5767
5289
|
// query() exposes the wire turn id while the task is registered.
|
|
5768
|
-
//
|
|
5769
|
-
// original tool row from the in-memory arc.
|
|
5290
|
+
// Keep the originating turn even for legacy missing/folded tool rows.
|
|
5770
5291
|
turnId: typeof meta.turnId === 'string' && meta.turnId
|
|
5771
5292
|
? meta.turnId
|
|
5772
5293
|
: (this.#currentQueryTurnId || null),
|