@yeaft/webchat-agent 1.0.325 → 1.0.328
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 +164 -139
- 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/engine.js +175 -68
- package/yeaft/memory/ams.js +18 -8
- package/yeaft/tasks/manager.js +48 -11
- package/yeaft/tools/bash.js +42 -11
- package/yeaft/tools/process-runner.js +24 -10
- package/yeaft/tools/types.js +5 -0
- package/yeaft/web-bridge.js +5 -6
|
Binary file
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -31,6 +31,7 @@ import { evaluateCompactTriggers } from './compact/triggers.js';
|
|
|
31
31
|
import { archiveTurn } from './archive/turn-archive.js';
|
|
32
32
|
import { archiveToolResults } from './archive/tool-results.js';
|
|
33
33
|
import { readSummary as readScopeSummary } from './memory/store.js';
|
|
34
|
+
import { ActiveMemorySet } from './memory/ams.js';
|
|
34
35
|
import { runAdjust } from './memory/adjust.js';
|
|
35
36
|
import { cleanMemoryPromptText, isMemoryPromptRelevant } from './memory/prompt-cleanup.js';
|
|
36
37
|
import { isVpSeedBackfillStub } from './memory/seed-backfill.js';
|
|
@@ -44,7 +45,7 @@ import { lookupModelLimitSync } from './llm/models-dev.js';
|
|
|
44
45
|
import { countTurns } from './turn-utils.js';
|
|
45
46
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
46
47
|
import { resolveThinking } from './router/thinking.js';
|
|
47
|
-
import { approxTokens } from './memory/budget.js';
|
|
48
|
+
import { approxTokens, computeBudget } from './memory/budget.js';
|
|
48
49
|
import { COLLAB_TOOL_POLICY, isToolErrorOutput, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
|
|
49
50
|
import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
|
|
50
51
|
import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
|
|
@@ -365,7 +366,7 @@ export function shouldAllowGroupReflection({
|
|
|
365
366
|
* @typedef {{ type: 'turn_start', turnNumber: number }} TurnStartEvent
|
|
366
367
|
* @typedef {{ type: 'turn_end', turnNumber: number, stopReason: string, terminal?: boolean }} TurnEndEvent
|
|
367
368
|
* @typedef {{ type: 'tool_start', id: string, name: string, input: object }} ToolStartEvent
|
|
368
|
-
* @typedef {{ type: 'tool_end', id: string, name: string, output: string, isError: boolean }} ToolEndEvent
|
|
369
|
+
* @typedef {{ type: 'tool_end', id: string, name: string, output: string, isError: boolean, skipped?: boolean }} ToolEndEvent
|
|
369
370
|
* @typedef {{ type: 'consolidate', archivedCount: number, extractedCount: number }} ConsolidateEvent
|
|
370
371
|
* @typedef {{ type: 'recall', entryCount: number, cached: boolean }} RecallEvent
|
|
371
372
|
* @typedef {{ type: 'fallback', from: string, to: string, reason: string }} FallbackEvent
|
|
@@ -399,7 +400,7 @@ export function shouldAllowGroupReflection({
|
|
|
399
400
|
* @param {{
|
|
400
401
|
* sessionId?: string|null,
|
|
401
402
|
* ownVpId?: string|null,
|
|
402
|
-
* summaries: { user?: string, session?: string, vp?: string, topics?: Array<{scope:string, summary:string}> }
|
|
403
|
+
* summaries: { user?: string, session?: string, vp?: string, topics?: Array<{scope:string, summary:string}>, relatedSessions?: Array<{sessionId:string, summary:string}> }
|
|
403
404
|
* }} args
|
|
404
405
|
* @returns {Array<{scope: string, summary: string}>}
|
|
405
406
|
*/
|
|
@@ -445,6 +446,18 @@ export function buildResidentEntries(args) {
|
|
|
445
446
|
if (args.sessionId && args.ownVpId && vpSummary && !isVpSeedBackfillStub(vpSummary)) {
|
|
446
447
|
out.push({ scope: `sessions/${args.sessionId}/vp/${args.ownVpId}`, summary: vpSummary });
|
|
447
448
|
}
|
|
449
|
+
// Related Session experience is useful but lower-priority than every memory
|
|
450
|
+
// source owned by the active Session/VP. Append it last so the resident
|
|
451
|
+
// budget can never evict current context in favour of historical prose.
|
|
452
|
+
if (Array.isArray(summaries.relatedSessions)) {
|
|
453
|
+
for (const related of summaries.relatedSessions) {
|
|
454
|
+
const relatedSessionId = typeof related?.sessionId === 'string' ? related.sessionId.trim() : '';
|
|
455
|
+
const summary = cleanMemoryPromptText(related?.summary);
|
|
456
|
+
if (relatedSessionId && relatedSessionId !== args.sessionId && summary) {
|
|
457
|
+
out.push({ scope: `sessions/${relatedSessionId}`, summary });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
448
461
|
return out;
|
|
449
462
|
}
|
|
450
463
|
|
|
@@ -452,6 +465,11 @@ function isZhRuntimeLanguage(language) {
|
|
|
452
465
|
return String(language || '').toLowerCase().startsWith('zh');
|
|
453
466
|
}
|
|
454
467
|
|
|
468
|
+
function sessionIdFromMemoryScope(scope) {
|
|
469
|
+
const match = /^(?:sessions|session|group)\/([^/]+)$/.exec(String(scope || ''));
|
|
470
|
+
return match ? match[1] : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
455
473
|
function resolveMemoryRecallLimit(config) {
|
|
456
474
|
const raw = config?.memoryRecallLimit ?? config?.dreamMemoryRecallLimit;
|
|
457
475
|
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_MEMORY_RECALL_LIMIT;
|
|
@@ -476,6 +494,7 @@ function loadedResidentDebugEntries(entries) {
|
|
|
476
494
|
kind: 'summary',
|
|
477
495
|
score: null,
|
|
478
496
|
tags: [],
|
|
497
|
+
category: entry.category || null,
|
|
479
498
|
body: entry.summary || '',
|
|
480
499
|
})).filter(entry => entry.body);
|
|
481
500
|
}
|
|
@@ -910,13 +929,17 @@ export class Engine {
|
|
|
910
929
|
* dream tick (Phase 6) is what populates these; on a fresh install they
|
|
911
930
|
* all return ''.
|
|
912
931
|
*
|
|
913
|
-
* @param {{sessionId?: string, vpId?: string, language?: string, topicScopes?: string[]}} ctx
|
|
914
|
-
* @returns {Promise<{user:string, session:string, vp:string, topics:Array<{scope:string, summary:string}>}>}
|
|
932
|
+
* @param {{sessionId?: string, vpId?: string, language?: string, topicScopes?: string[], relatedSessionIds?: string[]}} ctx
|
|
933
|
+
* @returns {Promise<{user:string, session:string, vp:string, topics:Array<{scope:string, summary:string}>, relatedSessions:Array<{sessionId:string, summary:string}>}>}
|
|
915
934
|
*/
|
|
916
|
-
async #loadLayerASummaries({ sessionId, vpId, language, topicScopes } = {}) {
|
|
917
|
-
if (!this.#yeaftDir) return { user: '', session: '', vp: '', topics: [] };
|
|
935
|
+
async #loadLayerASummaries({ sessionId, vpId, language, topicScopes, relatedSessionIds } = {}) {
|
|
936
|
+
if (!this.#yeaftDir) return { user: '', session: '', vp: '', topics: [], relatedSessions: [] };
|
|
918
937
|
const memoryRoot = `${this.#yeaftDir}/memory`;
|
|
919
938
|
const topicScopeList = Array.isArray(topicScopes) ? topicScopes.slice(0, 12) : [];
|
|
939
|
+
const relatedIds = Array.from(new Set((Array.isArray(relatedSessionIds) ? relatedSessionIds : [])
|
|
940
|
+
.filter(id => typeof id === 'string' && id.trim() && id.trim() !== sessionId)
|
|
941
|
+
.map(id => id.trim())))
|
|
942
|
+
.slice(0, 8);
|
|
920
943
|
const tasks = [
|
|
921
944
|
readScopeSummary({ kind: 'user' }, { root: memoryRoot, language }).catch(() => ''),
|
|
922
945
|
sessionId
|
|
@@ -926,10 +949,18 @@ export class Engine {
|
|
|
926
949
|
? readScopeSummary({ kind: 'session-vp', sessionId, id: vpId }, { root: memoryRoot, language }).catch(() => '')
|
|
927
950
|
: Promise.resolve(''),
|
|
928
951
|
Promise.all(topicScopeList.map(scope => readTopicSummary(scope, { root: memoryRoot, language }))),
|
|
952
|
+
Promise.all(relatedIds.map(async relatedSessionId => ({
|
|
953
|
+
sessionId: relatedSessionId,
|
|
954
|
+
summary: await readScopeSummary(
|
|
955
|
+
{ kind: 'session', id: relatedSessionId },
|
|
956
|
+
{ root: memoryRoot, language },
|
|
957
|
+
).catch(() => ''),
|
|
958
|
+
}))),
|
|
929
959
|
];
|
|
930
|
-
const [user, session, vp, topicsRaw] = await Promise.all(tasks);
|
|
960
|
+
const [user, session, vp, topicsRaw, relatedSessionsRaw] = await Promise.all(tasks);
|
|
931
961
|
const topics = (topicsRaw || []).filter(t => t && t.summary);
|
|
932
|
-
|
|
962
|
+
const relatedSessions = (relatedSessionsRaw || []).filter(entry => entry && entry.summary);
|
|
963
|
+
return { user: user || '', session: session || '', vp: vp || '', topics, relatedSessions };
|
|
933
964
|
}
|
|
934
965
|
|
|
935
966
|
async #loadSessionTopicLabels(sessionId, limit = 8) {
|
|
@@ -967,27 +998,42 @@ export class Engine {
|
|
|
967
998
|
* } | null}
|
|
968
999
|
*/
|
|
969
1000
|
#prepareAms(args) {
|
|
970
|
-
if (!this.#amsRegistry) return null;
|
|
971
1001
|
const sessionKey = args.sessionId || 'default';
|
|
972
1002
|
const ownVpId = args.ownVpId || null;
|
|
973
|
-
|
|
1003
|
+
// Some read-only Engine entry points (notably sub-agents) intentionally do
|
|
1004
|
+
// not own the parent's persistent registry. They still need the single AMS
|
|
1005
|
+
// render outlet, otherwise FTS recall succeeds and then vanishes before the
|
|
1006
|
+
// prompt. Use an isolated per-query AMS in that case: it preserves budgets,
|
|
1007
|
+
// cleanup, and dedupe without sharing mutable parent state or writing disk.
|
|
1008
|
+
const ams = this.#amsRegistry
|
|
1009
|
+
? this.#amsRegistry.getOrCreate(sessionKey, { ownVpId })
|
|
1010
|
+
: new ActiveMemorySet({
|
|
1011
|
+
ownVpId,
|
|
1012
|
+
budget: computeBudget(this.#config?.maxContextTokens),
|
|
1013
|
+
});
|
|
974
1014
|
|
|
975
1015
|
// Prime #adjustRanBySession from disk-hydrated state on first access:
|
|
976
1016
|
// a reactivated group resumes with whatever adjustRanThisSession bit
|
|
977
1017
|
// it had on disconnect, so we don't burn a fresh adjust on every
|
|
978
1018
|
// reload. Once set true in this session we never clear it.
|
|
979
|
-
if (
|
|
1019
|
+
if (this.#amsRegistry
|
|
1020
|
+
&& !this.#adjustRanBySession.has(sessionKey)
|
|
980
1021
|
&& this.#amsRegistry.adjustRanThisSession(sessionKey)) {
|
|
981
1022
|
this.#adjustRanBySession.set(sessionKey, true);
|
|
982
1023
|
}
|
|
983
1024
|
|
|
984
1025
|
// (a) Resident: rebuild from the same scope summaries the worker
|
|
985
1026
|
// prompt is already going to see.
|
|
1027
|
+
const relatedSessionIds = new Set((args.summaries?.relatedSessions || [])
|
|
1028
|
+
.map(entry => entry?.sessionId)
|
|
1029
|
+
.filter(Boolean));
|
|
986
1030
|
const residentEntries = buildResidentEntries({
|
|
987
1031
|
sessionId: args.sessionId,
|
|
988
1032
|
ownVpId,
|
|
989
1033
|
summaries: args.summaries || {},
|
|
990
|
-
})
|
|
1034
|
+
}).map(entry => relatedSessionIds.has(entry.scope.replace(/^sessions\//, ''))
|
|
1035
|
+
? { ...entry, category: 'experience' }
|
|
1036
|
+
: { ...entry, category: 'memory' });
|
|
991
1037
|
ams.setResident(residentEntries);
|
|
992
1038
|
|
|
993
1039
|
// (b) onDemand: replace with this turn's FTS hits.
|
|
@@ -996,7 +1042,11 @@ export class Engine {
|
|
|
996
1042
|
|
|
997
1043
|
// (c) Snapshot — render the AMS layers as a single prompt block.
|
|
998
1044
|
const snapshot = ams.snapshot({ userMsg: args.userMsg || '' });
|
|
999
|
-
const snapshotBlock = this.#renderAmsSnapshot(
|
|
1045
|
+
const snapshotBlock = this.#renderAmsSnapshot(
|
|
1046
|
+
snapshot,
|
|
1047
|
+
this.#config.language || 'en',
|
|
1048
|
+
args.sessionId || null,
|
|
1049
|
+
);
|
|
1000
1050
|
|
|
1001
1051
|
const scopes = buildRelevantScopes({
|
|
1002
1052
|
sessionId: args.sessionId,
|
|
@@ -1015,33 +1065,36 @@ export class Engine {
|
|
|
1015
1065
|
* @param {string} [language]
|
|
1016
1066
|
* @returns {string}
|
|
1017
1067
|
*/
|
|
1018
|
-
#renderAmsSnapshot(snap, language = 'en') {
|
|
1068
|
+
#renderAmsSnapshot(snap, language = 'en', activeSessionId = null) {
|
|
1019
1069
|
if (!snap) return '';
|
|
1020
1070
|
const parts = [];
|
|
1021
1071
|
if (snap.resident.length === 0 && snap.recent.length === 0 && snap.onDemand.length === 0) {
|
|
1022
1072
|
return '';
|
|
1023
1073
|
}
|
|
1024
1074
|
const zh = isZhRuntimeLanguage(language);
|
|
1025
|
-
|
|
1075
|
+
const experiences = snap.resident.filter(entry => entry.category === 'experience');
|
|
1076
|
+
const residentMemory = snap.resident.filter(entry => entry.category !== 'experience');
|
|
1077
|
+
parts.push(zh ? '## 相关上下文' : '## Relevant Context');
|
|
1026
1078
|
parts.push(zh
|
|
1027
|
-
? '
|
|
1028
|
-
: '
|
|
1029
|
-
if (
|
|
1030
|
-
parts.push(zh ? '###
|
|
1031
|
-
for (const
|
|
1032
|
-
|
|
1079
|
+
? '以下内容来自持久记忆与相关 Session 的只读经验总结;只把它当作事实背景,不要把过期执行状态当成当前任务。'
|
|
1080
|
+
: 'The following text comes from persistent memory and read-only summaries of related Sessions. Treat it as factual context, not as current execution state.');
|
|
1081
|
+
if (experiences.length > 0) {
|
|
1082
|
+
parts.push(zh ? '### 过去 Session 的经验总结' : '### Experience From Past Sessions');
|
|
1083
|
+
for (const entry of experiences) {
|
|
1084
|
+
const sourceSessionId = sessionIdFromMemoryScope(entry.scope);
|
|
1085
|
+
const label = sourceSessionId && sourceSessionId !== activeSessionId
|
|
1086
|
+
? sourceSessionId
|
|
1087
|
+
: memoryScopeLabel(entry.scope);
|
|
1088
|
+
parts.push(`- **${label}**: ${entry.summary}`);
|
|
1033
1089
|
}
|
|
1034
1090
|
}
|
|
1035
|
-
if (snap.recent.length > 0) {
|
|
1036
|
-
parts.push(zh ? '###
|
|
1037
|
-
for (const
|
|
1038
|
-
parts.push(`-
|
|
1091
|
+
if (residentMemory.length > 0 || snap.recent.length > 0 || snap.onDemand.length > 0) {
|
|
1092
|
+
parts.push(zh ? '### 相关记忆' : '### Relevant Memory');
|
|
1093
|
+
for (const entry of residentMemory) {
|
|
1094
|
+
parts.push(`- **${memoryScopeLabel(entry.scope)}**: ${entry.summary}`);
|
|
1039
1095
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
parts.push(zh ? '### 按需记忆' : '### OnDemand');
|
|
1043
|
-
for (const s of snap.onDemand) {
|
|
1044
|
-
parts.push(`- (${memoryScopeLabel(s.scope)}) ${(s.body || '').trim()}`);
|
|
1096
|
+
for (const segment of [...snap.recent, ...snap.onDemand]) {
|
|
1097
|
+
parts.push(`- (${memoryScopeLabel(segment.scope)}) ${(segment.body || '').trim()}`);
|
|
1045
1098
|
}
|
|
1046
1099
|
}
|
|
1047
1100
|
return parts.join('\n');
|
|
@@ -1339,6 +1392,7 @@ export class Engine {
|
|
|
1339
1392
|
// can mark "after this batch, end the turn — do NOT call adapter
|
|
1340
1393
|
// again". Honored at the top of the tool-loop continuation.
|
|
1341
1394
|
requestEndTurn: vpCtx?.requestEndTurn,
|
|
1395
|
+
requestToolBatchBarrier: vpCtx?.requestToolBatchBarrier,
|
|
1342
1396
|
// Result-producing async-task ownership hook. Tools such as SpawnAgent
|
|
1343
1397
|
// call this with the new `task.id` so the engine keeps the current query
|
|
1344
1398
|
// parked at end_turn until the result arrives. Persistent background
|
|
@@ -2145,6 +2199,7 @@ export class Engine {
|
|
|
2145
2199
|
: (typeof senderVpId === 'string' ? senderVpId : undefined),
|
|
2146
2200
|
language: this.#config.language || 'en',
|
|
2147
2201
|
topicScopes: topicScopesForResident,
|
|
2202
|
+
relatedSessionIds: projectSessionIds,
|
|
2148
2203
|
});
|
|
2149
2204
|
|
|
2150
2205
|
// ─── AMS: populate + snapshot ───────────────────────────────
|
|
@@ -2210,7 +2265,9 @@ export class Engine {
|
|
|
2210
2265
|
|
|
2211
2266
|
const projectDoc = this.#getProjectDocBlock(workDir);
|
|
2212
2267
|
const activeTasks = this.#taskManager
|
|
2213
|
-
? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId
|
|
2268
|
+
? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId, {
|
|
2269
|
+
language: this.#config.language || 'en',
|
|
2270
|
+
})
|
|
2214
2271
|
: '';
|
|
2215
2272
|
let resolvedSkillContent = '';
|
|
2216
2273
|
let resolvedSkills = [];
|
|
@@ -3636,6 +3693,8 @@ export class Engine {
|
|
|
3636
3693
|
}
|
|
3637
3694
|
|
|
3638
3695
|
// Execute tool calls and feed results back
|
|
3696
|
+
/** @type {{ kind?: string, message?: string, sourceToolCallId?: string, sourceToolName?: string } | null} */
|
|
3697
|
+
let toolBatchBarrier = null;
|
|
3639
3698
|
let currentToolCallForAsyncTask = null;
|
|
3640
3699
|
// task-707: requestEndTurn is a per-batch closure that lets a tool
|
|
3641
3700
|
// signal "end this turn after the current batch — no adapter retry".
|
|
@@ -3667,6 +3726,17 @@ export class Engine {
|
|
|
3667
3726
|
endTurnRequested = reason || { kind: 'tool_handoff' };
|
|
3668
3727
|
}
|
|
3669
3728
|
},
|
|
3729
|
+
requestToolBatchBarrier: (reason) => {
|
|
3730
|
+
if (toolBatchBarrier != null) return;
|
|
3731
|
+
const detail = reason && typeof reason === 'object'
|
|
3732
|
+
? { ...reason }
|
|
3733
|
+
: { message: String(reason || 'A preceding tool result invalidated the remaining batch.') };
|
|
3734
|
+
toolBatchBarrier = {
|
|
3735
|
+
...detail,
|
|
3736
|
+
sourceToolCallId: currentToolCallForAsyncTask?.id || null,
|
|
3737
|
+
sourceToolName: currentToolCallForAsyncTask?.name || null,
|
|
3738
|
+
};
|
|
3739
|
+
},
|
|
3670
3740
|
});
|
|
3671
3741
|
|
|
3672
3742
|
// task-325a: track whether we aborted mid tool-loop so we can
|
|
@@ -3681,11 +3751,14 @@ export class Engine {
|
|
|
3681
3751
|
// that's already running (the signal is passed in, tools decide
|
|
3682
3752
|
// themselves whether to bail early), but we stop dispatching
|
|
3683
3753
|
// any remaining tools the moment abort fires.
|
|
3684
|
-
if (signal?.aborted) {
|
|
3754
|
+
if (signal?.aborted && !toolBatchBarrier) {
|
|
3685
3755
|
abortedDuringTools = true;
|
|
3686
3756
|
break;
|
|
3687
3757
|
}
|
|
3758
|
+
if (signal?.aborted) abortedDuringTools = true;
|
|
3688
3759
|
|
|
3760
|
+
const activeToolBatchBarrier = toolBatchBarrier;
|
|
3761
|
+
const skipped = activeToolBatchBarrier != null;
|
|
3689
3762
|
const toolStartTime = Date.now();
|
|
3690
3763
|
|
|
3691
3764
|
// PR-L: duplicate-call detection. If this exact (toolName,
|
|
@@ -3696,25 +3769,27 @@ export class Engine {
|
|
|
3696
3769
|
// assistant(tool_use) → user(tool_result, …) pairing demanded
|
|
3697
3770
|
// by the Anthropic / OpenAI Responses APIs stays intact. We
|
|
3698
3771
|
// don't block the call — the LLM still decides.
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
argsHash: dupHash,
|
|
3709
|
-
currentTurn: queryNumber,
|
|
3710
|
-
lookbackTurns: 2,
|
|
3711
|
-
});
|
|
3712
|
-
if (dupInfo.count + 1 >= DUP_TOOL_THRESHOLD) {
|
|
3713
|
-
pendingDupReminders.push(buildDuplicateReminder({
|
|
3772
|
+
if (!skipped) {
|
|
3773
|
+
const dupHash = argsHashOf(tc.input);
|
|
3774
|
+
// PR-L follow-up: lookback is by user-conversation turn
|
|
3775
|
+
// (`queryNumber`), NOT by inner adapter loop iteration. Each call
|
|
3776
|
+
// to query() bumps queryNumber once, so "last 2 turns" means the
|
|
3777
|
+
// current user turn + the previous two user turns — the natural
|
|
3778
|
+
// semantic for "the model is stuck in a loop across the
|
|
3779
|
+
// conversation."
|
|
3780
|
+
const dupInfo = this.#execLog.dupInfo({
|
|
3714
3781
|
toolName: tc.name,
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3782
|
+
argsHash: dupHash,
|
|
3783
|
+
currentTurn: queryNumber,
|
|
3784
|
+
lookbackTurns: 2,
|
|
3785
|
+
});
|
|
3786
|
+
if (dupInfo.count + 1 >= DUP_TOOL_THRESHOLD) {
|
|
3787
|
+
pendingDupReminders.push(buildDuplicateReminder({
|
|
3788
|
+
toolName: tc.name,
|
|
3789
|
+
count: dupInfo.count + 1,
|
|
3790
|
+
lastResultBrief: dupInfo.lastResultBrief,
|
|
3791
|
+
}));
|
|
3792
|
+
}
|
|
3718
3793
|
}
|
|
3719
3794
|
|
|
3720
3795
|
let output;
|
|
@@ -3722,18 +3797,40 @@ export class Engine {
|
|
|
3722
3797
|
let isError = false;
|
|
3723
3798
|
let toolErrorOutput = null;
|
|
3724
3799
|
let fatalToolError = null;
|
|
3725
|
-
currentToolCallForAsyncTask =
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3800
|
+
currentToolCallForAsyncTask = skipped
|
|
3801
|
+
? null
|
|
3802
|
+
: {
|
|
3803
|
+
id: tc.id,
|
|
3804
|
+
name: tc.name,
|
|
3805
|
+
threadId: runtimeThreadId,
|
|
3806
|
+
};
|
|
3730
3807
|
|
|
3731
3808
|
// Resolve tool: prefer ToolRegistry, fallback to legacy #tools Map
|
|
3732
3809
|
const hasTool = this.#toolRegistry
|
|
3733
3810
|
? this.#toolRegistry.isAllowed(tc.name, { collabToolPolicy: effectiveCollabToolPolicy })
|
|
3734
3811
|
: this.#tools.has(tc.name);
|
|
3735
3812
|
|
|
3736
|
-
if (
|
|
3813
|
+
if (skipped) {
|
|
3814
|
+
const source = activeToolBatchBarrier.sourceToolName || 'a preceding tool';
|
|
3815
|
+
const sourceId = activeToolBatchBarrier.sourceToolCallId
|
|
3816
|
+
? ` (${activeToolBatchBarrier.sourceToolCallId})`
|
|
3817
|
+
: '';
|
|
3818
|
+
output = [
|
|
3819
|
+
`Skipped ${tc.name} because ${source}${sourceId} invalidated the remaining tool batch.`,
|
|
3820
|
+
activeToolBatchBarrier.message || 'Review the preceding tool result before deciding whether to retry this call.',
|
|
3821
|
+
'This tool was not executed. Submit it again only after reviewing the preceding result.',
|
|
3822
|
+
].join('\n');
|
|
3823
|
+
isError = true;
|
|
3824
|
+
yield {
|
|
3825
|
+
type: 'tool_end',
|
|
3826
|
+
id: tc.id,
|
|
3827
|
+
name: tc.name,
|
|
3828
|
+
output,
|
|
3829
|
+
isError: true,
|
|
3830
|
+
skipped: true,
|
|
3831
|
+
threadId: this.currentThreadId,
|
|
3832
|
+
};
|
|
3833
|
+
} else if (!hasTool) {
|
|
3737
3834
|
output = `Error: unknown tool "${tc.name}"`;
|
|
3738
3835
|
isError = true;
|
|
3739
3836
|
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
|
|
@@ -3816,13 +3913,14 @@ export class Engine {
|
|
|
3816
3913
|
durationMs: toolDurationMs,
|
|
3817
3914
|
isError,
|
|
3818
3915
|
toolOutput: output,
|
|
3916
|
+
...(skipped ? { skipped: true } : {}),
|
|
3819
3917
|
...(displayImages.length > 0 ? { displayImageCount: displayImages.length } : {}),
|
|
3820
3918
|
};
|
|
3821
3919
|
|
|
3822
3920
|
// 2026-05-13: feed the per-tool counters. Stays best-effort — a
|
|
3823
3921
|
// stats sink that throws shouldn't crash the engine. `record`
|
|
3824
3922
|
// already swallows internal write errors.
|
|
3825
|
-
if (this.#toolStats && typeof this.#toolStats.record === 'function') {
|
|
3923
|
+
if (!skipped && this.#toolStats && typeof this.#toolStats.record === 'function') {
|
|
3826
3924
|
try {
|
|
3827
3925
|
this.#toolStats.record({
|
|
3828
3926
|
name: tc.name,
|
|
@@ -3841,6 +3939,7 @@ export class Engine {
|
|
|
3841
3939
|
toolOutput: output,
|
|
3842
3940
|
durationMs: toolDurationMs,
|
|
3843
3941
|
isError,
|
|
3942
|
+
skipped,
|
|
3844
3943
|
});
|
|
3845
3944
|
|
|
3846
3945
|
// Append only the bounded copy to the model message history. Raw
|
|
@@ -3876,14 +3975,16 @@ export class Engine {
|
|
|
3876
3975
|
// user-conversation turn), not the inner loop's turnNumber.
|
|
3877
3976
|
// Aligns exec-log layout with dup detection lookback and the
|
|
3878
3977
|
// T2 fallback-stub readTurn() call below.
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3978
|
+
if (!skipped) {
|
|
3979
|
+
this.#execLog.append(queryNumber, buildExecLogEntry({
|
|
3980
|
+
loopIdx: queryToolCount,
|
|
3981
|
+
toolName: tc.name,
|
|
3982
|
+
args: tc.input,
|
|
3983
|
+
output,
|
|
3984
|
+
isError,
|
|
3985
|
+
}));
|
|
3986
|
+
queryToolCount += 1;
|
|
3987
|
+
}
|
|
3887
3988
|
if (fatalToolError) throw fatalToolError;
|
|
3888
3989
|
}
|
|
3889
3990
|
|
|
@@ -3895,6 +3996,11 @@ export class Engine {
|
|
|
3895
3996
|
conversationMessages.push({ role: 'user', content: reminder });
|
|
3896
3997
|
}
|
|
3897
3998
|
|
|
3999
|
+
// A batch barrier deliberately returns control to the provider. Any
|
|
4000
|
+
// handoff requested by an earlier call belongs to the invalidated plan
|
|
4001
|
+
// and must not leak into a later provider-generated batch.
|
|
4002
|
+
if (toolBatchBarrier) endTurnRequested = null;
|
|
4003
|
+
|
|
3898
4004
|
// task-707: tool-callable end-turn signal. If a tool in this batch
|
|
3899
4005
|
// called toolCtx.requestEndTurn(reason), break out of the outer
|
|
3900
4006
|
// while-loop now — DON'T call adapter.stream() again. The
|
|
@@ -3907,7 +4013,7 @@ export class Engine {
|
|
|
3907
4013
|
// collapse the arc into a summary that's only valuable across
|
|
3908
4014
|
// multi-iteration tool loops) and BEFORE the abortedDuringTools
|
|
3909
4015
|
// check (so a clean handoff doesn't get reported as 'aborted').
|
|
3910
|
-
if (endTurnRequested) {
|
|
4016
|
+
if (endTurnRequested && !toolBatchBarrier) {
|
|
3911
4017
|
if (pendingSubAgentNotifs.length > 0) {
|
|
3912
4018
|
acknowledgePendingNotifications(notifScope, pendingSubAgentNotifs.map(n => n.id));
|
|
3913
4019
|
}
|
|
@@ -3945,7 +4051,8 @@ export class Engine {
|
|
|
3945
4051
|
// batch within the same query gets a distinct entry — without
|
|
3946
4052
|
// this the second batch would be silently skipped.
|
|
3947
4053
|
const t1BatchDue = queryToolCount - lastT1AtToolCount >= TOOL_BATCH_SIZE;
|
|
3948
|
-
if (groupReflectionAllowed && t1BatchDue && !
|
|
4054
|
+
if (groupReflectionAllowed && t1BatchDue && !toolBatchBarrier
|
|
4055
|
+
&& !abortedDuringTools && !signal?.aborted) {
|
|
3949
4056
|
const t1DedupKey = `${queryNumber}:t1:${queryToolCount}`;
|
|
3950
4057
|
if (this.#reflectedTurns.has(t1DedupKey)) {
|
|
3951
4058
|
// Defensive: should never hit since t1BatchDue gates re-entry
|
package/yeaft/memory/ams.js
CHANGED
|
@@ -29,14 +29,14 @@ const RECENT_DEFAULT_CAPACITY = 64;
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* @typedef {object} AmsLayers
|
|
32
|
-
* @property {Map<string, string>}
|
|
32
|
+
* @property {Map<string, { summary: string, category?: string }>} resident
|
|
33
33
|
* @property {Array<{ id: string, seg: import('./segment.js').Segment, ts: number }>} recent
|
|
34
34
|
* @property {Map<string, import('./segment.js').Segment>} onDemand
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @typedef {object} AmsSnapshot
|
|
39
|
-
* @property {Array<{ scope: string, summary: string }>} resident
|
|
39
|
+
* @property {Array<{ scope: string, summary: string, category?: string }>} resident
|
|
40
40
|
* @property {import('./segment.js').Segment[]} recent
|
|
41
41
|
* @property {import('./segment.js').Segment[]} onDemand
|
|
42
42
|
* @property {{ resident: number, recent: number, onDemand: number, total: number }} usage
|
|
@@ -55,8 +55,8 @@ export class ActiveMemorySet {
|
|
|
55
55
|
this.ownVpId = opts.ownVpId || null;
|
|
56
56
|
this.budget = opts.budget;
|
|
57
57
|
this.recentCapacity = opts.recentCapacity || RECENT_DEFAULT_CAPACITY;
|
|
58
|
-
/** @type {Map<string, string>} */
|
|
59
|
-
this._resident = new Map(); // scope →
|
|
58
|
+
/** @type {Map<string, { summary: string, category?: string }>} */
|
|
59
|
+
this._resident = new Map(); // scope → prompt-facing summary metadata
|
|
60
60
|
/** @type {Map<string, { seg: import('./segment.js').Segment, ts: number }>} */
|
|
61
61
|
this._recent = new Map(); // segId → entry (insertion-order is LRU order)
|
|
62
62
|
/** @type {Map<string, import('./segment.js').Segment>} */
|
|
@@ -69,7 +69,7 @@ export class ActiveMemorySet {
|
|
|
69
69
|
* Replace the resident layer with a fresh set of scope→summary
|
|
70
70
|
* pairs. Foreign VP scopes are silently dropped.
|
|
71
71
|
*
|
|
72
|
-
* @param {Array<{ scope: string, summary: string }>} entries
|
|
72
|
+
* @param {Array<{ scope: string, summary: string, category?: string }>} entries
|
|
73
73
|
*/
|
|
74
74
|
setResident(entries) {
|
|
75
75
|
this._resident.clear();
|
|
@@ -77,7 +77,10 @@ export class ActiveMemorySet {
|
|
|
77
77
|
if (this._isForeignVp(e.scope)) continue;
|
|
78
78
|
const summary = cleanMemoryPromptText(e.summary);
|
|
79
79
|
if (!summary) continue;
|
|
80
|
-
this._resident.set(e.scope,
|
|
80
|
+
this._resident.set(e.scope, {
|
|
81
|
+
summary,
|
|
82
|
+
...(typeof e.category === 'string' && e.category ? { category: e.category } : {}),
|
|
83
|
+
});
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -159,8 +162,15 @@ export class ActiveMemorySet {
|
|
|
159
162
|
// Resident: pack scopes by priority order (caller provides via insert
|
|
160
163
|
// order — current group's own vp first, then user, etc.).
|
|
161
164
|
const { picked: resPicked, cost: resCost } = pickMemoryItems({
|
|
162
|
-
items: [...this._resident.entries()].map(([scope,
|
|
163
|
-
scope,
|
|
165
|
+
items: [...this._resident.entries()].map(([scope, entry]) => ({
|
|
166
|
+
scope,
|
|
167
|
+
// Related-Session summaries are explicitly historical context. Keep the
|
|
168
|
+
// bounded prose intact and label it as experience instead of dropping
|
|
169
|
+
// the whole paragraph because it mentions an old PR/tag/task state.
|
|
170
|
+
summary: entry.category === 'experience'
|
|
171
|
+
? cleanMemoryPromptText(entry.summary)
|
|
172
|
+
: filterMemoryPromptTextForPrompt(entry.summary, userMsg),
|
|
173
|
+
...(entry.category ? { category: entry.category } : {}),
|
|
164
174
|
})),
|
|
165
175
|
budget: this.budget.resident,
|
|
166
176
|
seen: seenPromptText,
|
package/yeaft/tasks/manager.js
CHANGED
|
@@ -20,6 +20,7 @@ import { getRuntimePlatformInfo } from '../runtime-platform.js';
|
|
|
20
20
|
const LOG_PREVIEW_BYTES = 4096;
|
|
21
21
|
const SUB_AGENT_LOG_PREVIEW_BYTES = 1024 * 1024;
|
|
22
22
|
const DEFAULT_CANCEL_ESCALATION_MS = 2000;
|
|
23
|
+
const PROMPT_TASK_LIMIT = 5;
|
|
23
24
|
|
|
24
25
|
function logPreviewBytesFor(task) {
|
|
25
26
|
return task?.kind === 'sub_agent' ? SUB_AGENT_LOG_PREVIEW_BYTES : LOG_PREVIEW_BYTES;
|
|
@@ -54,9 +55,35 @@ function publicSnapshot(task) {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
function taskKindLabel(kind, language) {
|
|
59
|
+
const zh = String(language || '').toLowerCase().startsWith('zh');
|
|
60
|
+
if (kind === 'sub_agent') return zh ? '子 Agent' : 'sub-agent';
|
|
61
|
+
if (kind === 'shell') return zh ? '后台命令' : 'background command';
|
|
62
|
+
return zh ? '后台任务' : 'background task';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeTaskName(task) {
|
|
66
|
+
const name = typeof task?.runtime?.name === 'string' ? task.runtime.name.trim() : '';
|
|
67
|
+
return /^[\p{L}\p{N}][\p{L}\p{N}._-]{0,63}$/u.test(name) ? name : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function promptTaskLabel(task, language) {
|
|
71
|
+
const name = task?.kind === 'sub_agent' ? safeTaskName(task) : '';
|
|
72
|
+
if (!name) return taskKindLabel(task?.kind, language);
|
|
73
|
+
return String(language || '').toLowerCase().startsWith('zh')
|
|
74
|
+
? `子 Agent ${name}`
|
|
75
|
+
: `sub-agent ${name}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function taskStatusLabel(status, language) {
|
|
79
|
+
const zh = String(language || '').toLowerCase().startsWith('zh');
|
|
80
|
+
if (!zh) return String(status || 'running').replace(/_/g, ' ');
|
|
81
|
+
const labels = {
|
|
82
|
+
running: '运行中',
|
|
83
|
+
queued: '等待中',
|
|
84
|
+
cancelling: '正在取消',
|
|
85
|
+
};
|
|
86
|
+
return labels[status] || String(status || '运行中').replace(/_/g, ' ');
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
export class TaskManager {
|
|
@@ -358,17 +385,27 @@ export class TaskManager {
|
|
|
358
385
|
return publicSnapshot(task);
|
|
359
386
|
}
|
|
360
387
|
|
|
361
|
-
renderActiveTasksForPrompt(sessionId = null) {
|
|
388
|
+
renderActiveTasksForPrompt(sessionId = null, { language = 'en', limit = PROMPT_TASK_LIMIT } = {}) {
|
|
362
389
|
const tasks = this.listActiveTasks(sessionId);
|
|
363
390
|
if (tasks.length === 0) return '';
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
391
|
+
const zh = String(language || '').toLowerCase().startsWith('zh');
|
|
392
|
+
const maxTasks = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : PROMPT_TASK_LIMIT;
|
|
393
|
+
const visible = tasks.slice(0, maxTasks);
|
|
394
|
+
const lines = [zh ? '## 可能相关的任务' : '## Possibly Relevant Tasks'];
|
|
395
|
+
lines.push(zh
|
|
396
|
+
? '以下任务仍在后台运行。需要进度或完整输出时使用任务工具查询;不要把它们当成记忆事实。'
|
|
397
|
+
: 'These tasks are still running in the background. Use the task tools for progress or full output; do not treat them as memory facts.');
|
|
398
|
+
for (const task of visible) {
|
|
399
|
+
const title = promptTaskLabel(task, language);
|
|
400
|
+
const detail = zh
|
|
401
|
+
? `${taskKindLabel(task.kind, language)},${taskStatusLabel(task.status, language)}`
|
|
402
|
+
: `${taskKindLabel(task.kind, language)}, ${taskStatusLabel(task.status, language)}`;
|
|
403
|
+
lines.push(`- ${title} (${detail})`);
|
|
404
|
+
}
|
|
405
|
+
if (tasks.length > visible.length) {
|
|
406
|
+
const remaining = tasks.length - visible.length;
|
|
407
|
+
lines.push(zh ? `- 另有 ${remaining} 个运行中任务,可用任务列表查看。` : `- ${remaining} more running task${remaining === 1 ? '' : 's'}; use the task list to inspect them.`);
|
|
370
408
|
}
|
|
371
|
-
lines.push('</active_tasks>');
|
|
372
409
|
return lines.join('\n');
|
|
373
410
|
}
|
|
374
411
|
}
|