@cjhyy/code-shell-core 0.8.7 → 0.8.9
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 +187 -35
- package/dist/engine/run-finalize.js +8 -6
- package/dist/engine/run-session-open.js +1 -1
- package/dist/engine/run-setup.d.ts +5 -0
- package/dist/engine/run-setup.js +6 -1
- package/dist/engine/run-tooling.js +7 -1
- package/dist/engine/run-types.d.ts +31 -0
- package/dist/engine/run-types.js +21 -0
- package/dist/engine/run-workspace.js +11 -5
- 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 +7 -3
- package/dist/prompt/composer.d.ts +15 -0
- package/dist/prompt/composer.js +23 -8
- package/dist/protocol/chat-session-manager.js +9 -0
- package/dist/protocol/server.js +77 -1
- package/dist/protocol/types.d.ts +26 -0
- package/dist/session/memory.d.ts +10 -0
- package/dist/session/memory.js +7 -1
- package/dist/session/session-manager.d.ts +3 -1
- package/dist/session/session-manager.js +42 -25
- package/dist/session/transcript.d.ts +36 -0
- package/dist/session/transcript.js +149 -4
- package/dist/tool-system/builtin/index.js +4 -1
- package/dist/tool-system/path-policy.js +25 -33
- 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";
|
|
@@ -43,7 +44,7 @@ import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
|
|
|
43
44
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
44
45
|
import { PromptCacheDiagnosticRecorder, promptCacheDropHint, } from "./prompt-cache-diagnostics.js";
|
|
45
46
|
import { buildRunUserMessageContent, prepareRunImageInput } from "./run-image-input.js";
|
|
46
|
-
import { QUICK_CHAT_RESTRICTED_PROFILE, } from "./run-types.js";
|
|
47
|
+
import { ISOLATED_TASK_PROFILE, QUICK_CHAT_RESTRICTED_PROFILE, } from "./run-types.js";
|
|
47
48
|
import { createSubAgentSpawner } from "./subagent-spawner.js";
|
|
48
49
|
import { AuxiliaryPipeline, sameLlmIdentity } from "./auxiliary-pipeline.js";
|
|
49
50
|
import { PermissionController } from "./permission-controller.js";
|
|
@@ -412,6 +413,7 @@ export class Engine {
|
|
|
412
413
|
// extension modules — later registrations override earlier ones by id.
|
|
413
414
|
this.behaviorProfiles = new Map([
|
|
414
415
|
QUICK_CHAT_RESTRICTED_PROFILE,
|
|
416
|
+
ISOLATED_TASK_PROFILE,
|
|
415
417
|
...(config.behaviorProfiles ?? []),
|
|
416
418
|
...(config.extensionModules ?? []).flatMap((module) => module.behaviorProfiles ?? []),
|
|
417
419
|
].map((profile) => [profile.id, profile]));
|
|
@@ -1098,16 +1100,18 @@ export class Engine {
|
|
|
1098
1100
|
session = openedResult.opened.session;
|
|
1099
1101
|
this.stampRunToolContext(toolCtx, session, options);
|
|
1100
1102
|
const sessionRun = runWithSid(session.state.sessionId, async () => {
|
|
1101
|
-
const hookMessages =
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1103
|
+
const hookMessages = profile?.disableHooks
|
|
1104
|
+
? []
|
|
1105
|
+
: await this.runSessionStartHooks({
|
|
1106
|
+
session,
|
|
1107
|
+
task,
|
|
1108
|
+
cwd,
|
|
1109
|
+
runPermissionMode,
|
|
1110
|
+
resumedFromDisk,
|
|
1111
|
+
options,
|
|
1112
|
+
taskText,
|
|
1113
|
+
messages,
|
|
1114
|
+
});
|
|
1111
1115
|
const sid = session.state.sessionId;
|
|
1112
1116
|
const { contextManager, llmClientPromise, toolExecutor } = this.wireRunContextAndPermission({
|
|
1113
1117
|
session,
|
|
@@ -1935,16 +1939,26 @@ export class Engine {
|
|
|
1935
1939
|
(visibilityStoredGoal !== undefined && visibilityStoredGoal.paused !== true) ||
|
|
1936
1940
|
(visibilityDefaultGoal !== undefined && visibilityDefaultGoal.paused !== true));
|
|
1937
1941
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists(cwd, sessionProfileOverrides);
|
|
1942
|
+
// A profile whose tool allowlist excludes the Skill tool can never invoke
|
|
1943
|
+
// a skill, so the full skills listing would be dead context for every one
|
|
1944
|
+
// of its turns (e.g. the Pet manager) — inject none via an empty allowlist.
|
|
1945
|
+
const runAllowedToolNames = toolCtx.allowedToolNames;
|
|
1946
|
+
const profileCanUseSkills = !runAllowedToolNames || runAllowedToolNames.has(skillToolDef.name);
|
|
1938
1947
|
const promptComposer = new PromptComposer(buildPromptComposerConfig({
|
|
1939
1948
|
cwd,
|
|
1940
1949
|
model: this.config.llm.model,
|
|
1941
1950
|
preset: this.preset,
|
|
1942
|
-
customSystemPrompt:
|
|
1943
|
-
|
|
1951
|
+
customSystemPrompt: profile?.disableInstructions
|
|
1952
|
+
? undefined
|
|
1953
|
+
: this.config.customSystemPrompt,
|
|
1954
|
+
appendSystemPrompt: [
|
|
1955
|
+
profile?.disableInstructions ? undefined : this.config.appendSystemPrompt,
|
|
1956
|
+
profile?.systemPromptAppend,
|
|
1957
|
+
]
|
|
1944
1958
|
.filter(Boolean)
|
|
1945
1959
|
.join("\n\n") || undefined,
|
|
1946
1960
|
responseLanguage: this.config.responseLanguage,
|
|
1947
|
-
userProfile: this.config.userProfile,
|
|
1961
|
+
userProfile: profile?.disableInstructions ? undefined : this.config.userProfile,
|
|
1948
1962
|
workspaceProfile: runWorkspaceProfile,
|
|
1949
1963
|
// Read from the Session's own persisted state, so every turn — not just
|
|
1950
1964
|
// the first — carries the standing brief.
|
|
@@ -1954,14 +1968,21 @@ export class Engine {
|
|
|
1954
1968
|
instructionBoundaryFinder: (scanCwd) => resolveInstructionBoundary(scanCwd, this.capabilities),
|
|
1955
1969
|
disabledSkills,
|
|
1956
1970
|
disabledPlugins,
|
|
1957
|
-
skillAllowlist:
|
|
1971
|
+
skillAllowlist: profileCanUseSkills ? toolCtx.skillAllowlist : [],
|
|
1958
1972
|
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1973
|
+
memoryCurrentProjectOnly: profile?.memoryCurrentProjectOnly,
|
|
1974
|
+
disableInstructions: profile?.disableInstructions,
|
|
1975
|
+
disableMemoryContext: profile?.disableMemoryContext,
|
|
1976
|
+
disableCapabilityContext: profile?.disableCapabilityContext,
|
|
1977
|
+
disableSourcesContext: profile?.disableSourcesContext,
|
|
1978
|
+
goalToolState: !runAllowedToolNames ||
|
|
1979
|
+
runAllowedToolNames.has("complete_goal") ||
|
|
1980
|
+
runAllowedToolNames.has("cancel_goal")
|
|
1962
1981
|
? { hasGoal: hasRunnableGoal }
|
|
1963
1982
|
: undefined,
|
|
1964
|
-
capabilityPromptSections:
|
|
1983
|
+
capabilityPromptSections: profile?.disableCapabilityContext
|
|
1984
|
+
? {}
|
|
1985
|
+
: this.capabilityPromptSections,
|
|
1965
1986
|
dynamicContextProviders: this.capabilityDynamicContextProviders,
|
|
1966
1987
|
getSettingsManager: () => this.getSettingsManager(),
|
|
1967
1988
|
toolCatalog: this.toolCatalog,
|
|
@@ -2018,7 +2039,7 @@ export class Engine {
|
|
|
2018
2039
|
toolRewriters: this.toolRewriters,
|
|
2019
2040
|
toolFeatureFlags: TOOL_FEATURE_FLAGS,
|
|
2020
2041
|
applyBuiltinOverrideVisibility,
|
|
2021
|
-
profileAllowedToolNames:
|
|
2042
|
+
profileAllowedToolNames: runAllowedToolNames,
|
|
2022
2043
|
runPlanMode,
|
|
2023
2044
|
});
|
|
2024
2045
|
return { promptComposer, toolDefs };
|
|
@@ -2228,13 +2249,28 @@ export class Engine {
|
|
|
2228
2249
|
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
2229
2250
|
* Returns the new model entry.
|
|
2230
2251
|
*
|
|
2252
|
+
* Per-session switches (`persist:false`) must not mutate the shared
|
|
2253
|
+
* ModelPool active key. Every desktop ChatSession owns an Engine, but those
|
|
2254
|
+
* Engines share one runtime pool; changing the pool selection here made the
|
|
2255
|
+
* next newly-created Session inherit whichever model another Session had
|
|
2256
|
+
* just selected (for example a Mimi manager model leaking into its delegated
|
|
2257
|
+
* Work Session).
|
|
2258
|
+
*
|
|
2231
2259
|
* Persists settings.defaults.text (= the connection id / pool key) so the
|
|
2232
2260
|
* next process startup defaults to the same model — without this, switches
|
|
2233
2261
|
* only live in memory and every restart reverts to the previously persisted
|
|
2234
2262
|
* defaults.text.
|
|
2235
2263
|
*/
|
|
2236
2264
|
switchModel(key, opts) {
|
|
2237
|
-
const
|
|
2265
|
+
const perSession = opts?.persist === false;
|
|
2266
|
+
const entry = perSession ? this.modelPool.get(key) : this.modelPool.switch(key);
|
|
2267
|
+
if (!entry) {
|
|
2268
|
+
const available = this.modelPool
|
|
2269
|
+
.list()
|
|
2270
|
+
.map((candidate) => candidate.key)
|
|
2271
|
+
.join(", ");
|
|
2272
|
+
throw new Error(`Model "${key}" not found. Available: ${available}`);
|
|
2273
|
+
}
|
|
2238
2274
|
// LLMConfig is pure model identity now — rotate it wholesale. Cross-model
|
|
2239
2275
|
// runtime knobs (temperature/timeout/retryMaxAttempts/imageDetail) live on
|
|
2240
2276
|
// this.config.clientDefaults and survive the switch untouched.
|
|
@@ -2243,7 +2279,7 @@ export class Engine {
|
|
|
2243
2279
|
// persist: false is the per-session path (ChatSession) — switching one
|
|
2244
2280
|
// session's model must not rewrite settings.defaults.text, the boot
|
|
2245
2281
|
// default every future session inherits.
|
|
2246
|
-
if (
|
|
2282
|
+
if (!perSession)
|
|
2247
2283
|
this.persistActiveModel(entry);
|
|
2248
2284
|
return entry;
|
|
2249
2285
|
}
|
|
@@ -2839,33 +2875,149 @@ export class Engine {
|
|
|
2839
2875
|
return contextManager;
|
|
2840
2876
|
}
|
|
2841
2877
|
/**
|
|
2842
|
-
* Archive a
|
|
2843
|
-
*
|
|
2844
|
-
*
|
|
2845
|
-
*
|
|
2846
|
-
*
|
|
2847
|
-
*
|
|
2848
|
-
*
|
|
2849
|
-
*
|
|
2850
|
-
|
|
2851
|
-
|
|
2878
|
+
* Archive a contiguous message window of a session into a single anchored
|
|
2879
|
+
* summary, leaving everything outside the window untouched, and cache the
|
|
2880
|
+
* result so a later forceCompact/resume reads the archived history. This is
|
|
2881
|
+
* a generic range-archival facade over ContextManager.summarizeRange.
|
|
2882
|
+
* Returns token stats before/after; equal before/after means the window was
|
|
2883
|
+
* empty/unresolvable or the summary was rejected.
|
|
2884
|
+
*
|
|
2885
|
+
* Window resolution depends on whether the caller supplied anchors:
|
|
2886
|
+
* - With `anchors`, the caller's `range` is IGNORED and the window is
|
|
2887
|
+
* resolved from the client-message-id anchors over a fresh, marker-aware
|
|
2888
|
+
* transcript replay. Callers (the pet segment closure) compute index
|
|
2889
|
+
* ranges over the RAW transcript message list, whose indices grow
|
|
2890
|
+
* forever — but the live list is trimmed by every persisted marker
|
|
2891
|
+
* (replay after restart) and by each in-process archival, so a raw index
|
|
2892
|
+
* range clamps to an empty window or, worse, onto the WRONG tail
|
|
2893
|
+
* messages, which would then be persistently mis-summarized against
|
|
2894
|
+
* correct anchors. Anchors are the only stable coordinates.
|
|
2895
|
+
* - Without `anchors`, legacy behavior: `range` is a half-open index window
|
|
2896
|
+
* over the cached/in-process message list, and nothing is persisted.
|
|
2897
|
+
*/
|
|
2898
|
+
async archiveTurnRange(sessionId, range, anchors) {
|
|
2852
2899
|
const effectiveSessionId = sessionId || this.lastSessionId;
|
|
2853
2900
|
if (!effectiveSessionId)
|
|
2854
2901
|
return { before: 0, after: 0 };
|
|
2855
2902
|
const session = this.sessionManager.resume(effectiveSessionId);
|
|
2856
|
-
|
|
2903
|
+
let sourceMessages;
|
|
2904
|
+
let window;
|
|
2905
|
+
if (anchors) {
|
|
2906
|
+
const { messages: liveMessages, liveIndexByClientMessageId } = session.transcript.toMessagesWithIndex();
|
|
2907
|
+
sourceMessages = liveMessages;
|
|
2908
|
+
// An absent from-anchor means "from the beginning" — over the live
|
|
2909
|
+
// list that is index 0, which includes any previously replayed
|
|
2910
|
+
// from-less summary at the head, so summarizeRange merge-feeds it
|
|
2911
|
+
// (extractAnchoredSummary) instead of losing it.
|
|
2912
|
+
const start = anchors.fromClientMessageId !== undefined
|
|
2913
|
+
? liveIndexByClientMessageId.get(anchors.fromClientMessageId)
|
|
2914
|
+
: 0;
|
|
2915
|
+
const end = liveIndexByClientMessageId.get(anchors.toClientMessageId);
|
|
2916
|
+
if (start === undefined || end === undefined || end <= start) {
|
|
2917
|
+
// Fail open: no summarization, no persistence. Falling back to the
|
|
2918
|
+
// caller's raw range is exactly the bug this path exists to fix —
|
|
2919
|
+
// it would summarize the wrong messages and persist that against
|
|
2920
|
+
// the (correct) anchors, with segmentId dedupe blocking correction.
|
|
2921
|
+
logger.warn("engine.archive_range.anchor_window_unresolved", {
|
|
2922
|
+
sessionId: effectiveSessionId,
|
|
2923
|
+
segmentId: anchors.segmentId,
|
|
2924
|
+
fromClientMessageId: anchors.fromClientMessageId,
|
|
2925
|
+
toClientMessageId: anchors.toClientMessageId,
|
|
2926
|
+
});
|
|
2927
|
+
const before = estimateTokens(sourceMessages);
|
|
2928
|
+
return { before, after: before };
|
|
2929
|
+
}
|
|
2930
|
+
window = { start, end };
|
|
2931
|
+
}
|
|
2932
|
+
else {
|
|
2933
|
+
sourceMessages =
|
|
2934
|
+
this.compactedMessagesBySession.get(effectiveSessionId) ?? session.transcript.toMessages();
|
|
2935
|
+
window = range;
|
|
2936
|
+
}
|
|
2857
2937
|
const before = estimateTokens(sourceMessages);
|
|
2858
2938
|
const contextManager = await this.prepareContextManagerForSession(effectiveSessionId, session, sourceMessages, "archive_range");
|
|
2859
2939
|
// Range archival is initiated deliberately, not by a pressure heuristic;
|
|
2860
2940
|
// don't let a stale run callback retained on lastContextManager double-emit.
|
|
2861
2941
|
contextManager.setOnCompact(() => { });
|
|
2862
|
-
const archived = await contextManager.summarizeRange(sourceMessages,
|
|
2942
|
+
const archived = await contextManager.summarizeRange(sourceMessages, window);
|
|
2863
2943
|
const after = estimateTokens(archived);
|
|
2944
|
+
// Persist the boundary so a restart replays the trimmed context. Only when
|
|
2945
|
+
// summarizeRange actually replaced the span (identity return means empty
|
|
2946
|
+
// window / rejected summary / no summarizer available) and the caller
|
|
2947
|
+
// supplied stable anchors to record it against.
|
|
2948
|
+
if (anchors && archived !== sourceMessages) {
|
|
2949
|
+
const clampedStart = Math.max(0, Math.min(window.start, archived.length - 1));
|
|
2950
|
+
const summaryMessage = archived[clampedStart];
|
|
2951
|
+
const summaryText = typeof summaryMessage?.content === "string" ? summaryMessage.content : undefined;
|
|
2952
|
+
if (summaryText && this.hasLiveArchiveAnchors(session, anchors)) {
|
|
2953
|
+
session.transcript.appendRangeArchive({ summary: summaryText, ...anchors });
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
// In the anchored path this replaces any in-process-only cache state with
|
|
2957
|
+
// the fresh-replay-based result — safe, because the transcript is the
|
|
2958
|
+
// superset of the cache and pressure compaction re-runs if needed.
|
|
2864
2959
|
this.compactedMessagesBySession.set(effectiveSessionId, archived);
|
|
2865
2960
|
this.lastSessionId = effectiveSessionId;
|
|
2866
2961
|
this.lastMessages = archived;
|
|
2867
2962
|
return { before, after };
|
|
2868
2963
|
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Guard against writing a marker anchored to a client message id the
|
|
2966
|
+
* transcript doesn't actually contain. toMessages()'s replay silently
|
|
2967
|
+
* ignores a range_archive event whose anchor doesn't resolve (fail open),
|
|
2968
|
+
* so a bad anchor wouldn't corrupt anything on its own — but if the marker
|
|
2969
|
+
* carries a segmentId, appendRangeArchive's idempotency check treats that
|
|
2970
|
+
* segmentId as "already recorded" forever, with no retry path (e.g. a
|
|
2971
|
+
* one-time migration keyed "migration-v1" would be permanently burned on
|
|
2972
|
+
* its first, failed attempt). Reject before writing so the caller can
|
|
2973
|
+
* retry with a corrected anchor. fromClientMessageId is optional in the
|
|
2974
|
+
* data model (absent means "from the beginning"), so only validate it when
|
|
2975
|
+
* present; missing it is fail-open by design, same as the replay path.
|
|
2976
|
+
*/
|
|
2977
|
+
hasLiveArchiveAnchors(session, anchors) {
|
|
2978
|
+
if (!session.transcript.hasClientMessageId(anchors.toClientMessageId))
|
|
2979
|
+
return false;
|
|
2980
|
+
if (anchors.fromClientMessageId !== undefined &&
|
|
2981
|
+
!session.transcript.hasClientMessageId(anchors.fromClientMessageId)) {
|
|
2982
|
+
return false;
|
|
2983
|
+
}
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Persist an archive boundary WITHOUT a summarization call — the caller
|
|
2988
|
+
* already has the summary text (e.g. the one-time migration built from
|
|
2989
|
+
* pet journal entries). Wraps the plain text in the anchored-summary
|
|
2990
|
+
* envelope so replay and rolling-merge treat it like a real archive.
|
|
2991
|
+
* Returns false when the segmentId was already recorded (idempotent) OR
|
|
2992
|
+
* when the anchors don't resolve to real messages in this transcript —
|
|
2993
|
+
* see hasLiveArchiveAnchors for why a dead anchor must be rejected before
|
|
2994
|
+
* the (potentially one-shot) segmentId gets burned.
|
|
2995
|
+
*/
|
|
2996
|
+
async appendArchiveMarker(sessionId, marker) {
|
|
2997
|
+
const session = this.sessionManager.resume(sessionId);
|
|
2998
|
+
if (!this.hasLiveArchiveAnchors(session, marker)) {
|
|
2999
|
+
logger.warn("engine.archive_marker.dead_anchor", {
|
|
3000
|
+
sessionId,
|
|
3001
|
+
segmentId: marker.segmentId,
|
|
3002
|
+
toClientMessageId: marker.toClientMessageId,
|
|
3003
|
+
fromClientMessageId: marker.fromClientMessageId,
|
|
3004
|
+
});
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
3007
|
+
const wrapped = buildAnchoredSummaryMessage(marker.summary, {
|
|
3008
|
+
...(session.transcript.isPersistent()
|
|
3009
|
+
? { transcriptPath: session.transcript.getFilePath() }
|
|
3010
|
+
: {}),
|
|
3011
|
+
});
|
|
3012
|
+
const content = typeof wrapped.content === "string" ? wrapped.content : marker.summary;
|
|
3013
|
+
const appended = session.transcript.appendRangeArchive({ ...marker, summary: content });
|
|
3014
|
+
if (!appended)
|
|
3015
|
+
return false;
|
|
3016
|
+
// The in-memory cache (if any) predates the marker; drop it so the next
|
|
3017
|
+
// run rebuilds from the trimmed transcript replay.
|
|
3018
|
+
this.compactedMessagesBySession.delete(sessionId);
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
2869
3021
|
recordCacheReadDiagnostics(sessionId, sample) {
|
|
2870
3022
|
const result = this.promptCacheDiagnostics.record(sessionId, sample);
|
|
2871
3023
|
if (result.kind === "scope_changed") {
|
|
@@ -88,11 +88,13 @@ export async function finalizeRunSuccess(args) {
|
|
|
88
88
|
// the turn loop has resolved (completion, error, or abort). Handlers
|
|
89
89
|
// are notify-only — any returned messages are dropped because the run
|
|
90
90
|
// is already over and there's no next turn to inject into.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
if (profile?.disableHooks !== true) {
|
|
92
|
+
await args.emitHook("on_session_end", {
|
|
93
|
+
sessionId: session.state.sessionId,
|
|
94
|
+
reason: result.reason,
|
|
95
|
+
turnCount,
|
|
96
|
+
}, options?.signal);
|
|
97
|
+
}
|
|
96
98
|
// Ephemeral side chats must never leak into durable memory, even after
|
|
97
99
|
// the user explicitly elevates tool permissions for a turn. Lifecycle
|
|
98
100
|
// isolation is independent of the run-scoped behavior/permission mode.
|
|
@@ -106,7 +108,7 @@ export async function finalizeRunSuccess(args) {
|
|
|
106
108
|
// Reuses the already-resolved auxSummaryClient (aux model, cheap). Best-
|
|
107
109
|
// effort: failures never touch the run result. The renderer writes the
|
|
108
110
|
// title into the sidebar on receipt of the session_title stream event.
|
|
109
|
-
{
|
|
111
|
+
if (profile?.disableSessionTitle !== true) {
|
|
110
112
|
const messageEvents = session.transcript.getEvents("message");
|
|
111
113
|
const userMsgEvents = messageEvents.filter((e) => e.data.role === "user");
|
|
112
114
|
const userMsgCount = userMsgEvents.length;
|
|
@@ -113,7 +113,7 @@ export function openRunSession(args) {
|
|
|
113
113
|
else {
|
|
114
114
|
// Cold start: shape (2) reuses the host-supplied sid; shape (3)
|
|
115
115
|
// lets sessionManager generate one with nanoid.
|
|
116
|
-
session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind);
|
|
116
|
+
session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind, options?.ephemeral === true);
|
|
117
117
|
const userMsg = { role: "user", content: args.userMessageContent };
|
|
118
118
|
claimClientMessageId(session, options?.clientMessageId, "submit");
|
|
119
119
|
if (args.parsedTask.hasImages)
|
|
@@ -32,6 +32,11 @@ export interface RunPromptComposerConfigInput {
|
|
|
32
32
|
disabledPlugins: ComposerOptions["disabledPlugins"];
|
|
33
33
|
skillAllowlist: ComposerOptions["skillAllowlist"];
|
|
34
34
|
memoriesMaxAgeDays: ComposerOptions["memoriesMaxAgeDays"];
|
|
35
|
+
memoryCurrentProjectOnly?: ComposerOptions["memoryCurrentProjectOnly"];
|
|
36
|
+
disableInstructions?: ComposerOptions["disableInstructions"];
|
|
37
|
+
disableMemoryContext?: ComposerOptions["disableMemoryContext"];
|
|
38
|
+
disableCapabilityContext?: ComposerOptions["disableCapabilityContext"];
|
|
39
|
+
disableSourcesContext?: ComposerOptions["disableSourcesContext"];
|
|
35
40
|
goalToolState: ComposerOptions["goalToolState"];
|
|
36
41
|
capabilityPromptSections: ComposerOptions["capabilityPromptSections"];
|
|
37
42
|
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, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
27
27
|
return {
|
|
28
28
|
cwd,
|
|
29
29
|
model,
|
|
@@ -49,6 +49,11 @@ export function buildPromptComposerConfig(args) {
|
|
|
49
49
|
disabledPlugins,
|
|
50
50
|
skillAllowlist,
|
|
51
51
|
memoriesMaxAgeDays,
|
|
52
|
+
memoryCurrentProjectOnly,
|
|
53
|
+
disableInstructions,
|
|
54
|
+
disableMemoryContext,
|
|
55
|
+
disableCapabilityContext,
|
|
56
|
+
disableSourcesContext,
|
|
52
57
|
goalToolState,
|
|
53
58
|
capabilityPromptSections,
|
|
54
59
|
dynamicContextProviders,
|
|
@@ -51,8 +51,14 @@ export function buildRunToolContext(args) {
|
|
|
51
51
|
return reason;
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
|
+
skillAllowlist: options?.skillAllowlist !== undefined
|
|
55
|
+
? [...options.skillAllowlist]
|
|
56
|
+
: args.base.skillAllowlist,
|
|
54
57
|
};
|
|
55
|
-
if (
|
|
58
|
+
if (options?.toolAllowlist !== undefined) {
|
|
59
|
+
toolCtx.allowedToolNames = new Set(options.toolAllowlist);
|
|
60
|
+
}
|
|
61
|
+
else if (profile?.allowedToolNames) {
|
|
56
62
|
toolCtx.allowedToolNames = profile.allowedToolNames;
|
|
57
63
|
}
|
|
58
64
|
if (profile?.createRunServices) {
|
|
@@ -32,6 +32,27 @@ 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
|
+
/** Skip repository/user instruction discovery for this run. */
|
|
36
|
+
disableInstructions?: boolean;
|
|
37
|
+
/** Skip persistent-memory injection for this run. */
|
|
38
|
+
disableMemoryContext?: boolean;
|
|
39
|
+
/** Skip capability-owned volatile context providers for this run. */
|
|
40
|
+
disableCapabilityContext?: boolean;
|
|
41
|
+
/** Skip bound-source metadata injection for this run. */
|
|
42
|
+
disableSourcesContext?: boolean;
|
|
43
|
+
/** Ignore the project's default digital-human profile for this run. */
|
|
44
|
+
disableWorkspaceProfile?: boolean;
|
|
45
|
+
/** Skip SessionStart/UserPromptSubmit/SessionEnd hooks for this run. */
|
|
46
|
+
disableHooks?: boolean;
|
|
47
|
+
/** Skip auxiliary title generation for this run. */
|
|
48
|
+
disableSessionTitle?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* When true, the injected persistent-memory index is trimmed to this run's
|
|
51
|
+
* project: global-layer project-type / dream-scope records tied to other
|
|
52
|
+
* projects are dropped. Meant for manager-style profiles whose runs never
|
|
53
|
+
* work inside other repos.
|
|
54
|
+
*/
|
|
55
|
+
memoryCurrentProjectOnly?: boolean;
|
|
35
56
|
/**
|
|
36
57
|
* Wrapper tag for host-provided runtime context injected at the system
|
|
37
58
|
* prompt tail (e.g. "pet-world"). Injection happens only when both this tag
|
|
@@ -60,6 +81,10 @@ export interface RunBehaviorProfile {
|
|
|
60
81
|
export declare const QUICK_CHAT_RESTRICTED_SYSTEM_PROMPT = "# Side Conversation Boundary\n\nThis is a side conversation, not the main-thread task execution environment.\n- Treat all content before this boundary as reference history only. Do not proactively continue any earlier plan, task, or modification.\n- Default to answering the user's question directly. Use lightweight read-only exploration only when needed.\n- Do not modify files, git state, configuration, or permissions unless the user explicitly asks after this boundary (for example, \"Allow you to modify files, please help me...\" or \"Please directly edit...\"). When explicitly requested, use the normally available tools subject to the current permission and approval mode.\n- Sub-agents are disabled for this side conversation. Do not create or invoke sub-agents.";
|
|
61
82
|
/** The side-conversation restriction expressed as a generic behavior profile. */
|
|
62
83
|
export declare const QUICK_CHAT_RESTRICTED_PROFILE: RunBehaviorProfile;
|
|
84
|
+
export declare const ISOLATED_TASK_BEHAVIOR_MODE: "isolatedTask";
|
|
85
|
+
export declare const ISOLATED_TASK_SYSTEM_PROMPT = "# Isolated Task Boundary\n\nThis is a bounded Task, not a continuation of any user conversation.\n- Use only the explicit Task input, the visible tool surface, and the selected Skill when one is available.\n- Do not infer or search for prior Session history, persistent memory, unrelated Skills, or other projects.\n- Do not create sub-agents or start unrelated work.\n- Finish the requested outcome, then return a concise result suitable for the owning application.";
|
|
86
|
+
/** Minimal, process-local run profile used by host-owned Tasks. */
|
|
87
|
+
export declare const ISOLATED_TASK_PROFILE: RunBehaviorProfile;
|
|
63
88
|
export interface EngineRunOptions {
|
|
64
89
|
cwd?: string;
|
|
65
90
|
onStream?: StreamCallback;
|
|
@@ -76,6 +101,12 @@ export interface EngineRunOptions {
|
|
|
76
101
|
attachments?: InputAttachmentMeta[];
|
|
77
102
|
/** Named per-run behavior profile supplied by interactive product surfaces. */
|
|
78
103
|
behaviorMode?: RunBehaviorMode;
|
|
104
|
+
/** Per-run hard tool allowlist. An empty list exposes no tools. */
|
|
105
|
+
toolAllowlist?: readonly string[];
|
|
106
|
+
/** Per-run hard Skill allowlist. An empty list exposes no Skills. */
|
|
107
|
+
skillAllowlist?: readonly string[];
|
|
108
|
+
/** Keep a fresh Session in process memory only and omit it from Session pickers. */
|
|
109
|
+
ephemeral?: boolean;
|
|
79
110
|
/**
|
|
80
111
|
* Generic per-run parameters consumed by the active behavior profile
|
|
81
112
|
* (createRunServices / buildVisibilityMeta / runtime-context injection).
|
package/dist/engine/run-types.js
CHANGED
|
@@ -10,3 +10,24 @@ export const QUICK_CHAT_RESTRICTED_PROFILE = {
|
|
|
10
10
|
id: "quickChatRestricted",
|
|
11
11
|
systemPromptAppend: QUICK_CHAT_RESTRICTED_SYSTEM_PROMPT,
|
|
12
12
|
};
|
|
13
|
+
export const ISOLATED_TASK_BEHAVIOR_MODE = "isolatedTask";
|
|
14
|
+
export const ISOLATED_TASK_SYSTEM_PROMPT = `# Isolated Task Boundary
|
|
15
|
+
|
|
16
|
+
This is a bounded Task, not a continuation of any user conversation.
|
|
17
|
+
- Use only the explicit Task input, the visible tool surface, and the selected Skill when one is available.
|
|
18
|
+
- Do not infer or search for prior Session history, persistent memory, unrelated Skills, or other projects.
|
|
19
|
+
- Do not create sub-agents or start unrelated work.
|
|
20
|
+
- Finish the requested outcome, then return a concise result suitable for the owning application.`;
|
|
21
|
+
/** Minimal, process-local run profile used by host-owned Tasks. */
|
|
22
|
+
export const ISOLATED_TASK_PROFILE = {
|
|
23
|
+
id: ISOLATED_TASK_BEHAVIOR_MODE,
|
|
24
|
+
systemPromptAppend: ISOLATED_TASK_SYSTEM_PROMPT,
|
|
25
|
+
disableMcp: true,
|
|
26
|
+
disableInstructions: true,
|
|
27
|
+
disableMemoryContext: true,
|
|
28
|
+
disableCapabilityContext: true,
|
|
29
|
+
disableSourcesContext: true,
|
|
30
|
+
disableWorkspaceProfile: true,
|
|
31
|
+
disableHooks: true,
|
|
32
|
+
disableSessionTitle: true,
|
|
33
|
+
};
|