@cjhyy/code-shell-core 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/automation/scheduler.d.ts +3 -0
- package/dist/automation/scheduler.js +21 -0
- package/dist/context/manager.d.ts +8 -2
- package/dist/context/manager.js +20 -4
- package/dist/context/notes.d.ts +39 -0
- package/dist/context/notes.js +314 -0
- package/dist/credentials/access.d.ts +9 -1
- package/dist/credentials/access.js +15 -4
- package/dist/credentials/store.d.ts +11 -0
- package/dist/credentials/store.js +44 -9
- package/dist/credentials/types.d.ts +4 -0
- package/dist/credentials/types.js +4 -0
- package/dist/credentials/use-credential-tool.js +12 -2
- package/dist/engine/engine-workspace-authority.js +10 -3
- package/dist/engine/engine.d.ts +8 -4
- package/dist/engine/engine.js +115 -15
- package/dist/engine/prompt-cache-diagnostics.js +10 -1
- package/dist/engine/run-goal.js +7 -3
- package/dist/engine/run-tooling.d.ts +3 -0
- package/dist/engine/run-tooling.js +25 -23
- package/dist/engine/run-types.d.ts +20 -0
- package/dist/engine/run-workspace.js +5 -21
- package/dist/engine/subagent-spawner.d.ts +3 -0
- package/dist/engine/subagent-spawner.js +48 -17
- package/dist/engine/turn-loop.d.ts +14 -4
- package/dist/engine/turn-loop.js +120 -28
- package/dist/engine/types.d.ts +19 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/links/index.d.ts +1 -0
- package/dist/links/index.js +1 -0
- package/dist/links/link-action-tool.d.ts +2 -1
- package/dist/links/link-action-tool.js +80 -44
- package/dist/links/status.d.ts +53 -0
- package/dist/links/status.js +175 -0
- package/dist/llm/prompt-cache.d.ts +35 -3
- package/dist/llm/prompt-cache.js +63 -3
- package/dist/llm/providers/openai.d.ts +3 -0
- package/dist/llm/providers/openai.js +57 -14
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/browser.md +4 -2
- package/dist/prompt/sections/context-notes.md +9 -0
- package/dist/protocol/background-result-wakeup.d.ts +8 -1
- package/dist/protocol/background-result-wakeup.js +76 -38
- package/dist/protocol/chat-session-manager.d.ts +7 -1
- package/dist/protocol/chat-session-manager.js +44 -8
- package/dist/protocol/chat-session.d.ts +2 -0
- package/dist/protocol/chat-session.js +4 -1
- package/dist/protocol/server.d.ts +4 -0
- package/dist/protocol/server.js +355 -62
- package/dist/protocol/session-message-result.d.ts +12 -0
- package/dist/protocol/session-message-result.js +42 -0
- package/dist/protocol/session-message-workspace.d.ts +21 -0
- package/dist/protocol/session-message-workspace.js +57 -0
- package/dist/protocol/types.d.ts +4 -0
- package/dist/session/session-manager.js +8 -7
- package/dist/session/session-message.d.ts +17 -2
- package/dist/session/transcript.d.ts +17 -0
- package/dist/session/transcript.js +271 -16
- package/dist/settings/schema.d.ts +9 -0
- package/dist/settings/schema.js +4 -0
- package/dist/themes/paths.js +20 -1
- package/dist/tool-system/browser-bridge.d.ts +21 -1
- package/dist/tool-system/browser-discovery.d.ts +6 -0
- package/dist/tool-system/browser-discovery.js +17 -0
- package/dist/tool-system/builtin/agent.js +23 -9
- package/dist/tool-system/builtin/browser-tools.js +30 -9
- package/dist/tool-system/builtin/context-notes.d.ts +12 -0
- package/dist/tool-system/builtin/context-notes.js +188 -0
- package/dist/tool-system/builtin/index.js +50 -3
- package/dist/tool-system/builtin/mcp-tools.d.ts +5 -3
- package/dist/tool-system/builtin/mcp-tools.js +10 -10
- package/dist/tool-system/builtin/send-message-to-session.js +19 -3
- package/dist/tool-system/builtin/tool-search.js +15 -3
- package/dist/tool-system/context.d.ts +9 -0
- package/dist/tool-system/executor.js +5 -3
- package/dist/tool-system/mcp-compat.d.ts +3 -0
- package/dist/tool-system/mcp-compat.js +51 -0
- package/dist/tool-system/mcp-manager.d.ts +27 -26
- package/dist/tool-system/mcp-manager.js +273 -111
- package/dist/tool-system/mcp-workspace.d.ts +18 -0
- package/dist/tool-system/mcp-workspace.js +56 -0
- package/dist/tool-system/permission.d.ts +6 -0
- package/dist/tool-system/permission.js +45 -9
- package/dist/tool-system/plan-mode-allowlist.js +5 -0
- package/dist/tool-system/sandbox/seatbelt.js +71 -2
- package/dist/tool-system/session-tool-host.js +9 -1
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
|
@@ -126,30 +126,26 @@ export function buildRunPermissionPipeline(args) {
|
|
|
126
126
|
}
|
|
127
127
|
/** engine.ts L1986-2003 —— MCP 连接(Runtime 池优先,回退 per-Engine)。 */
|
|
128
128
|
export async function connectRunMcp(args) {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const mcpDisabled = args.mcpDisabled;
|
|
136
|
-
if (!mcpDisabled && Object.keys(mcpServers).length > 0 && !args.getManager()) {
|
|
137
|
-
if (args.runtimePool) {
|
|
138
|
-
args.setManager(args.runtimePool);
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
args.setManager(new MCPManager(args.toolRegistry));
|
|
142
|
-
}
|
|
143
|
-
await args.getManager().connectAll(mcpServers, args.engineForConnect, (event) => {
|
|
144
|
-
// Fire-and-forget onto the notification hook: hosts/plugins learn
|
|
145
|
-
// about MCP availability changes without polling the pool.
|
|
146
|
-
args.emitNotificationHook({
|
|
147
|
-
kind: event.type,
|
|
148
|
-
server: event.server,
|
|
149
|
-
...(event.error ? { error: event.error } : {}),
|
|
150
|
-
});
|
|
151
|
-
});
|
|
129
|
+
// A pool may outlive a run and the engine registry may predate discovery.
|
|
130
|
+
// Refresh scope and the engine's MCP-only view on every run (including cwd
|
|
131
|
+
// and project changes); identical scopes still share a single transport.
|
|
132
|
+
const enabledServers = args.mcpDisabled ? {} : args.mcpServers;
|
|
133
|
+
if (!args.getManager() && Object.keys(enabledServers).length > 0) {
|
|
134
|
+
args.setManager(args.runtimePool ?? new MCPManager(args.toolRegistry));
|
|
152
135
|
}
|
|
136
|
+
const manager = args.getManager();
|
|
137
|
+
if (!manager)
|
|
138
|
+
return;
|
|
139
|
+
await manager.connectAll(enabledServers, args.engineForConnect, (event) => {
|
|
140
|
+
args.emitNotificationHook({
|
|
141
|
+
kind: event.type,
|
|
142
|
+
server: event.server,
|
|
143
|
+
...(event.error ? { error: event.error } : {}),
|
|
144
|
+
});
|
|
145
|
+
}, args.toolContext);
|
|
146
|
+
manager.syncToolsToRegistry(args.toolRegistry, args.toolContext, new Set(Object.entries(enabledServers)
|
|
147
|
+
.filter(([, config]) => config.enabled !== false)
|
|
148
|
+
.map(([name]) => name)));
|
|
153
149
|
}
|
|
154
150
|
/**
|
|
155
151
|
* Build the availability-guard context for a tool surface.
|
|
@@ -167,11 +163,15 @@ export async function connectRunMcp(args) {
|
|
|
167
163
|
export function buildToolVisibility(inputs) {
|
|
168
164
|
return {
|
|
169
165
|
cwd: inputs.cwd,
|
|
166
|
+
...(inputs.contextStrategy ? { contextStrategy: inputs.contextStrategy } : {}),
|
|
170
167
|
...(inputs.workspace ? { workspace: inputs.workspace } : {}),
|
|
171
168
|
hasGoal: inputs.hasGoal,
|
|
172
169
|
...(inputs.sessionId ? { sessionId: inputs.sessionId } : {}),
|
|
173
170
|
...(inputs.settingsScope ? { settingsScope: inputs.settingsScope } : {}),
|
|
174
171
|
...(inputs.host !== undefined ? { host: inputs.host } : {}),
|
|
172
|
+
...(inputs.hasBrowserAutomation !== undefined
|
|
173
|
+
? { hasBrowserAutomation: inputs.hasBrowserAutomation }
|
|
174
|
+
: {}),
|
|
175
175
|
...(inputs.isSubAgent !== undefined ? { isSubAgent: inputs.isSubAgent } : {}),
|
|
176
176
|
...(inputs.behaviorProfile !== undefined ? { behaviorProfile: inputs.behaviorProfile } : {}),
|
|
177
177
|
...(inputs.sessionMessageTargets?.length
|
|
@@ -188,11 +188,13 @@ export function assembleRunToolDefs(args) {
|
|
|
188
188
|
const guardCwd = args.guardCwd;
|
|
189
189
|
const toolVisibility = buildToolVisibility({
|
|
190
190
|
cwd: guardCwd,
|
|
191
|
+
contextStrategy: toolCtx.contextNotes ? toolCtx.contextStrategy : undefined,
|
|
191
192
|
workspace: toolCtx.workspace,
|
|
192
193
|
hasGoal: args.hasRunnableGoal,
|
|
193
194
|
sessionId: toolCtx.sessionId,
|
|
194
195
|
settingsScope: args.settingsScope,
|
|
195
196
|
host: args.builtinToolHost,
|
|
197
|
+
hasBrowserAutomation: toolCtx.browser !== undefined,
|
|
196
198
|
isSubAgent: args.isSubAgent,
|
|
197
199
|
behaviorProfile: args.behaviorProfileId,
|
|
198
200
|
sessionMessageTargets: toolCtx.sessionMessages?.targets,
|
|
@@ -22,10 +22,19 @@ export type RunBehaviorMode = string;
|
|
|
22
22
|
*/
|
|
23
23
|
export interface RunBehaviorProfile {
|
|
24
24
|
id: string;
|
|
25
|
+
/** Default context policy; explicit Engine/settings choices take precedence. */
|
|
26
|
+
contextStrategy?: "summary" | "notes";
|
|
25
27
|
/** Appended to the system prompt after config.appendSystemPrompt. */
|
|
26
28
|
systemPromptAppend?: string;
|
|
27
29
|
/** Hard tool allowlist for the run (model visibility + execution gate). */
|
|
28
30
|
allowedToolNames?: ReadonlySet<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Positive safe-integer ceiling on model/tool turns. Clamps the existing
|
|
33
|
+
* resolved config/Goal limit, including during live Goal edits/extensions.
|
|
34
|
+
* Invalid values are ignored. The existing no-tools final summary may run
|
|
35
|
+
* once after this ceiling is reached.
|
|
36
|
+
*/
|
|
37
|
+
maxTurns?: number;
|
|
29
38
|
/** When set, the run's permission mode is locked to this value. */
|
|
30
39
|
forcePermissionMode?: NonNullable<EngineConfig["permissionMode"]>;
|
|
31
40
|
/** When true, per-run planMode requests are ignored. */
|
|
@@ -70,6 +79,13 @@ export interface RunBehaviorProfile {
|
|
|
70
79
|
profileParams: Readonly<Record<string, unknown>>;
|
|
71
80
|
reportResult: (key: string, value: unknown) => void;
|
|
72
81
|
}) => Record<string, unknown>;
|
|
82
|
+
/**
|
|
83
|
+
* Called with this run's services after each new steered user message is
|
|
84
|
+
* persisted, before its injection event and the next model step. Profiles
|
|
85
|
+
* may invalidate deferred drafts that answered an earlier input revision.
|
|
86
|
+
* Initial input, queued/revoked steers and duplicate messages do not call it.
|
|
87
|
+
*/
|
|
88
|
+
onUserInputChanged?: (services: Record<string, unknown>) => void;
|
|
73
89
|
/**
|
|
74
90
|
* Per-run metadata exposed to builtin availability guards / definition
|
|
75
91
|
* rewriters via ToolVisibilityContext.profileMeta.
|
|
@@ -98,6 +114,8 @@ export interface EngineRunOptions {
|
|
|
98
114
|
planMode?: boolean;
|
|
99
115
|
approvalRouter?: ApprovalRouter;
|
|
100
116
|
goal?: string | GoalConfig;
|
|
117
|
+
/** Run this standalone turn without explicit, persisted, or configured Goal mode. */
|
|
118
|
+
disableGoal?: boolean;
|
|
101
119
|
injected?: boolean;
|
|
102
120
|
clientMessageId?: string;
|
|
103
121
|
/**
|
|
@@ -170,6 +188,8 @@ export type AgentRuntimeProgressEvent = {
|
|
|
170
188
|
usage: TokenUsage;
|
|
171
189
|
};
|
|
172
190
|
export interface ChildEngineRuntime {
|
|
191
|
+
/** Release child-local resources after the entire supervised lifetime settles. */
|
|
192
|
+
dispose?(): void | Promise<void>;
|
|
173
193
|
run(task: string, options?: EngineRunOptions): Promise<EngineResult>;
|
|
174
194
|
setAgentControlStateListener(listener: ((state: LiveChildState) => void) | undefined): void;
|
|
175
195
|
setAgentDirectionsDeliveredListener?(listener: ((envelopeIds: string[]) => void) | undefined): void;
|
|
@@ -54,30 +54,12 @@ export async function resolveRunWorkspace(args) {
|
|
|
54
54
|
? await args.sessionManager.resolveSessionWorkspaceForResume(options.sessionId)
|
|
55
55
|
: undefined;
|
|
56
56
|
if (workspaceResume && !workspaceResume.ok) {
|
|
57
|
-
return
|
|
58
|
-
ok: false,
|
|
59
|
-
result: {
|
|
60
|
-
text: `ERROR: ${workspaceResume.message}`,
|
|
61
|
-
reason: "completed",
|
|
62
|
-
sessionId: options.sessionId,
|
|
63
|
-
turnCount: 0,
|
|
64
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
57
|
+
return workspaceError(options?.sessionId, workspaceResume.message);
|
|
67
58
|
}
|
|
68
59
|
if (workspaceResume?.ok &&
|
|
69
60
|
workspaceResume.reason === "worktree_missing_branch_gone" &&
|
|
70
61
|
workspaceResume.message) {
|
|
71
|
-
return
|
|
72
|
-
ok: false,
|
|
73
|
-
result: {
|
|
74
|
-
text: workspaceResume.message,
|
|
75
|
-
reason: "completed",
|
|
76
|
-
sessionId: options.sessionId,
|
|
77
|
-
turnCount: 0,
|
|
78
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
79
|
-
},
|
|
80
|
-
};
|
|
62
|
+
return workspaceError(options?.sessionId, workspaceResume.message);
|
|
81
63
|
}
|
|
82
64
|
// Existing P1 sessions resolve cwd from SessionWorkspace, even if the host
|
|
83
65
|
// passes a stale cwd. Legacy sessions without workspace keep the historical
|
|
@@ -158,7 +140,9 @@ function workspaceError(sessionId, message) {
|
|
|
158
140
|
ok: false,
|
|
159
141
|
result: {
|
|
160
142
|
text: `ERROR: ${message}`,
|
|
161
|
-
|
|
143
|
+
// Like other initialization failures, this run never reached a model
|
|
144
|
+
// turn. Hosts must see a failure rather than report a completed delivery.
|
|
145
|
+
reason: "model_error",
|
|
162
146
|
sessionId: sessionId ?? "workspace-invalid",
|
|
163
147
|
turnCount: 0,
|
|
164
148
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
@@ -17,6 +17,9 @@ export declare function resolveChildMcpServers(allowlist: string[] | undefined,
|
|
|
17
17
|
export declare function wrapChildStream(destination: StreamCallback | undefined, agentId: string): StreamCallback | undefined;
|
|
18
18
|
export interface CreateSubAgentSpawnerDeps {
|
|
19
19
|
parentConfig: EngineConfig;
|
|
20
|
+
parentSessionId?: string;
|
|
21
|
+
/** Tool context is assembled before the run session is initialized. */
|
|
22
|
+
getParentSessionId?: () => string;
|
|
20
23
|
/** Fully resolved sandbox for this parent run, including settings layers. */
|
|
21
24
|
parentSandbox: SandboxConfig;
|
|
22
25
|
presetName: AgentPresetName;
|
|
@@ -252,7 +252,11 @@ export function createSubAgentSpawner(deps) {
|
|
|
252
252
|
maxContextTokens: deps.parentConfig.maxContextTokens ?? 200_000,
|
|
253
253
|
sessionStorageDir: deps.parentConfig.sessionStorageDir,
|
|
254
254
|
headless: deps.parentConfig.headless,
|
|
255
|
-
readOnlySession: request.readOnlySession,
|
|
255
|
+
readOnlySession: deps.parentConfig.readOnlySession || request.readOnlySession,
|
|
256
|
+
projectTrusted: deps.parentConfig.projectTrusted,
|
|
257
|
+
allowBackgroundShells: deps.parentConfig.allowBackgroundShells,
|
|
258
|
+
approvalBackend: deps.parentConfig.approvalBackend,
|
|
259
|
+
approvalRouter: deps.parentConfig.approvalRouter,
|
|
256
260
|
skillAllowlist: request.skillAllowlist,
|
|
257
261
|
sandbox: resolveChildSandbox(request.sandboxMode, deps.parentSandbox),
|
|
258
262
|
mcpServers: resolveChildMcpServers(request.mcpAllowlist, deps.parentConfig.mcpServers),
|
|
@@ -261,28 +265,55 @@ export function createSubAgentSpawner(deps) {
|
|
|
261
265
|
};
|
|
262
266
|
const destination = request.streamOverride ?? deps.parentStream;
|
|
263
267
|
const childSessionId = request.resumeSessionId ?? request.agentId;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
const parentSessionId = deps.getParentSessionId?.() ?? deps.parentSessionId;
|
|
269
|
+
const host = parentSessionId
|
|
270
|
+
? deps.parentConfig.createChildHostBindings?.({
|
|
271
|
+
parentSessionId,
|
|
272
|
+
sessionId: childSessionId,
|
|
273
|
+
signal: request.signal,
|
|
274
|
+
})
|
|
275
|
+
: undefined;
|
|
276
|
+
if (host) {
|
|
277
|
+
childConfig.browserBridge = host.browserBridge;
|
|
278
|
+
childConfig.askUser = host.askUser;
|
|
279
|
+
childConfig.injectCredentialToBrowser = host.injectCredentialToBrowser;
|
|
280
|
+
childConfig.approvalBackend = host.approvalBackend ?? childConfig.approvalBackend;
|
|
281
|
+
}
|
|
282
|
+
let runtime;
|
|
283
|
+
try {
|
|
284
|
+
if (deps.childRunner.createChild && request.runtimeGeneration !== undefined) {
|
|
285
|
+
runtime = deps.childRunner.createChild(childConfig);
|
|
286
|
+
const supervisor = new ChildRunSupervisor(runtime, childSessionId, request.runtimeGeneration);
|
|
287
|
+
const ownerToken = nanoid();
|
|
288
|
+
const bound = request.bindLiveControl?.(supervisor, ownerToken);
|
|
289
|
+
supervisor.setCloseIntake(() => request.closeLiveControl?.(supervisor, bound && bound !== true ? bound : undefined));
|
|
290
|
+
if (bound === false) {
|
|
291
|
+
throw new Error(`child session ${childSessionId} already has a live transcript writer`);
|
|
292
|
+
}
|
|
293
|
+
host?.activate?.();
|
|
294
|
+
return await supervisor.run(request.prompt, {
|
|
295
|
+
signal: request.signal,
|
|
296
|
+
onStream: wrapChildRuntimeStream(destination, request.agentId, request.onProgressEvent),
|
|
297
|
+
sessionId: childSessionId,
|
|
298
|
+
onAgentProgress: request.onAgentProgress,
|
|
299
|
+
});
|
|
272
300
|
}
|
|
273
|
-
|
|
301
|
+
host?.activate?.();
|
|
302
|
+
const result = await deps.childRunner.runChild(childConfig, request.prompt, {
|
|
274
303
|
signal: request.signal,
|
|
275
304
|
onStream: wrapChildRuntimeStream(destination, request.agentId, request.onProgressEvent),
|
|
276
305
|
sessionId: childSessionId,
|
|
277
|
-
onAgentProgress: request.onAgentProgress,
|
|
278
306
|
});
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
try {
|
|
311
|
+
host?.dispose();
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await runtime?.dispose?.();
|
|
315
|
+
}
|
|
279
316
|
}
|
|
280
|
-
const result = await deps.childRunner.runChild(childConfig, request.prompt, {
|
|
281
|
-
signal: request.signal,
|
|
282
|
-
onStream: wrapChildRuntimeStream(destination, request.agentId, request.onProgressEvent),
|
|
283
|
-
sessionId: childSessionId,
|
|
284
|
-
});
|
|
285
|
-
return result;
|
|
286
317
|
},
|
|
287
318
|
};
|
|
288
319
|
}
|
|
@@ -11,6 +11,7 @@ import type { LiveChildState } from "../tool-system/builtin/agent-registry.js";
|
|
|
11
11
|
import { ModelFacade } from "./model-facade.js";
|
|
12
12
|
import { ToolExecutor } from "../tool-system/executor.js";
|
|
13
13
|
import { ContextManager } from "../context/manager.js";
|
|
14
|
+
import type { SessionContextNotes } from "../context/notes.js";
|
|
14
15
|
import { HookRegistry } from "../hooks/registry.js";
|
|
15
16
|
import type { GoalJudgeRuntimeContext } from "../hooks/goal-stop-hook.js";
|
|
16
17
|
import type { PromptCacheDiagnosticSample } from "./prompt-cache-diagnostics.js";
|
|
@@ -34,6 +35,8 @@ export interface TurnLoopConfig {
|
|
|
34
35
|
* summarized into durable history. Engine uses this for dynamicContext.
|
|
35
36
|
*/
|
|
36
37
|
volatileContextMessages?: Iterable<Message>;
|
|
38
|
+
/** Current instructions and lifecycle hooks, reattached after a notes transition. */
|
|
39
|
+
retainedContextMessages?: Iterable<Message>;
|
|
37
40
|
/**
|
|
38
41
|
* Fired after each turn boundary is recorded. Lets the engine flush an
|
|
39
42
|
* up-to-date snapshot to state.json mid-run, so a long run doesn't leave
|
|
@@ -66,6 +69,7 @@ export interface TurnLoopDeps {
|
|
|
66
69
|
model: ModelFacade;
|
|
67
70
|
toolExecutor: ToolExecutor;
|
|
68
71
|
contextManager: ContextManager;
|
|
72
|
+
contextNotes?: SessionContextNotes;
|
|
69
73
|
hooks: HookRegistry;
|
|
70
74
|
transcript: Transcript;
|
|
71
75
|
systemPrompt: string;
|
|
@@ -289,12 +293,18 @@ export declare class TurnLoop {
|
|
|
289
293
|
private stripVolatileContextMessages;
|
|
290
294
|
/**
|
|
291
295
|
* Keep volatile context out of compaction/summarization without moving it on
|
|
292
|
-
* every model round.
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
+
* every model round. Normalizing a newly appended tool result must not move
|
|
297
|
+
* an earlier volatile snapshot and invalidate an otherwise unchanged prefix.
|
|
298
|
+
* Keep its position when the preceding stable messages remain intact; reset
|
|
299
|
+
* to the tail when that boundary was rewritten or compaction changed length.
|
|
296
300
|
*/
|
|
297
301
|
private restoreVolatileAfterContextManagement;
|
|
302
|
+
private noteReminderIssued;
|
|
303
|
+
private readonly retainedHookMessages;
|
|
304
|
+
private readonly pendingContextReminders;
|
|
305
|
+
private appendContextReminder;
|
|
306
|
+
/** Called only before a model step or after every result in a tool batch is recorded. */
|
|
307
|
+
private applyNotesRollover;
|
|
298
308
|
private manageContextMessages;
|
|
299
309
|
private manageContextMessagesSync;
|
|
300
310
|
private markPendingImagesConsumed;
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -342,25 +342,114 @@ export class TurnLoop {
|
|
|
342
342
|
}
|
|
343
343
|
/**
|
|
344
344
|
* Keep volatile context out of compaction/summarization without moving it on
|
|
345
|
-
* every model round.
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
345
|
+
* every model round. Normalizing a newly appended tool result must not move
|
|
346
|
+
* an earlier volatile snapshot and invalidate an otherwise unchanged prefix.
|
|
347
|
+
* Keep its position when the preceding stable messages remain intact; reset
|
|
348
|
+
* to the tail when that boundary was rewritten or compaction changed length.
|
|
349
349
|
*/
|
|
350
350
|
restoreVolatileAfterContextManagement(original, stableInput, managedStable) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
351
|
+
if (stableInput.length === managedStable.length) {
|
|
352
|
+
// Some cleanup passes copy messages even when their final content is
|
|
353
|
+
// unchanged. Object identity is only a fast path, not a cache boundary.
|
|
354
|
+
const firstChangedIndex = stableInput.findIndex((message, index) => managedStable[index] !== message &&
|
|
355
|
+
JSON.stringify(managedStable[index]) !== JSON.stringify(message));
|
|
356
|
+
if (firstChangedIndex === -1)
|
|
357
|
+
return original;
|
|
358
|
+
let stableIndex = 0;
|
|
359
|
+
let lastVolatileBoundary = 0;
|
|
360
|
+
for (const message of original) {
|
|
361
|
+
if (this.volatileContextMessages.has(message)) {
|
|
362
|
+
lastVolatileBoundary = stableIndex;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
stableIndex++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (firstChangedIndex >= lastVolatileBoundary) {
|
|
369
|
+
// Apply the managed tail in place, including its persisted/truncated
|
|
370
|
+
// results. Never restore old payloads just to preserve cache hits.
|
|
371
|
+
stableIndex = 0;
|
|
372
|
+
return original.map((message) => this.volatileContextMessages.has(message) ? message : managedStable[stableIndex++]);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
355
375
|
const volatile = original.filter((message) => this.volatileContextMessages.has(message));
|
|
356
376
|
return [...managedStable, ...volatile];
|
|
357
377
|
}
|
|
378
|
+
noteReminderIssued = false;
|
|
379
|
+
retainedHookMessages = new Map();
|
|
380
|
+
pendingContextReminders = new Map();
|
|
381
|
+
appendContextReminder(messages, message, retainForRun = false) {
|
|
382
|
+
messages.push(message);
|
|
383
|
+
if (!this.deps.contextNotes)
|
|
384
|
+
return;
|
|
385
|
+
const target = retainForRun ? this.retainedHookMessages : this.pendingContextReminders;
|
|
386
|
+
target.set(JSON.stringify(message), message);
|
|
387
|
+
}
|
|
388
|
+
/** Called only before a model step or after every result in a tool batch is recorded. */
|
|
389
|
+
applyNotesRollover(messages) {
|
|
390
|
+
const notes = this.deps.contextNotes;
|
|
391
|
+
if (!notes ||
|
|
392
|
+
!notes.hasPendingRollover() ||
|
|
393
|
+
this.pendingImageMessages.size > 0 ||
|
|
394
|
+
this.sensitiveToolResultRedactions.size > 0) {
|
|
395
|
+
return messages;
|
|
396
|
+
}
|
|
397
|
+
if (this.config.signal?.aborted || this.goalControlStopRequested)
|
|
398
|
+
return messages;
|
|
399
|
+
const stable = this.stripVolatileContextMessages(messages);
|
|
400
|
+
const liveContent = new Set(stable.map((message) => JSON.stringify(message)));
|
|
401
|
+
for (const key of this.retainedHookMessages.keys()) {
|
|
402
|
+
if (!liveContent.has(key))
|
|
403
|
+
this.retainedHookMessages.delete(key);
|
|
404
|
+
}
|
|
405
|
+
const retainedByContent = new Map([
|
|
406
|
+
...(this.config.retainedContextMessages ?? []),
|
|
407
|
+
...this.retainedHookMessages.values(),
|
|
408
|
+
...this.pendingContextReminders.values(),
|
|
409
|
+
].map((message) => [JSON.stringify(message), message]));
|
|
410
|
+
const retained = [...retainedByContent.values()];
|
|
411
|
+
const checkpoint = notes.applyRollover(stable, retained);
|
|
412
|
+
if (!checkpoint)
|
|
413
|
+
return messages;
|
|
414
|
+
const managed = [...retained, ...checkpoint];
|
|
415
|
+
this.deps.contextManager.recordNotesCompaction(stable, managed);
|
|
416
|
+
this.noteReminderIssued = false;
|
|
417
|
+
return this.restoreVolatileAfterContextManagement(messages, stable, managed);
|
|
418
|
+
}
|
|
358
419
|
async manageContextMessages(messages) {
|
|
359
|
-
|
|
360
|
-
|
|
420
|
+
messages = this.applyNotesRollover(messages);
|
|
421
|
+
const notes = this.deps.contextNotes;
|
|
422
|
+
if (notes) {
|
|
423
|
+
const stable = this.stripVolatileContextMessages(messages);
|
|
424
|
+
const limits = this.deps.contextManager.checkLimits(stable);
|
|
425
|
+
if (limits.needsCompact) {
|
|
426
|
+
try {
|
|
427
|
+
notes.requestRollover();
|
|
428
|
+
messages = this.applyNotesRollover(messages);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// No usable note (or it was already consumed): existing summary is the fallback.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const nearBudget = this.deps.contextManager.shouldPrepareNote(this.stripVolatileContextMessages(messages));
|
|
435
|
+
if (!nearBudget)
|
|
436
|
+
this.noteReminderIssued = false;
|
|
437
|
+
const canSaveNotes = this.deps.tools.some((tool) => tool.name === "SaveContextNote");
|
|
438
|
+
if (nearBudget && !this.noteReminderIssued && canSaveNotes) {
|
|
439
|
+
messages = [
|
|
440
|
+
...messages,
|
|
441
|
+
{
|
|
442
|
+
role: "user",
|
|
443
|
+
content: "<system-reminder>Context budget is running low. Use SaveContextNote to preserve the current goal, latest corrections, decisions, verified work, pending questions and next steps. Then call NewContext in a separate tool batch and continue the same task. Original history remains available through SearchHistory.</system-reminder>",
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
this.noteReminderIssued = true;
|
|
447
|
+
}
|
|
361
448
|
}
|
|
362
449
|
const stable = this.stripVolatileContextMessages(messages);
|
|
363
|
-
const managed = await this.deps.contextManager.manageAsync(stable, this.config.signal
|
|
450
|
+
const managed = await this.deps.contextManager.manageAsync(stable, this.config.signal, {
|
|
451
|
+
preferNotes: notes !== undefined,
|
|
452
|
+
});
|
|
364
453
|
return this.restoreVolatileAfterContextManagement(messages, stable, managed);
|
|
365
454
|
}
|
|
366
455
|
manageContextMessagesSync(messages) {
|
|
@@ -647,7 +736,7 @@ export class TurnLoop {
|
|
|
647
736
|
});
|
|
648
737
|
const turnStartInjection = wrapHookMessages(turnStartHook.messages);
|
|
649
738
|
if (turnStartInjection) {
|
|
650
|
-
|
|
739
|
+
this.appendContextReminder(messages, turnStartInjection, true);
|
|
651
740
|
}
|
|
652
741
|
if (this.goalControlStopRequested) {
|
|
653
742
|
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
@@ -659,21 +748,21 @@ export class TurnLoop {
|
|
|
659
748
|
// cap, the limit a re-blocked goal actually hits first.)
|
|
660
749
|
const turnsRemaining = this.config.maxTurns - this.turnCount;
|
|
661
750
|
if (turnsRemaining === 2) {
|
|
662
|
-
|
|
751
|
+
this.appendContextReminder(messages, {
|
|
663
752
|
role: "user",
|
|
664
753
|
content: "<system-reminder>Warning: you have only 2 turns remaining before the turn limit is reached. " +
|
|
665
754
|
"Start wrapping up your work and prepare a summary of what you've accomplished and what remains to be done.</system-reminder>",
|
|
666
755
|
});
|
|
667
756
|
}
|
|
668
757
|
else if (turnsRemaining === 1) {
|
|
669
|
-
|
|
758
|
+
this.appendContextReminder(messages, {
|
|
670
759
|
role: "user",
|
|
671
760
|
content: "<system-reminder>Warning: you have only 1 turn remaining before the turn limit is reached. " +
|
|
672
761
|
"Wrap up your work now — your next turn will be your last.</system-reminder>",
|
|
673
762
|
});
|
|
674
763
|
}
|
|
675
764
|
else if (turnsRemaining === 0) {
|
|
676
|
-
|
|
765
|
+
this.appendContextReminder(messages, {
|
|
677
766
|
role: "user",
|
|
678
767
|
content: "<system-reminder>This is your LAST turn. You MUST respond with a final text summary now. " +
|
|
679
768
|
"Do NOT call any tools. Summarize what you have accomplished and list any remaining work.</system-reminder>",
|
|
@@ -732,7 +821,7 @@ export class TurnLoop {
|
|
|
732
821
|
});
|
|
733
822
|
const compactInjection = wrapHookMessages(compactHook.messages);
|
|
734
823
|
if (compactInjection) {
|
|
735
|
-
|
|
824
|
+
this.appendContextReminder(messages, compactInjection, true);
|
|
736
825
|
}
|
|
737
826
|
}
|
|
738
827
|
if (this.goalControlStopRequested) {
|
|
@@ -760,6 +849,7 @@ export class TurnLoop {
|
|
|
760
849
|
});
|
|
761
850
|
let response;
|
|
762
851
|
try {
|
|
852
|
+
this.deps.contextNotes?.markModelBoundary();
|
|
763
853
|
response = await this.callModelWithFallback(messages, assistantMessageId);
|
|
764
854
|
}
|
|
765
855
|
catch (err) {
|
|
@@ -846,6 +936,7 @@ export class TurnLoop {
|
|
|
846
936
|
if (anchor)
|
|
847
937
|
this.deps.recordContextUsageAnchor?.(anchor);
|
|
848
938
|
}
|
|
939
|
+
this.pendingContextReminders.clear();
|
|
849
940
|
messages = this.markPendingImagesConsumed(messages);
|
|
850
941
|
// stopBlockCount guards only the no-tool final-answer loop evaluated by
|
|
851
942
|
// on_stop (text -> block -> text -> block). A response that structurally
|
|
@@ -889,7 +980,7 @@ export class TurnLoop {
|
|
|
889
980
|
},
|
|
890
981
|
});
|
|
891
982
|
}
|
|
892
|
-
|
|
983
|
+
this.appendContextReminder(messages, {
|
|
893
984
|
role: "user",
|
|
894
985
|
content: "<system-reminder>Your previous response was truncated by the max output token limit before the tool call finished, so its arguments are incomplete. Do not assume it ran. Either retry with a smaller/more focused tool call (e.g. write the file in sections via Edit), or raise this model's maxOutputTokens.</system-reminder>",
|
|
895
986
|
});
|
|
@@ -1133,13 +1224,13 @@ export class TurnLoop {
|
|
|
1133
1224
|
this.maybeAnnounceApproachingLimit();
|
|
1134
1225
|
const injection = wrapHookMessages(stopHook.messages);
|
|
1135
1226
|
if (injection) {
|
|
1136
|
-
|
|
1227
|
+
this.appendContextReminder(messages, injection, true);
|
|
1137
1228
|
}
|
|
1138
1229
|
else {
|
|
1139
1230
|
// No guidance from the handler — inject a generic nudge so the
|
|
1140
1231
|
// model knows it must keep going rather than re-emitting the
|
|
1141
1232
|
// same final answer.
|
|
1142
|
-
|
|
1233
|
+
this.appendContextReminder(messages, {
|
|
1143
1234
|
role: "user",
|
|
1144
1235
|
content: "<system-reminder>The goal is not yet complete. Continue working toward it.</system-reminder>",
|
|
1145
1236
|
});
|
|
@@ -1284,6 +1375,7 @@ export class TurnLoop {
|
|
|
1284
1375
|
const toolResultMessage = { role: "user", content: resultBlocks };
|
|
1285
1376
|
messages.push(toolResultMessage);
|
|
1286
1377
|
this.trackFreshImageMessage(toolResultMessage);
|
|
1378
|
+
messages = this.applyNotesRollover(messages);
|
|
1287
1379
|
// B-3: tell the model which of its requested tool calls were dropped by
|
|
1288
1380
|
// the per-turn cap so it can re-issue them, instead of silently assuming
|
|
1289
1381
|
// they ran. Appended to the same user message that carries the results.
|
|
@@ -1298,7 +1390,7 @@ export class TurnLoop {
|
|
|
1298
1390
|
// Separate user message (not folded into the tool_result blocks) so the
|
|
1299
1391
|
// OpenAI converter — which lifts tool_results into standalone role:tool
|
|
1300
1392
|
// messages — keeps the reminder as a plain user turn after them.
|
|
1301
|
-
|
|
1393
|
+
this.appendContextReminder(messages, {
|
|
1302
1394
|
role: "user",
|
|
1303
1395
|
content: `<system-reminder>Only the first ${toolCalls.length} of your ${response.toolCalls.length} ` +
|
|
1304
1396
|
`tool calls ran this turn (per-turn limit is ${this.config.maxToolCallsPerTurn}). ` +
|
|
@@ -1383,7 +1475,7 @@ export class TurnLoop {
|
|
|
1383
1475
|
return { text: finalText, reason: "completed", messages };
|
|
1384
1476
|
}
|
|
1385
1477
|
tlog.warn("turn.goal_self_reported_complete_persist_failed", { cat: "goal" });
|
|
1386
|
-
|
|
1478
|
+
this.appendContextReminder(messages, {
|
|
1387
1479
|
role: "user",
|
|
1388
1480
|
content: "<system-reminder>complete_goal 未能持久化完成状态;目标仍处于活动状态。请不要宣称已经结束,并在会话状态恢复可写后重试。</system-reminder>",
|
|
1389
1481
|
});
|
|
@@ -1419,7 +1511,7 @@ export class TurnLoop {
|
|
|
1419
1511
|
};
|
|
1420
1512
|
}
|
|
1421
1513
|
tlog.warn("turn.goal_user_cancelled_persist_failed", { cat: "goal" });
|
|
1422
|
-
|
|
1514
|
+
this.appendContextReminder(messages, {
|
|
1423
1515
|
role: "user",
|
|
1424
1516
|
content: "<system-reminder>cancel_goal 未能持久化删除状态;目标仍处于活动状态。请在会话状态恢复可写后重试。</system-reminder>",
|
|
1425
1517
|
});
|
|
@@ -1452,7 +1544,7 @@ export class TurnLoop {
|
|
|
1452
1544
|
};
|
|
1453
1545
|
}
|
|
1454
1546
|
if (budgetDecision === "nudge") {
|
|
1455
|
-
|
|
1547
|
+
this.appendContextReminder(messages, {
|
|
1456
1548
|
role: "user",
|
|
1457
1549
|
content: "<system-reminder>You are approaching the token budget limit. Please start wrapping up your work and provide a summary.</system-reminder>",
|
|
1458
1550
|
});
|
|
@@ -1465,7 +1557,7 @@ export class TurnLoop {
|
|
|
1465
1557
|
guard.noteText(response.text);
|
|
1466
1558
|
const turnReminder = guard.turnEnded(this.turnCount);
|
|
1467
1559
|
if (turnReminder) {
|
|
1468
|
-
|
|
1560
|
+
this.appendContextReminder(messages, { role: "user", content: turnReminder });
|
|
1469
1561
|
tlog.info("guard.silent_turn", { cat: "guard", turn: this.turnCount });
|
|
1470
1562
|
}
|
|
1471
1563
|
}
|
|
@@ -1477,7 +1569,7 @@ export class TurnLoop {
|
|
|
1477
1569
|
if (taskGuard) {
|
|
1478
1570
|
const taskReminder = taskGuard.turnEnded(this.turnCount);
|
|
1479
1571
|
if (taskReminder) {
|
|
1480
|
-
|
|
1572
|
+
this.appendContextReminder(messages, { role: "user", content: taskReminder });
|
|
1481
1573
|
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
1482
1574
|
}
|
|
1483
1575
|
}
|
|
@@ -1556,7 +1648,7 @@ export class TurnLoop {
|
|
|
1556
1648
|
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1557
1649
|
return { text: finalText, reason: "completed", messages };
|
|
1558
1650
|
}
|
|
1559
|
-
|
|
1651
|
+
this.appendContextReminder(messages, {
|
|
1560
1652
|
role: "user",
|
|
1561
1653
|
content: "<system-reminder>Turn limit reached. Provide a final summary of what you accomplished and what remains to be done. Do NOT call any tools.</system-reminder>",
|
|
1562
1654
|
});
|
|
@@ -1754,11 +1846,11 @@ export class TurnLoop {
|
|
|
1754
1846
|
break;
|
|
1755
1847
|
}
|
|
1756
1848
|
consumed = true;
|
|
1757
|
-
this.deps.setOriginClientMessageId?.(clientMessageId);
|
|
1758
1849
|
const message = { role: "user", content };
|
|
1759
1850
|
messages.push(message);
|
|
1760
1851
|
this.trackFreshImageMessage(message);
|
|
1761
1852
|
this.deps.transcript.appendMessage("user", content, { steerId: id, clientMessageId });
|
|
1853
|
+
this.deps.setOriginClientMessageId?.(clientMessageId);
|
|
1762
1854
|
this.config.onStream?.({ type: "steer_injected", text, id });
|
|
1763
1855
|
}
|
|
1764
1856
|
return consumed;
|
|
@@ -1796,7 +1888,7 @@ export class TurnLoop {
|
|
|
1796
1888
|
this.deps.onAgentDirectionsDelivered?.(envelopeIds);
|
|
1797
1889
|
const hookInjection = wrapHookMessages(hook.messages);
|
|
1798
1890
|
if (hookInjection)
|
|
1799
|
-
|
|
1891
|
+
this.appendContextReminder(messages, hookInjection, true);
|
|
1800
1892
|
return true;
|
|
1801
1893
|
}
|
|
1802
1894
|
/**
|