@cjhyy/code-shell-core 0.8.6 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agent-server-stdio.js +7 -5
- package/dist/context/manager.js +10 -2
- package/dist/engine/engine.d.ts +60 -8
- package/dist/engine/engine.js +153 -16
- package/dist/engine/run-setup.d.ts +1 -0
- package/dist/engine/run-setup.js +2 -1
- package/dist/engine/run-types.d.ts +7 -0
- package/dist/engine/turn-loop.d.ts +0 -3
- package/dist/engine/turn-loop.js +1 -80
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/panel-apps/manifest.d.ts +11 -11
- package/dist/panel-apps/manifest.js +16 -3
- package/dist/prompt/composer.d.ts +7 -0
- package/dist/prompt/composer.js +15 -8
- package/dist/prompt/sections/browser.md +2 -2
- package/dist/protocol/server.js +41 -1
- package/dist/protocol/types.d.ts +14 -0
- package/dist/session/memory.d.ts +10 -0
- package/dist/session/memory.js +7 -1
- package/dist/session/session-manager.js +1 -0
- package/dist/session/transcript.d.ts +36 -0
- package/dist/session/transcript.js +149 -4
- package/dist/tool-system/browser-bridge.d.ts +3 -0
- package/dist/tool-system/builtin/browser-tools.js +13 -0
- package/dist/tool-system/path-policy.js +84 -5
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -215,11 +215,13 @@ function freshSettings() {
|
|
|
215
215
|
}
|
|
216
216
|
const chatManager = new ChatSessionManager({
|
|
217
217
|
runtime,
|
|
218
|
-
// resolvedLlmConfig is the bootstrap-time snapshot.
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
218
|
+
// resolvedLlmConfig is the bootstrap-time snapshot. Explicit global model
|
|
219
|
+
// changes can move modelPool.activeKey ahead of it, so newly-created
|
|
220
|
+
// sessions re-resolve from the pool each time the factory fires. A
|
|
221
|
+
// per-session configure intentionally does NOT move that shared key; this
|
|
222
|
+
// keeps one Session's selected model from leaking into the next Session.
|
|
223
|
+
// Fall back to the snapshot only when the pool can't resolve (no active key
|
|
224
|
+
// — shouldn't happen in practice).
|
|
223
225
|
engineFactory: (slice) => {
|
|
224
226
|
// Effective cwd for THIS session: explicit slice.cwd → that project; absent
|
|
225
227
|
// → the no-repo sandbox (NOT the worker's stale boot cwd). See
|
package/dist/context/manager.js
CHANGED
|
@@ -547,8 +547,16 @@ export class ContextManager {
|
|
|
547
547
|
const window = messages.slice(start, end);
|
|
548
548
|
// Feed a prior anchored summary back so the LLM merge-updates rather than
|
|
549
549
|
// re-summarizes from scratch, matching the rolling-summary behavior of
|
|
550
|
-
// trySummaryCompact()
|
|
551
|
-
|
|
550
|
+
// trySummaryCompact() — but ONLY when that anchored summary sits inside
|
|
551
|
+
// the window being replaced. A summary inside [start, end) is about to be
|
|
552
|
+
// overwritten by this call, so it must be merged in or its content is
|
|
553
|
+
// lost. A summary OUTSIDE the window belongs to a different, untouched
|
|
554
|
+
// span and must NOT be merged in: doing so (via `?? this.lastSummary`,
|
|
555
|
+
// which is a single most-recent-summary carryover with no span
|
|
556
|
+
// awareness) made every subsequent range archive fold in the text of
|
|
557
|
+
// every prior one, so N archived spans produced N copies of the same
|
|
558
|
+
// growing summary instead of N independent summaries.
|
|
559
|
+
const priorSummary = extractAnchoredSummary(window);
|
|
552
560
|
const prompt = buildSummarizationPrompt(window, priorSummary);
|
|
553
561
|
const summary = await this.summarizeFn(prompt, opts.signal);
|
|
554
562
|
if (!summary || summary.length <= 50)
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -457,6 +457,13 @@ export declare class Engine {
|
|
|
457
457
|
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
458
458
|
* Returns the new model entry.
|
|
459
459
|
*
|
|
460
|
+
* Per-session switches (`persist:false`) must not mutate the shared
|
|
461
|
+
* ModelPool active key. Every desktop ChatSession owns an Engine, but those
|
|
462
|
+
* Engines share one runtime pool; changing the pool selection here made the
|
|
463
|
+
* next newly-created Session inherit whichever model another Session had
|
|
464
|
+
* just selected (for example a Mimi manager model leaking into its delegated
|
|
465
|
+
* Work Session).
|
|
466
|
+
*
|
|
460
467
|
* Persists settings.defaults.text (= the connection id / pool key) so the
|
|
461
468
|
* next process startup defaults to the same model — without this, switches
|
|
462
469
|
* only live in memory and every restart reverts to the previously persisted
|
|
@@ -623,22 +630,67 @@ export declare class Engine {
|
|
|
623
630
|
*/
|
|
624
631
|
private prepareContextManagerForSession;
|
|
625
632
|
/**
|
|
626
|
-
* Archive a
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
633
|
+
* Archive a contiguous message window of a session into a single anchored
|
|
634
|
+
* summary, leaving everything outside the window untouched, and cache the
|
|
635
|
+
* result so a later forceCompact/resume reads the archived history. This is
|
|
636
|
+
* a generic range-archival facade over ContextManager.summarizeRange.
|
|
637
|
+
* Returns token stats before/after; equal before/after means the window was
|
|
638
|
+
* empty/unresolvable or the summary was rejected.
|
|
639
|
+
*
|
|
640
|
+
* Window resolution depends on whether the caller supplied anchors:
|
|
641
|
+
* - With `anchors`, the caller's `range` is IGNORED and the window is
|
|
642
|
+
* resolved from the client-message-id anchors over a fresh, marker-aware
|
|
643
|
+
* transcript replay. Callers (the pet segment closure) compute index
|
|
644
|
+
* ranges over the RAW transcript message list, whose indices grow
|
|
645
|
+
* forever — but the live list is trimmed by every persisted marker
|
|
646
|
+
* (replay after restart) and by each in-process archival, so a raw index
|
|
647
|
+
* range clamps to an empty window or, worse, onto the WRONG tail
|
|
648
|
+
* messages, which would then be persistently mis-summarized against
|
|
649
|
+
* correct anchors. Anchors are the only stable coordinates.
|
|
650
|
+
* - Without `anchors`, legacy behavior: `range` is a half-open index window
|
|
651
|
+
* over the cached/in-process message list, and nothing is persisted.
|
|
634
652
|
*/
|
|
635
653
|
archiveTurnRange(sessionId: string, range: {
|
|
636
654
|
start: number;
|
|
637
655
|
end: number;
|
|
656
|
+
}, anchors?: {
|
|
657
|
+
toClientMessageId: string;
|
|
658
|
+
fromClientMessageId?: string;
|
|
659
|
+
segmentId?: string;
|
|
638
660
|
}): Promise<{
|
|
639
661
|
before: number;
|
|
640
662
|
after: number;
|
|
641
663
|
}>;
|
|
664
|
+
/**
|
|
665
|
+
* Guard against writing a marker anchored to a client message id the
|
|
666
|
+
* transcript doesn't actually contain. toMessages()'s replay silently
|
|
667
|
+
* ignores a range_archive event whose anchor doesn't resolve (fail open),
|
|
668
|
+
* so a bad anchor wouldn't corrupt anything on its own — but if the marker
|
|
669
|
+
* carries a segmentId, appendRangeArchive's idempotency check treats that
|
|
670
|
+
* segmentId as "already recorded" forever, with no retry path (e.g. a
|
|
671
|
+
* one-time migration keyed "migration-v1" would be permanently burned on
|
|
672
|
+
* its first, failed attempt). Reject before writing so the caller can
|
|
673
|
+
* retry with a corrected anchor. fromClientMessageId is optional in the
|
|
674
|
+
* data model (absent means "from the beginning"), so only validate it when
|
|
675
|
+
* present; missing it is fail-open by design, same as the replay path.
|
|
676
|
+
*/
|
|
677
|
+
private hasLiveArchiveAnchors;
|
|
678
|
+
/**
|
|
679
|
+
* Persist an archive boundary WITHOUT a summarization call — the caller
|
|
680
|
+
* already has the summary text (e.g. the one-time migration built from
|
|
681
|
+
* pet journal entries). Wraps the plain text in the anchored-summary
|
|
682
|
+
* envelope so replay and rolling-merge treat it like a real archive.
|
|
683
|
+
* Returns false when the segmentId was already recorded (idempotent) OR
|
|
684
|
+
* when the anchors don't resolve to real messages in this transcript —
|
|
685
|
+
* see hasLiveArchiveAnchors for why a dead anchor must be rejected before
|
|
686
|
+
* the (potentially one-shot) segmentId gets burned.
|
|
687
|
+
*/
|
|
688
|
+
appendArchiveMarker(sessionId: string, marker: {
|
|
689
|
+
summary: string;
|
|
690
|
+
toClientMessageId: string;
|
|
691
|
+
fromClientMessageId?: string;
|
|
692
|
+
segmentId?: string;
|
|
693
|
+
}): Promise<boolean>;
|
|
642
694
|
private recordCacheReadDiagnostics;
|
|
643
695
|
private getSettingsManager;
|
|
644
696
|
/**
|
package/dist/engine/engine.js
CHANGED
|
@@ -12,6 +12,7 @@ import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-q
|
|
|
12
12
|
import { RunEnvironmentResolver } from "./run-environment.js";
|
|
13
13
|
import { BUILTIN_TOOLS, } from "../tool-system/builtin/index.js";
|
|
14
14
|
import { asyncAgentRegistry } from "../tool-system/builtin/agent-registry.js";
|
|
15
|
+
import { skillToolDef } from "../tool-system/builtin/skill.js";
|
|
15
16
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
16
17
|
import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
|
|
17
18
|
import { HookRegistry } from "../hooks/registry.js";
|
|
@@ -20,7 +21,7 @@ import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
|
|
|
20
21
|
import { pluginAgentDirs } from "../plugins/installer/loadPluginAgents.js";
|
|
21
22
|
import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
|
|
22
23
|
import { ContextManager } from "../context/manager.js";
|
|
23
|
-
import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
|
|
24
|
+
import { CONTEXT_PACKAGE_MAX_OUTPUT_TOKENS, buildAnchoredSummaryMessage, buildContextPackagePromptFromSerialized, estimateTokens, groupMessagesByApiRound, serializeContextPackageMessages, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
|
|
24
25
|
import { PromptComposer } from "../prompt/composer.js";
|
|
25
26
|
import { SessionManager, assertSafeSessionId, isEphemeralSessionState, sessionsRoot, } from "../session/session-manager.js";
|
|
26
27
|
import { createRunUsageAccounting, wireRunModelFacade } from "./run-accounting.js";
|
|
@@ -1935,6 +1936,10 @@ export class Engine {
|
|
|
1935
1936
|
(visibilityStoredGoal !== undefined && visibilityStoredGoal.paused !== true) ||
|
|
1936
1937
|
(visibilityDefaultGoal !== undefined && visibilityDefaultGoal.paused !== true));
|
|
1937
1938
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists(cwd, sessionProfileOverrides);
|
|
1939
|
+
// A profile whose tool allowlist excludes the Skill tool can never invoke
|
|
1940
|
+
// a skill, so the full skills listing would be dead context for every one
|
|
1941
|
+
// of its turns (e.g. the Pet manager) — inject none via an empty allowlist.
|
|
1942
|
+
const profileCanUseSkills = !profile?.allowedToolNames || profile.allowedToolNames.has(skillToolDef.name);
|
|
1938
1943
|
const promptComposer = new PromptComposer(buildPromptComposerConfig({
|
|
1939
1944
|
cwd,
|
|
1940
1945
|
model: this.config.llm.model,
|
|
@@ -1954,8 +1959,9 @@ export class Engine {
|
|
|
1954
1959
|
instructionBoundaryFinder: (scanCwd) => resolveInstructionBoundary(scanCwd, this.capabilities),
|
|
1955
1960
|
disabledSkills,
|
|
1956
1961
|
disabledPlugins,
|
|
1957
|
-
skillAllowlist: this.config.skillAllowlist,
|
|
1962
|
+
skillAllowlist: profileCanUseSkills ? this.config.skillAllowlist : [],
|
|
1958
1963
|
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1964
|
+
memoryCurrentProjectOnly: profile?.memoryCurrentProjectOnly,
|
|
1959
1965
|
goalToolState: !profile?.allowedToolNames ||
|
|
1960
1966
|
profile.allowedToolNames.has("complete_goal") ||
|
|
1961
1967
|
profile.allowedToolNames.has("cancel_goal")
|
|
@@ -2228,13 +2234,28 @@ export class Engine {
|
|
|
2228
2234
|
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
2229
2235
|
* Returns the new model entry.
|
|
2230
2236
|
*
|
|
2237
|
+
* Per-session switches (`persist:false`) must not mutate the shared
|
|
2238
|
+
* ModelPool active key. Every desktop ChatSession owns an Engine, but those
|
|
2239
|
+
* Engines share one runtime pool; changing the pool selection here made the
|
|
2240
|
+
* next newly-created Session inherit whichever model another Session had
|
|
2241
|
+
* just selected (for example a Mimi manager model leaking into its delegated
|
|
2242
|
+
* Work Session).
|
|
2243
|
+
*
|
|
2231
2244
|
* Persists settings.defaults.text (= the connection id / pool key) so the
|
|
2232
2245
|
* next process startup defaults to the same model — without this, switches
|
|
2233
2246
|
* only live in memory and every restart reverts to the previously persisted
|
|
2234
2247
|
* defaults.text.
|
|
2235
2248
|
*/
|
|
2236
2249
|
switchModel(key, opts) {
|
|
2237
|
-
const
|
|
2250
|
+
const perSession = opts?.persist === false;
|
|
2251
|
+
const entry = perSession ? this.modelPool.get(key) : this.modelPool.switch(key);
|
|
2252
|
+
if (!entry) {
|
|
2253
|
+
const available = this.modelPool
|
|
2254
|
+
.list()
|
|
2255
|
+
.map((candidate) => candidate.key)
|
|
2256
|
+
.join(", ");
|
|
2257
|
+
throw new Error(`Model "${key}" not found. Available: ${available}`);
|
|
2258
|
+
}
|
|
2238
2259
|
// LLMConfig is pure model identity now — rotate it wholesale. Cross-model
|
|
2239
2260
|
// runtime knobs (temperature/timeout/retryMaxAttempts/imageDetail) live on
|
|
2240
2261
|
// this.config.clientDefaults and survive the switch untouched.
|
|
@@ -2243,7 +2264,7 @@ export class Engine {
|
|
|
2243
2264
|
// persist: false is the per-session path (ChatSession) — switching one
|
|
2244
2265
|
// session's model must not rewrite settings.defaults.text, the boot
|
|
2245
2266
|
// default every future session inherits.
|
|
2246
|
-
if (
|
|
2267
|
+
if (!perSession)
|
|
2247
2268
|
this.persistActiveModel(entry);
|
|
2248
2269
|
return entry;
|
|
2249
2270
|
}
|
|
@@ -2839,33 +2860,149 @@ export class Engine {
|
|
|
2839
2860
|
return contextManager;
|
|
2840
2861
|
}
|
|
2841
2862
|
/**
|
|
2842
|
-
* Archive a
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
*
|
|
2846
|
-
*
|
|
2847
|
-
*
|
|
2848
|
-
*
|
|
2849
|
-
*
|
|
2850
|
-
|
|
2851
|
-
|
|
2863
|
+
* Archive a contiguous message window of a session into a single anchored
|
|
2864
|
+
* summary, leaving everything outside the window untouched, and cache the
|
|
2865
|
+
* result so a later forceCompact/resume reads the archived history. This is
|
|
2866
|
+
* a generic range-archival facade over ContextManager.summarizeRange.
|
|
2867
|
+
* Returns token stats before/after; equal before/after means the window was
|
|
2868
|
+
* empty/unresolvable or the summary was rejected.
|
|
2869
|
+
*
|
|
2870
|
+
* Window resolution depends on whether the caller supplied anchors:
|
|
2871
|
+
* - With `anchors`, the caller's `range` is IGNORED and the window is
|
|
2872
|
+
* resolved from the client-message-id anchors over a fresh, marker-aware
|
|
2873
|
+
* transcript replay. Callers (the pet segment closure) compute index
|
|
2874
|
+
* ranges over the RAW transcript message list, whose indices grow
|
|
2875
|
+
* forever — but the live list is trimmed by every persisted marker
|
|
2876
|
+
* (replay after restart) and by each in-process archival, so a raw index
|
|
2877
|
+
* range clamps to an empty window or, worse, onto the WRONG tail
|
|
2878
|
+
* messages, which would then be persistently mis-summarized against
|
|
2879
|
+
* correct anchors. Anchors are the only stable coordinates.
|
|
2880
|
+
* - Without `anchors`, legacy behavior: `range` is a half-open index window
|
|
2881
|
+
* over the cached/in-process message list, and nothing is persisted.
|
|
2882
|
+
*/
|
|
2883
|
+
async archiveTurnRange(sessionId, range, anchors) {
|
|
2852
2884
|
const effectiveSessionId = sessionId || this.lastSessionId;
|
|
2853
2885
|
if (!effectiveSessionId)
|
|
2854
2886
|
return { before: 0, after: 0 };
|
|
2855
2887
|
const session = this.sessionManager.resume(effectiveSessionId);
|
|
2856
|
-
|
|
2888
|
+
let sourceMessages;
|
|
2889
|
+
let window;
|
|
2890
|
+
if (anchors) {
|
|
2891
|
+
const { messages: liveMessages, liveIndexByClientMessageId } = session.transcript.toMessagesWithIndex();
|
|
2892
|
+
sourceMessages = liveMessages;
|
|
2893
|
+
// An absent from-anchor means "from the beginning" — over the live
|
|
2894
|
+
// list that is index 0, which includes any previously replayed
|
|
2895
|
+
// from-less summary at the head, so summarizeRange merge-feeds it
|
|
2896
|
+
// (extractAnchoredSummary) instead of losing it.
|
|
2897
|
+
const start = anchors.fromClientMessageId !== undefined
|
|
2898
|
+
? liveIndexByClientMessageId.get(anchors.fromClientMessageId)
|
|
2899
|
+
: 0;
|
|
2900
|
+
const end = liveIndexByClientMessageId.get(anchors.toClientMessageId);
|
|
2901
|
+
if (start === undefined || end === undefined || end <= start) {
|
|
2902
|
+
// Fail open: no summarization, no persistence. Falling back to the
|
|
2903
|
+
// caller's raw range is exactly the bug this path exists to fix —
|
|
2904
|
+
// it would summarize the wrong messages and persist that against
|
|
2905
|
+
// the (correct) anchors, with segmentId dedupe blocking correction.
|
|
2906
|
+
logger.warn("engine.archive_range.anchor_window_unresolved", {
|
|
2907
|
+
sessionId: effectiveSessionId,
|
|
2908
|
+
segmentId: anchors.segmentId,
|
|
2909
|
+
fromClientMessageId: anchors.fromClientMessageId,
|
|
2910
|
+
toClientMessageId: anchors.toClientMessageId,
|
|
2911
|
+
});
|
|
2912
|
+
const before = estimateTokens(sourceMessages);
|
|
2913
|
+
return { before, after: before };
|
|
2914
|
+
}
|
|
2915
|
+
window = { start, end };
|
|
2916
|
+
}
|
|
2917
|
+
else {
|
|
2918
|
+
sourceMessages =
|
|
2919
|
+
this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
|
|
2920
|
+
window = range;
|
|
2921
|
+
}
|
|
2857
2922
|
const before = estimateTokens(sourceMessages);
|
|
2858
2923
|
const contextManager = await this.prepareContextManagerForSession(effectiveSessionId, session, sourceMessages, "archive_range");
|
|
2859
2924
|
// Range archival is initiated deliberately, not by a pressure heuristic;
|
|
2860
2925
|
// don't let a stale run callback retained on lastContextManager double-emit.
|
|
2861
2926
|
contextManager.setOnCompact(() => { });
|
|
2862
|
-
const archived = await contextManager.summarizeRange(sourceMessages,
|
|
2927
|
+
const archived = await contextManager.summarizeRange(sourceMessages, window);
|
|
2863
2928
|
const after = estimateTokens(archived);
|
|
2929
|
+
// Persist the boundary so a restart replays the trimmed context. Only when
|
|
2930
|
+
// summarizeRange actually replaced the span (identity return means empty
|
|
2931
|
+
// window / rejected summary / no summarizer available) and the caller
|
|
2932
|
+
// supplied stable anchors to record it against.
|
|
2933
|
+
if (anchors && archived !== sourceMessages) {
|
|
2934
|
+
const clampedStart = Math.max(0, Math.min(window.start, archived.length - 1));
|
|
2935
|
+
const summaryMessage = archived[clampedStart];
|
|
2936
|
+
const summaryText = typeof summaryMessage?.content === "string" ? summaryMessage.content : undefined;
|
|
2937
|
+
if (summaryText && this.hasLiveArchiveAnchors(session, anchors)) {
|
|
2938
|
+
session.transcript.appendRangeArchive({ summary: summaryText, ...anchors });
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
// In the anchored path this replaces any in-process-only cache state with
|
|
2942
|
+
// the fresh-replay-based result — safe, because the transcript is the
|
|
2943
|
+
// superset of the cache and pressure compaction re-runs if needed.
|
|
2864
2944
|
this.compactedMessagesBySession.set(effectiveSessionId, archived);
|
|
2865
2945
|
this.lastSessionId = effectiveSessionId;
|
|
2866
2946
|
this.lastMessages = archived;
|
|
2867
2947
|
return { before, after };
|
|
2868
2948
|
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Guard against writing a marker anchored to a client message id the
|
|
2951
|
+
* transcript doesn't actually contain. toMessages()'s replay silently
|
|
2952
|
+
* ignores a range_archive event whose anchor doesn't resolve (fail open),
|
|
2953
|
+
* so a bad anchor wouldn't corrupt anything on its own — but if the marker
|
|
2954
|
+
* carries a segmentId, appendRangeArchive's idempotency check treats that
|
|
2955
|
+
* segmentId as "already recorded" forever, with no retry path (e.g. a
|
|
2956
|
+
* one-time migration keyed "migration-v1" would be permanently burned on
|
|
2957
|
+
* its first, failed attempt). Reject before writing so the caller can
|
|
2958
|
+
* retry with a corrected anchor. fromClientMessageId is optional in the
|
|
2959
|
+
* data model (absent means "from the beginning"), so only validate it when
|
|
2960
|
+
* present; missing it is fail-open by design, same as the replay path.
|
|
2961
|
+
*/
|
|
2962
|
+
hasLiveArchiveAnchors(session, anchors) {
|
|
2963
|
+
if (!session.transcript.hasClientMessageId(anchors.toClientMessageId))
|
|
2964
|
+
return false;
|
|
2965
|
+
if (anchors.fromClientMessageId !== undefined &&
|
|
2966
|
+
!session.transcript.hasClientMessageId(anchors.fromClientMessageId)) {
|
|
2967
|
+
return false;
|
|
2968
|
+
}
|
|
2969
|
+
return true;
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Persist an archive boundary WITHOUT a summarization call — the caller
|
|
2973
|
+
* already has the summary text (e.g. the one-time migration built from
|
|
2974
|
+
* pet journal entries). Wraps the plain text in the anchored-summary
|
|
2975
|
+
* envelope so replay and rolling-merge treat it like a real archive.
|
|
2976
|
+
* Returns false when the segmentId was already recorded (idempotent) OR
|
|
2977
|
+
* when the anchors don't resolve to real messages in this transcript —
|
|
2978
|
+
* see hasLiveArchiveAnchors for why a dead anchor must be rejected before
|
|
2979
|
+
* the (potentially one-shot) segmentId gets burned.
|
|
2980
|
+
*/
|
|
2981
|
+
async appendArchiveMarker(sessionId, marker) {
|
|
2982
|
+
const session = this.sessionManager.resume(sessionId);
|
|
2983
|
+
if (!this.hasLiveArchiveAnchors(session, marker)) {
|
|
2984
|
+
logger.warn("engine.archive_marker.dead_anchor", {
|
|
2985
|
+
sessionId,
|
|
2986
|
+
segmentId: marker.segmentId,
|
|
2987
|
+
toClientMessageId: marker.toClientMessageId,
|
|
2988
|
+
fromClientMessageId: marker.fromClientMessageId,
|
|
2989
|
+
});
|
|
2990
|
+
return false;
|
|
2991
|
+
}
|
|
2992
|
+
const wrapped = buildAnchoredSummaryMessage(marker.summary, {
|
|
2993
|
+
...(session.transcript.isPersistent()
|
|
2994
|
+
? { transcriptPath: session.transcript.getFilePath() }
|
|
2995
|
+
: {}),
|
|
2996
|
+
});
|
|
2997
|
+
const content = typeof wrapped.content === "string" ? wrapped.content : marker.summary;
|
|
2998
|
+
const appended = session.transcript.appendRangeArchive({ ...marker, summary: content });
|
|
2999
|
+
if (!appended)
|
|
3000
|
+
return false;
|
|
3001
|
+
// The in-memory cache (if any) predates the marker; drop it so the next
|
|
3002
|
+
// run rebuilds from the trimmed transcript replay.
|
|
3003
|
+
this.compactedMessagesBySession.delete(sessionId);
|
|
3004
|
+
return true;
|
|
3005
|
+
}
|
|
2869
3006
|
recordCacheReadDiagnostics(sessionId, sample) {
|
|
2870
3007
|
const result = this.promptCacheDiagnostics.record(sessionId, sample);
|
|
2871
3008
|
if (result.kind === "scope_changed") {
|
|
@@ -32,6 +32,7 @@ export interface RunPromptComposerConfigInput {
|
|
|
32
32
|
disabledPlugins: ComposerOptions["disabledPlugins"];
|
|
33
33
|
skillAllowlist: ComposerOptions["skillAllowlist"];
|
|
34
34
|
memoriesMaxAgeDays: ComposerOptions["memoriesMaxAgeDays"];
|
|
35
|
+
memoryCurrentProjectOnly?: ComposerOptions["memoryCurrentProjectOnly"];
|
|
35
36
|
goalToolState: ComposerOptions["goalToolState"];
|
|
36
37
|
capabilityPromptSections: ComposerOptions["capabilityPromptSections"];
|
|
37
38
|
dynamicContextProviders: ComposerOptions["dynamicContextProviders"];
|
package/dist/engine/run-setup.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveRunProfileState(args) {
|
|
|
23
23
|
}
|
|
24
24
|
/** Build the prompt-composer options for a run without capturing the Engine facade. */
|
|
25
25
|
export function buildPromptComposerConfig(args) {
|
|
26
|
-
const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
26
|
+
const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
27
27
|
return {
|
|
28
28
|
cwd,
|
|
29
29
|
model,
|
|
@@ -49,6 +49,7 @@ export function buildPromptComposerConfig(args) {
|
|
|
49
49
|
disabledPlugins,
|
|
50
50
|
skillAllowlist,
|
|
51
51
|
memoriesMaxAgeDays,
|
|
52
|
+
memoryCurrentProjectOnly,
|
|
52
53
|
goalToolState,
|
|
53
54
|
capabilityPromptSections,
|
|
54
55
|
dynamicContextProviders,
|
|
@@ -32,6 +32,13 @@ export interface RunBehaviorProfile {
|
|
|
32
32
|
disablePlanMode?: boolean;
|
|
33
33
|
/** When true, MCP servers are neither connected nor exposed for the run. */
|
|
34
34
|
disableMcp?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When true, the injected persistent-memory index is trimmed to this run's
|
|
37
|
+
* project: global-layer project-type / dream-scope records tied to other
|
|
38
|
+
* projects are dropped. Meant for manager-style profiles whose runs never
|
|
39
|
+
* work inside other repos.
|
|
40
|
+
*/
|
|
41
|
+
memoryCurrentProjectOnly?: boolean;
|
|
35
42
|
/**
|
|
36
43
|
* Wrapper tag for host-provided runtime context injected at the system
|
|
37
44
|
* prompt tail (e.g. "pet-world"). Injection happens only when both this tag
|
|
@@ -202,9 +202,6 @@ export declare class TurnLoop {
|
|
|
202
202
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
203
203
|
*/
|
|
204
204
|
private stopBlockCount;
|
|
205
|
-
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
206
|
-
private repeatedToolBatchFingerprint;
|
|
207
|
-
private repeatedToolBatchCount;
|
|
208
205
|
/**
|
|
209
206
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
210
207
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Following Claude Code's po_() pattern:
|
|
5
5
|
* pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { buildAgentDirectionMessage } from "../tool-system/builtin/agent-notifications.js";
|
|
9
9
|
import { newTurnId } from "./turn-state.js";
|
|
10
10
|
import { formatFriendlyError } from "./friendly-error.js";
|
|
@@ -40,39 +40,6 @@ export function toolResultToBlock(result) {
|
|
|
40
40
|
block.is_error = true;
|
|
41
41
|
return block;
|
|
42
42
|
}
|
|
43
|
-
const REPEATED_TOOL_BATCH_LIMIT = 3;
|
|
44
|
-
function canonicalToolValue(value) {
|
|
45
|
-
if (value === null)
|
|
46
|
-
return "null";
|
|
47
|
-
if (value === undefined)
|
|
48
|
-
return "undefined";
|
|
49
|
-
if (Array.isArray(value))
|
|
50
|
-
return `[${value.map(canonicalToolValue).join(",")}]`;
|
|
51
|
-
if (typeof value === "object") {
|
|
52
|
-
return `{${Object.entries(value)
|
|
53
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
54
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalToolValue(entry)}`)
|
|
55
|
-
.join(",")}}`;
|
|
56
|
-
}
|
|
57
|
-
return JSON.stringify(value);
|
|
58
|
-
}
|
|
59
|
-
function repeatedToolBatchFingerprint(toolCalls, results) {
|
|
60
|
-
// Hash immediately and never log the canonical source: tool results may
|
|
61
|
-
// contain credentials or large media payloads. Call ids are deliberately
|
|
62
|
-
// omitted because providers generate a fresh id for every identical retry.
|
|
63
|
-
return createHash("sha256")
|
|
64
|
-
.update(canonicalToolValue({
|
|
65
|
-
calls: toolCalls.map((call) => ({ toolName: call.toolName, args: call.args })),
|
|
66
|
-
results: results.map((result) => ({
|
|
67
|
-
toolName: result.toolName,
|
|
68
|
-
isError: result.isError === true || Boolean(result.error),
|
|
69
|
-
error: result.error,
|
|
70
|
-
result: result.result,
|
|
71
|
-
contentBlocks: result.contentBlocks,
|
|
72
|
-
})),
|
|
73
|
-
}))
|
|
74
|
-
.digest("hex");
|
|
75
|
-
}
|
|
76
43
|
export class TurnLoop {
|
|
77
44
|
deps;
|
|
78
45
|
config;
|
|
@@ -109,9 +76,6 @@ export class TurnLoop {
|
|
|
109
76
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
110
77
|
*/
|
|
111
78
|
stopBlockCount = 0;
|
|
112
|
-
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
113
|
-
repeatedToolBatchFingerprint;
|
|
114
|
-
repeatedToolBatchCount = 0;
|
|
115
79
|
/**
|
|
116
80
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
117
81
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
|
@@ -1445,49 +1409,6 @@ export class TurnLoop {
|
|
|
1445
1409
|
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
1446
1410
|
}
|
|
1447
1411
|
}
|
|
1448
|
-
const toolBatchFingerprint = repeatedToolBatchFingerprint(toolCalls, results);
|
|
1449
|
-
if (toolBatchFingerprint === this.repeatedToolBatchFingerprint) {
|
|
1450
|
-
this.repeatedToolBatchCount++;
|
|
1451
|
-
}
|
|
1452
|
-
else {
|
|
1453
|
-
this.repeatedToolBatchFingerprint = toolBatchFingerprint;
|
|
1454
|
-
this.repeatedToolBatchCount = 1;
|
|
1455
|
-
}
|
|
1456
|
-
if (this.repeatedToolBatchCount >= REPEATED_TOOL_BATCH_LIMIT) {
|
|
1457
|
-
tlog.warn("turn.repeated_tool_batch_stopped", {
|
|
1458
|
-
cat: "turn",
|
|
1459
|
-
repeatedCount: this.repeatedToolBatchCount,
|
|
1460
|
-
tools: toolCalls.map((call) => call.toolName),
|
|
1461
|
-
});
|
|
1462
|
-
await this.emitHook("on_turn_end", {
|
|
1463
|
-
turnNumber: this.turnCount,
|
|
1464
|
-
hasToolUse: true,
|
|
1465
|
-
toolCallCount: toolCalls.length,
|
|
1466
|
-
});
|
|
1467
|
-
finalText =
|
|
1468
|
-
`检测到同一组工具调用及其结果连续重复 ${REPEATED_TOOL_BATCH_LIMIT} 次,` +
|
|
1469
|
-
"已自动停止,避免继续空转。请调整请求或让 Session 获取新的上下文后再试。";
|
|
1470
|
-
this.deps.transcript.appendMessage("assistant", finalText);
|
|
1471
|
-
messages.push({ role: "assistant", content: finalText });
|
|
1472
|
-
this.config.onStream?.({
|
|
1473
|
-
type: "assistant_message",
|
|
1474
|
-
messageId: assistantMessageId,
|
|
1475
|
-
message: { role: "assistant", content: finalText },
|
|
1476
|
-
});
|
|
1477
|
-
this.finalizeModelTurn();
|
|
1478
|
-
if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
1479
|
-
this.repeatedToolBatchFingerprint = undefined;
|
|
1480
|
-
this.repeatedToolBatchCount = 0;
|
|
1481
|
-
continue;
|
|
1482
|
-
}
|
|
1483
|
-
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1484
|
-
return {
|
|
1485
|
-
text: finalText,
|
|
1486
|
-
reason: "completed",
|
|
1487
|
-
messages,
|
|
1488
|
-
completionKind: "limit_stop",
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
1412
|
// Hook: turn end
|
|
1492
1413
|
await this.emitHook("on_turn_end", {
|
|
1493
1414
|
turnNumber: this.turnCount,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.8.
|
|
6
|
+
export declare const VERSION = "0.8.8";
|
|
7
7
|
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
|
|
9
9
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.8.
|
|
6
|
+
export const VERSION = "0.8.8";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Engine (primary API) ────────────────────────────────────────
|