@cjhyy/code-shell-core 0.9.2 → 0.9.4
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 +26 -1
- package/dist/cli/agent-server-tcp.js +58 -7
- package/dist/engine/engine.d.ts +8 -0
- package/dist/engine/engine.js +16 -12
- package/dist/engine/model-facade.d.ts +3 -0
- package/dist/engine/model-facade.js +2 -0
- package/dist/engine/run-tooling.js +8 -8
- package/dist/engine/streaming-tool-queue.d.ts +4 -1
- package/dist/engine/streaming-tool-queue.js +17 -2
- package/dist/engine/turn-loop.d.ts +14 -5
- package/dist/engine/turn-loop.js +92 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/llm/prompt-cache.d.ts +48 -0
- package/dist/llm/prompt-cache.js +100 -0
- package/dist/llm/providers/anthropic.d.ts +3 -0
- package/dist/llm/providers/anthropic.js +77 -53
- package/dist/llm/providers/openai.d.ts +7 -27
- package/dist/llm/providers/openai.js +120 -68
- package/dist/llm/types.d.ts +3 -0
- package/dist/onboarding.js +72 -49
- package/dist/panel-apps/manifest.d.ts +12 -12
- package/dist/profile/types.d.ts +26 -26
- package/dist/protocol/background-result-wakeup.d.ts +3 -1
- package/dist/protocol/background-result-wakeup.js +5 -5
- package/dist/protocol/server.d.ts +13 -0
- package/dist/protocol/server.js +22 -3
- package/dist/services/index.d.ts +0 -1
- package/dist/services/index.js +0 -1
- package/dist/session/memory.js +2 -2
- package/dist/session/session-manager.js +30 -1
- package/dist/tool-system/builtin/agent-notifications.d.ts +25 -1
- package/dist/tool-system/builtin/agent-notifications.js +334 -2
- package/dist/tool-system/context.d.ts +9 -4
- package/dist/tool-system/external-tool-exposure.js +11 -10
- package/package.json +2 -1
- package/dist/services/notifier.d.ts +0 -33
- package/dist/services/notifier.js +0 -83
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
* bootstrap is invisible to them.
|
|
31
31
|
*/
|
|
32
32
|
import { join } from "node:path";
|
|
33
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
33
34
|
import { Engine } from "../engine/engine.js";
|
|
34
35
|
import { EngineRuntime } from "../engine/runtime.js";
|
|
35
36
|
import { ChatSessionManager } from "../protocol/chat-session-manager.js";
|
|
36
|
-
import { SessionManager } from "../session/session-manager.js";
|
|
37
|
+
import { assertSafeSessionId, SessionManager, sessionsRoot } from "../session/session-manager.js";
|
|
37
38
|
import { validateSettings } from "../settings/schema.js";
|
|
38
39
|
import { AgentServer } from "../protocol/server.js";
|
|
39
40
|
import { StdioTransport } from "../protocol/transport.js";
|
|
@@ -122,6 +123,29 @@ const composition = compileComposition({ modules: await loadConfiguredAgentModul
|
|
|
122
123
|
// byte-for-byte.
|
|
123
124
|
const dataRoot = process.env.CODE_SHELL_DATA_ROOT?.trim() || undefined;
|
|
124
125
|
const dataSessionsDir = dataRoot ? join(dataRoot, "sessions") : undefined;
|
|
126
|
+
const notificationSessionsDir = dataSessionsDir ?? sessionsRoot();
|
|
127
|
+
const notificationPersistence = {
|
|
128
|
+
fileForSession(sessionId) {
|
|
129
|
+
try {
|
|
130
|
+
assertSafeSessionId(sessionId);
|
|
131
|
+
return join(notificationSessionsDir, sessionId, "pending-notifications.json");
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
listSessionIds() {
|
|
138
|
+
try {
|
|
139
|
+
return readdirSync(notificationSessionsDir, { withFileTypes: true })
|
|
140
|
+
.filter((entry) => entry.isDirectory() &&
|
|
141
|
+
existsSync(join(notificationSessionsDir, entry.name, "pending-notifications.json")))
|
|
142
|
+
.map((entry) => entry.name);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
125
149
|
// Load settings once to derive llm config for the seed engine.
|
|
126
150
|
// Desktop is a host application: read the full disk hierarchy (incl. the
|
|
127
151
|
// user's ~/.code-shell). The SDK default 'project' would skip user config.
|
|
@@ -364,6 +388,7 @@ const agentServer = new AgentServer({
|
|
|
364
388
|
// Cold background-wakeup rehydrate must read the same sessions store the
|
|
365
389
|
// engines write when the data root is relocated (undefined → default root).
|
|
366
390
|
sessionDiskRoot: dataSessionsDir,
|
|
391
|
+
notificationPersistence,
|
|
367
392
|
// Config hot-reload (layer 2) reads disk through the SAME closure the
|
|
368
393
|
// engineFactory uses for new sessions, so a reloaded running session and a
|
|
369
394
|
// newly-created session converge on identical disk config (no divergence).
|
|
@@ -34,11 +34,39 @@ import { startAutomation } from "../automation/index.js";
|
|
|
34
34
|
import { CronStore, defaultCronStorePath } from "../automation/store.js";
|
|
35
35
|
import { resolveLLMConfigForTag } from "../engine/resolve-llm-config.js";
|
|
36
36
|
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
38
|
+
import { join } from "node:path";
|
|
37
39
|
import { getApprovalRouter } from "../tool-system/permission.js";
|
|
38
|
-
import { SessionManager } from "../session/session-manager.js";
|
|
40
|
+
import { assertSafeSessionId, SessionManager, sessionsRoot } from "../session/session-manager.js";
|
|
41
|
+
import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
|
|
39
42
|
const cwd = process.env.AGENT_CWD ?? process.cwd();
|
|
40
43
|
const port = Number(process.env.AGENT_TCP_PORT ?? "4321");
|
|
41
44
|
const host = process.env.AGENT_TCP_HOST ?? "127.0.0.1";
|
|
45
|
+
const dataRoot = process.env.CODE_SHELL_DATA_ROOT?.trim() || undefined;
|
|
46
|
+
const dataSessionsDir = dataRoot ? join(dataRoot, "sessions") : undefined;
|
|
47
|
+
const notificationSessionsDir = dataSessionsDir ?? sessionsRoot();
|
|
48
|
+
notificationQueue.attachPersistence({
|
|
49
|
+
fileForSession(sessionId) {
|
|
50
|
+
try {
|
|
51
|
+
assertSafeSessionId(sessionId);
|
|
52
|
+
return join(notificationSessionsDir, sessionId, "pending-notifications.json");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
listSessionIds() {
|
|
59
|
+
try {
|
|
60
|
+
return readdirSync(notificationSessionsDir, { withFileTypes: true })
|
|
61
|
+
.filter((entry) => entry.isDirectory() &&
|
|
62
|
+
existsSync(join(notificationSessionsDir, entry.name, "pending-notifications.json")))
|
|
63
|
+
.map((entry) => entry.name);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
42
70
|
const settingsManager = new SettingsManager(cwd, "full");
|
|
43
71
|
// Read once at startup and reuse for every session (see engineFactory below).
|
|
44
72
|
// This is intentional for the TCP host: it's a headless long-running server,
|
|
@@ -53,7 +81,12 @@ if (!seedLlm) {
|
|
|
53
81
|
}
|
|
54
82
|
const llmConfig = seedLlm;
|
|
55
83
|
// ── Shared runtime (same bootstrap as stdio) ─────────────────────
|
|
56
|
-
const seedEngine = new Engine({
|
|
84
|
+
const seedEngine = new Engine({
|
|
85
|
+
llm: llmConfig,
|
|
86
|
+
cwd,
|
|
87
|
+
settingsScope: "full",
|
|
88
|
+
sessionStorageDir: dataSessionsDir,
|
|
89
|
+
});
|
|
57
90
|
const modelPool = seedEngine.getModelPool();
|
|
58
91
|
const toolRegistry = seedEngine.getRuntimeToolRegistry();
|
|
59
92
|
const resolvedLlmConfig = seedEngine.getConfig().llm;
|
|
@@ -92,11 +125,13 @@ const chatManager = new ChatSessionManager({
|
|
|
92
125
|
...personalizationFrom(settings.agent),
|
|
93
126
|
maxTurns: slice.maxTurns,
|
|
94
127
|
maxContextTokens: slice.maxContextTokens,
|
|
128
|
+
sessionStorageDir: slice.sessionStorageDir ?? dataSessionsDir,
|
|
95
129
|
...(slice.cwd ? { cwd: slice.cwd } : {}),
|
|
96
130
|
});
|
|
97
131
|
},
|
|
98
132
|
maxSessions: 16,
|
|
99
133
|
idleTtlMs: 30 * 60 * 1000,
|
|
134
|
+
...(dataRoot ? { dataRoot } : {}),
|
|
100
135
|
});
|
|
101
136
|
chatManager.startIdleSweeper();
|
|
102
137
|
// ── Automation (same module the desktop loads) ──────────────────
|
|
@@ -109,23 +144,38 @@ const automationRunManager = createRunManager({
|
|
|
109
144
|
approvalBackend: new HeadlessApprovalBackend("approve-read-only"),
|
|
110
145
|
});
|
|
111
146
|
const automation = startAutomation({
|
|
112
|
-
store: new CronStore(defaultCronStorePath()),
|
|
147
|
+
store: new CronStore(defaultCronStorePath(dataRoot)),
|
|
113
148
|
runManager: automationRunManager,
|
|
114
149
|
});
|
|
115
150
|
// ── Serve over TCP ──────────────────────────────────────────────
|
|
116
151
|
// One AgentServer per accepted connection, all sharing the same chatManager.
|
|
117
152
|
const servers = new Set();
|
|
118
|
-
const goalDiskManager = new SessionManager();
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
const goalDiskManager = new SessionManager(dataSessionsDir);
|
|
154
|
+
function createTcpAgentServer(transport, connectionId, ownsBackgroundWakeups) {
|
|
155
|
+
return new AgentServer({
|
|
121
156
|
chatManager,
|
|
122
157
|
transport,
|
|
123
|
-
connectionId
|
|
158
|
+
connectionId,
|
|
124
159
|
approvalRouter: getApprovalRouter(),
|
|
160
|
+
sessionDiskRoot: dataSessionsDir,
|
|
161
|
+
ownsBackgroundWakeups,
|
|
125
162
|
readActiveGoalFromDisk: (sessionId) => goalDiskManager.readActiveGoal(sessionId),
|
|
126
163
|
updateActiveGoalOnDisk: (sessionId, patch) => goalDiskManager.updateActiveGoal(sessionId, patch)?.goal,
|
|
127
164
|
clearActiveGoalOnDisk: (sessionId, expected) => goalDiskManager.clearActiveGoal(sessionId, expected),
|
|
128
165
|
});
|
|
166
|
+
}
|
|
167
|
+
// Own restoration for the whole TCP process, not for the first client. This
|
|
168
|
+
// wakes pending chats immediately after a headless restart even if nobody has
|
|
169
|
+
// connected a UI yet. Per-connection servers still forward live observations
|
|
170
|
+
// but do not race this owner for mailbox consumption.
|
|
171
|
+
const backgroundWakeTransport = {
|
|
172
|
+
send() { },
|
|
173
|
+
onMessage() { },
|
|
174
|
+
close() { },
|
|
175
|
+
};
|
|
176
|
+
const backgroundWakeServer = createTcpAgentServer(backgroundWakeTransport, "tcp-background-wakeup", true);
|
|
177
|
+
listenTcp({ port, host }, (transport, socket) => {
|
|
178
|
+
const server = createTcpAgentServer(transport, randomUUID(), false);
|
|
129
179
|
servers.add(server);
|
|
130
180
|
socket.once("close", () => {
|
|
131
181
|
server.disconnect();
|
|
@@ -138,6 +188,7 @@ listenTcp({ port, host }, (transport, socket) => {
|
|
|
138
188
|
automation.stop();
|
|
139
189
|
for (const s of servers)
|
|
140
190
|
s.close();
|
|
191
|
+
backgroundWakeServer.close();
|
|
141
192
|
void listener.close().then(() => process.exit(0));
|
|
142
193
|
};
|
|
143
194
|
process.on("SIGTERM", shutdown);
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -317,6 +317,14 @@ export declare class Engine {
|
|
|
317
317
|
* in-process AgentServer to decide whether to wire an interactive askUser.
|
|
318
318
|
*/
|
|
319
319
|
isHeadless(): boolean;
|
|
320
|
+
/**
|
|
321
|
+
* A background_notification park only makes sense where the Session can be
|
|
322
|
+
* woken by the completion later (server refuses headless; sub-agent sessions
|
|
323
|
+
* are not in chatManager) — everywhere else honouring it would end the run
|
|
324
|
+
* early and orphan the background result. A reply_committed boundary is
|
|
325
|
+
* synchronous and never suppressed.
|
|
326
|
+
*/
|
|
327
|
+
private suppressesRunYield;
|
|
320
328
|
get permissionMode(): NonNullable<EngineConfig["permissionMode"]>;
|
|
321
329
|
get planMode(): boolean;
|
|
322
330
|
/**
|
package/dist/engine/engine.js
CHANGED
|
@@ -804,6 +804,16 @@ export class Engine {
|
|
|
804
804
|
isHeadless() {
|
|
805
805
|
return this.config.headless === true;
|
|
806
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* A background_notification park only makes sense where the Session can be
|
|
809
|
+
* woken by the completion later (server refuses headless; sub-agent sessions
|
|
810
|
+
* are not in chatManager) — everywhere else honouring it would end the run
|
|
811
|
+
* early and orphan the background result. A reply_committed boundary is
|
|
812
|
+
* synchronous and never suppressed.
|
|
813
|
+
*/
|
|
814
|
+
suppressesRunYield(reason) {
|
|
815
|
+
return (reason === "background_notification" && (this.isHeadless() || this.config.isSubAgent === true));
|
|
816
|
+
}
|
|
807
817
|
get permissionMode() {
|
|
808
818
|
return this.permissionController.permissionMode;
|
|
809
819
|
}
|
|
@@ -2172,18 +2182,12 @@ export class Engine {
|
|
|
2172
2182
|
publishGoalJudgeContext: (context) => {
|
|
2173
2183
|
publishGoalJudgeContext(context);
|
|
2174
2184
|
},
|
|
2175
|
-
// A background_notification yield
|
|
2176
|
-
// woken
|
|
2177
|
-
//
|
|
2178
|
-
//
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
...(this.isHeadless() || this.config.isSubAgent === true
|
|
2182
|
-
? {}
|
|
2183
|
-
: {
|
|
2184
|
-
peekToolRunYield: () => toolCtx.runYield?.peek?.(),
|
|
2185
|
-
consumeToolRunYield: () => toolCtx.runYield?.consume(),
|
|
2186
|
-
}),
|
|
2185
|
+
// A background_notification yield is visible only where the Session
|
|
2186
|
+
// can be woken later. A committed host reply is a synchronous terminal
|
|
2187
|
+
// boundary, so it must remain visible in headless and sub-agent runs.
|
|
2188
|
+
// One predicate serves peek and consume so they can never disagree.
|
|
2189
|
+
peekToolRunYield: (reason) => !this.suppressesRunYield(reason) && toolCtx.runYield?.peek(reason) === true,
|
|
2190
|
+
consumeToolRunYield: (reason) => !this.suppressesRunYield(reason) && toolCtx.runYield?.consume(reason) === true,
|
|
2187
2191
|
ctxOverheadStore: {
|
|
2188
2192
|
get: (s) => this.ctxOverheadBySid.get(s) ?? 0,
|
|
2189
2193
|
set: (s, n) => {
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* Model call facade — wraps LLM client with transcript integration.
|
|
3
3
|
*/
|
|
4
4
|
import type { LLMClientBase } from "../llm/client-base.js";
|
|
5
|
+
import type { PromptCacheRequestContext } from "../llm/prompt-cache.js";
|
|
5
6
|
import type { Message, ToolDefinition, LLMResponse, StreamCallback } from "../types.js";
|
|
6
7
|
import { Transcript } from "../session/transcript.js";
|
|
7
8
|
import { type PromptPrefixFingerprint } from "./prompt-cache-diagnostics.js";
|
|
8
9
|
export interface ModelCallRecordingOptions {
|
|
9
10
|
sensitiveToolResultRedactions?: ReadonlyMap<string, string>;
|
|
11
|
+
/** Cache boundary metadata; scopeId is filled from the active session. */
|
|
12
|
+
promptCache?: Omit<PromptCacheRequestContext, "scopeId">;
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
15
|
* Prompt-cache hit rate for one request, as CC computes it:
|
|
@@ -89,6 +89,7 @@ export class ModelFacade {
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
signal,
|
|
92
|
+
promptCache: { scopeId: sid, ...recordingOptions?.promptCache },
|
|
92
93
|
});
|
|
93
94
|
}
|
|
94
95
|
catch (err) {
|
|
@@ -144,6 +145,7 @@ export class ModelFacade {
|
|
|
144
145
|
tools,
|
|
145
146
|
stream: false,
|
|
146
147
|
signal,
|
|
148
|
+
promptCache: { scopeId: sid, ...recordingOptions?.promptCache },
|
|
147
149
|
});
|
|
148
150
|
}
|
|
149
151
|
catch (err) {
|
|
@@ -10,7 +10,7 @@ import { applyDynamicToolDef } from "./dynamic-tool-defs.js";
|
|
|
10
10
|
/** engine.ts L1485-1522 —— ToolContext 组装(spawner、agentDefinitions、base 由调用方传入)。 */
|
|
11
11
|
export function buildRunToolContext(args) {
|
|
12
12
|
const { options, profile, profileParams } = args;
|
|
13
|
-
|
|
13
|
+
const pendingRunYield = new Set();
|
|
14
14
|
// sessionId is filled in after the session bundle is resolved below
|
|
15
15
|
// (the session may be cold-started or resumed). Until then this is
|
|
16
16
|
// intentionally shaped as a mutable local; we treat it as immutable
|
|
@@ -40,16 +40,16 @@ export function buildRunToolContext(args) {
|
|
|
40
40
|
toolCtx.cwd = nextCwd;
|
|
41
41
|
},
|
|
42
42
|
runYield: {
|
|
43
|
+
// Reasons accumulate independently — a batch may both commit a host
|
|
44
|
+
// reply and launch background work; the turn loop decides precedence.
|
|
43
45
|
request(reason) {
|
|
44
|
-
pendingRunYield
|
|
46
|
+
pendingRunYield.add(reason);
|
|
45
47
|
},
|
|
46
|
-
peek() {
|
|
47
|
-
return pendingRunYield;
|
|
48
|
+
peek(reason) {
|
|
49
|
+
return pendingRunYield.has(reason);
|
|
48
50
|
},
|
|
49
|
-
consume() {
|
|
50
|
-
|
|
51
|
-
pendingRunYield = undefined;
|
|
52
|
-
return reason;
|
|
51
|
+
consume(reason) {
|
|
52
|
+
return pendingRunYield.delete(reason);
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
55
|
skillAllowlist: options?.skillAllowlist !== undefined
|
|
@@ -17,12 +17,15 @@ import type { ToolCall, ToolResult } from "../types.js";
|
|
|
17
17
|
import type { ToolExecutor } from "../tool-system/executor.js";
|
|
18
18
|
export declare class StreamingToolQueue {
|
|
19
19
|
private readonly executor;
|
|
20
|
+
private readonly pendingUnsafeSkipReason?;
|
|
20
21
|
private readonly pending;
|
|
21
22
|
private readonly unsafeQueue;
|
|
22
23
|
private readonly callOrder;
|
|
23
24
|
private readonly toolNameById;
|
|
24
25
|
private draining;
|
|
25
|
-
constructor(executor: ToolExecutor
|
|
26
|
+
constructor(executor: ToolExecutor, options?: {
|
|
27
|
+
pendingUnsafeSkipReason?: () => string | undefined;
|
|
28
|
+
});
|
|
26
29
|
/**
|
|
27
30
|
* Enqueue a tool from the completed response. Concurrency-safe tools start
|
|
28
31
|
* immediately within this post-response phase; unsafe tools are queued for
|
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export class StreamingToolQueue {
|
|
17
17
|
executor;
|
|
18
|
+
pendingUnsafeSkipReason;
|
|
18
19
|
pending = new Map();
|
|
19
20
|
unsafeQueue = [];
|
|
20
21
|
callOrder = [];
|
|
21
22
|
toolNameById = new Map();
|
|
22
23
|
draining = false;
|
|
23
|
-
constructor(executor) {
|
|
24
|
+
constructor(executor, options) {
|
|
24
25
|
this.executor = executor;
|
|
26
|
+
this.pendingUnsafeSkipReason = options?.pendingUnsafeSkipReason;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* Enqueue a tool from the completed response. Concurrency-safe tools start
|
|
@@ -62,8 +64,21 @@ export class StreamingToolQueue {
|
|
|
62
64
|
this.draining = true;
|
|
63
65
|
const resultMap = new Map();
|
|
64
66
|
// Execute unsafe tools sequentially. A rejection here must not stop the
|
|
65
|
-
// remaining unsafe tools from running.
|
|
67
|
+
// remaining unsafe tools from running. A trusted terminal boundary may,
|
|
68
|
+
// however, skip calls that have not started yet. We still synthesize one
|
|
69
|
+
// result per call so tool_use/tool_result history remains structurally
|
|
70
|
+
// complete even though the run will stop after this batch.
|
|
66
71
|
for (const call of this.unsafeQueue) {
|
|
72
|
+
const skipReason = this.pendingUnsafeSkipReason?.();
|
|
73
|
+
if (skipReason) {
|
|
74
|
+
resultMap.set(call.id, {
|
|
75
|
+
id: call.id,
|
|
76
|
+
toolName: call.toolName,
|
|
77
|
+
error: `Tool execution skipped: ${skipReason}`,
|
|
78
|
+
isError: true,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
67
82
|
const p = this.executor.executeSingle(call);
|
|
68
83
|
this.pending.set(call.id, p);
|
|
69
84
|
resultMap.set(call.id, await this.toResult(call.id, call.toolName, p));
|
|
@@ -150,10 +150,10 @@ export interface TurnLoopDeps {
|
|
|
150
150
|
* built-in judge closure; it is never added to the public on_stop context.
|
|
151
151
|
*/
|
|
152
152
|
publishGoalJudgeContext?: (context: GoalJudgeRuntimeContext) => void;
|
|
153
|
-
/**
|
|
154
|
-
peekToolRunYield?: (
|
|
155
|
-
/** Consume a
|
|
156
|
-
consumeToolRunYield?: (
|
|
153
|
+
/** Whether a trusted tool has a specific run boundary pending (not cleared). */
|
|
154
|
+
peekToolRunYield?: (reason: import("../tool-system/context.js").ToolRunYieldReason) => boolean;
|
|
155
|
+
/** Consume a specific pending run boundary; true if it was pending. */
|
|
156
|
+
consumeToolRunYield?: (reason: import("../tool-system/context.js").ToolRunYieldReason) => boolean;
|
|
157
157
|
}
|
|
158
158
|
export interface TurnLoopResult {
|
|
159
159
|
text: string;
|
|
@@ -287,7 +287,16 @@ export declare class TurnLoop {
|
|
|
287
287
|
private finalizeModelTurn;
|
|
288
288
|
private prepareMessagesForModel;
|
|
289
289
|
private stripVolatileContextMessages;
|
|
290
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Keep volatile context out of compaction/summarization without moving it on
|
|
292
|
+
* every model round. If context management is a no-op, return the original
|
|
293
|
+
* array so the provider sees a strictly append-only prompt. A real rewrite
|
|
294
|
+
* (dedupe/compaction/truncation) already invalidates the old prefix, so start
|
|
295
|
+
* a fresh append-only segment with the volatile snapshot at the new tail.
|
|
296
|
+
*/
|
|
297
|
+
private restoreVolatileAfterContextManagement;
|
|
298
|
+
private manageContextMessages;
|
|
299
|
+
private manageContextMessagesSync;
|
|
291
300
|
private markPendingImagesConsumed;
|
|
292
301
|
private redactConsumedSensitiveToolResults;
|
|
293
302
|
private modelCallRecordingOptions;
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -340,10 +340,36 @@ export class TurnLoop {
|
|
|
340
340
|
});
|
|
341
341
|
return changed ? stripped : messages;
|
|
342
342
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Keep volatile context out of compaction/summarization without moving it on
|
|
345
|
+
* every model round. If context management is a no-op, return the original
|
|
346
|
+
* array so the provider sees a strictly append-only prompt. A real rewrite
|
|
347
|
+
* (dedupe/compaction/truncation) already invalidates the old prefix, so start
|
|
348
|
+
* a fresh append-only segment with the volatile snapshot at the new tail.
|
|
349
|
+
*/
|
|
350
|
+
restoreVolatileAfterContextManagement(original, stableInput, managedStable) {
|
|
351
|
+
const unchanged = stableInput.length === managedStable.length &&
|
|
352
|
+
stableInput.every((message, index) => managedStable[index] === message);
|
|
353
|
+
if (unchanged)
|
|
354
|
+
return original;
|
|
355
|
+
const volatile = original.filter((message) => this.volatileContextMessages.has(message));
|
|
356
|
+
return [...managedStable, ...volatile];
|
|
357
|
+
}
|
|
358
|
+
async manageContextMessages(messages) {
|
|
359
|
+
if (this.volatileContextMessages.size === 0) {
|
|
360
|
+
return this.deps.contextManager.manageAsync(messages, this.config.signal);
|
|
361
|
+
}
|
|
362
|
+
const stable = this.stripVolatileContextMessages(messages);
|
|
363
|
+
const managed = await this.deps.contextManager.manageAsync(stable, this.config.signal);
|
|
364
|
+
return this.restoreVolatileAfterContextManagement(messages, stable, managed);
|
|
365
|
+
}
|
|
366
|
+
manageContextMessagesSync(messages) {
|
|
367
|
+
if (this.volatileContextMessages.size === 0) {
|
|
368
|
+
return this.deps.contextManager.manage(messages);
|
|
369
|
+
}
|
|
370
|
+
const stable = this.stripVolatileContextMessages(messages);
|
|
371
|
+
const managed = this.deps.contextManager.manage(stable);
|
|
372
|
+
return this.restoreVolatileAfterContextManagement(messages, stable, managed);
|
|
347
373
|
}
|
|
348
374
|
markPendingImagesConsumed(messages) {
|
|
349
375
|
if (this.pendingImageMessages.size === 0)
|
|
@@ -367,11 +393,15 @@ export class TurnLoop {
|
|
|
367
393
|
this.sensitiveToolResultRedactions.clear();
|
|
368
394
|
return redacted;
|
|
369
395
|
}
|
|
370
|
-
modelCallRecordingOptions() {
|
|
371
|
-
|
|
372
|
-
return undefined;
|
|
396
|
+
modelCallRecordingOptions(messages) {
|
|
397
|
+
const volatileIndex = messages.findIndex((message) => this.volatileContextMessages.has(message));
|
|
373
398
|
return {
|
|
374
|
-
|
|
399
|
+
...(this.sensitiveToolResultRedactions.size > 0
|
|
400
|
+
? { sensitiveToolResultRedactions: new Map(this.sensitiveToolResultRedactions) }
|
|
401
|
+
: {}),
|
|
402
|
+
promptCache: {
|
|
403
|
+
stablePrefixMessageCount: volatileIndex >= 0 ? volatileIndex : messages.length,
|
|
404
|
+
},
|
|
375
405
|
};
|
|
376
406
|
}
|
|
377
407
|
trackFreshImageMessage(message) {
|
|
@@ -657,7 +687,6 @@ export class TurnLoop {
|
|
|
657
687
|
// pendingImageMessages are preserved through this next model request.
|
|
658
688
|
const hasPendingSensitiveToolResults = this.sensitiveToolResultRedactions.size > 0;
|
|
659
689
|
messages = this.prepareMessagesForModel(messages);
|
|
660
|
-
messages = this.stripVolatileContextMessages(messages);
|
|
661
690
|
if (hasPendingSensitiveToolResults) {
|
|
662
691
|
tlog.info("turn.sensitive_tool_result_context_management_skipped", {
|
|
663
692
|
cat: "turn",
|
|
@@ -666,7 +695,7 @@ export class TurnLoop {
|
|
|
666
695
|
}
|
|
667
696
|
else {
|
|
668
697
|
// Context management (async — may trigger LLM summarization)
|
|
669
|
-
messages = await this.
|
|
698
|
+
messages = await this.manageContextMessages(messages);
|
|
670
699
|
// manageAsync can itself issue an LLM summarization call lasting several
|
|
671
700
|
// seconds; if the signal aborted during it, stop here rather than
|
|
672
701
|
// proceeding into the (expensive) main model call. Belt to the loop-top
|
|
@@ -710,7 +739,6 @@ export class TurnLoop {
|
|
|
710
739
|
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
711
740
|
return { text: finalText, reason: "completed", messages };
|
|
712
741
|
}
|
|
713
|
-
messages = this.appendVolatileContextMessages(messages);
|
|
714
742
|
// Model call (with streaming fallback and max_output_tokens continuation)
|
|
715
743
|
this.config.onStream?.({
|
|
716
744
|
type: "stream_request_start",
|
|
@@ -721,7 +749,15 @@ export class TurnLoop {
|
|
|
721
749
|
this.streamedToolIds.clear();
|
|
722
750
|
// Tool queue is created before the call, but enqueue happens only after
|
|
723
751
|
// the complete LLMResponse is available below.
|
|
724
|
-
const streamingQueue = new StreamingToolQueue(this.deps.toolExecutor
|
|
752
|
+
const streamingQueue = new StreamingToolQueue(this.deps.toolExecutor, {
|
|
753
|
+
// Once a trusted reply tool commits the authoritative host response,
|
|
754
|
+
// later sequential calls from the same model batch must not execute.
|
|
755
|
+
// Concurrency-safe calls may already be running; drain still awaits
|
|
756
|
+
// those so the transcript remains complete.
|
|
757
|
+
pendingUnsafeSkipReason: () => this.deps.peekToolRunYield?.("reply_committed")
|
|
758
|
+
? "an authoritative host reply was already committed"
|
|
759
|
+
: undefined,
|
|
760
|
+
});
|
|
725
761
|
let response;
|
|
726
762
|
try {
|
|
727
763
|
response = await this.callModelWithFallback(messages, assistantMessageId);
|
|
@@ -891,7 +927,8 @@ export class TurnLoop {
|
|
|
891
927
|
},
|
|
892
928
|
];
|
|
893
929
|
try {
|
|
894
|
-
const
|
|
930
|
+
const preparedContinuationMessages = this.prepareMessagesForModel(contMessages);
|
|
931
|
+
const contResponse = await this.deps.model.call(this.deps.systemPrompt, preparedContinuationMessages, this.deps.tools, this.config.onStream, this.config.signal, this.modelCallRecordingOptions(preparedContinuationMessages));
|
|
895
932
|
// Continuations are separate provider responses, so preserve the
|
|
896
933
|
// same structural tool_use invariant before processing this one.
|
|
897
934
|
if (contResponse.toolCalls.length > 0) {
|
|
@@ -995,7 +1032,7 @@ export class TurnLoop {
|
|
|
995
1032
|
// answered before parking the run. Keep the tool's yield request
|
|
996
1033
|
// pending across that extra model round, then park once the model
|
|
997
1034
|
// has replied and there is still no background result to consume.
|
|
998
|
-
if (this.deps.consumeToolRunYield?.(
|
|
1035
|
+
if (this.deps.consumeToolRunYield?.("background_notification")) {
|
|
999
1036
|
tlog.info("turn.background_notification_wait_after_steer", { cat: "turn" });
|
|
1000
1037
|
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1001
1038
|
return {
|
|
@@ -1272,30 +1309,47 @@ export class TurnLoop {
|
|
|
1272
1309
|
// Tool results just pushed; recompute ctx so the bar updates *before*
|
|
1273
1310
|
// the next model round-trip — large tool outputs can move it sharply.
|
|
1274
1311
|
this.emitCtxFromMessages(messages);
|
|
1275
|
-
// A trusted tool
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1279
|
-
//
|
|
1280
|
-
//
|
|
1281
|
-
|
|
1312
|
+
// A trusted tool marked a run boundary. reply_committed: the
|
|
1313
|
+
// authoritative user-facing reply is recorded — another model request
|
|
1314
|
+
// could only produce duplicate tools or stray assistant text.
|
|
1315
|
+
// background_notification: async work was launched whose completion
|
|
1316
|
+
// notification will wake this Session, so the run parks instead of
|
|
1317
|
+
// asking the model for another step with no new evidence. One batch
|
|
1318
|
+
// may pend both; the park wins so the completion finds a parked run.
|
|
1319
|
+
const replyCommitted = this.deps.peekToolRunYield?.("reply_committed") === true;
|
|
1320
|
+
const backgroundWait = this.deps.peekToolRunYield?.("background_notification") === true;
|
|
1321
|
+
if (replyCommitted || backgroundWait) {
|
|
1282
1322
|
// Close the current model turn before a queued steer re-drives it;
|
|
1283
1323
|
// interrupt-and-redrive swaps the turn signal at this boundary.
|
|
1284
1324
|
this.finalizeModelTurn();
|
|
1285
1325
|
if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
1326
|
+
// A user message arrived while the batch was executing. The
|
|
1327
|
+
// committed reply answered the previous content only — lift the
|
|
1328
|
+
// reply barrier so the re-driven round can answer the new message
|
|
1329
|
+
// (the host itself rejects duplicate authoritative replies), and
|
|
1330
|
+
// keep any background park pending across the extra round.
|
|
1331
|
+
if (replyCommitted)
|
|
1332
|
+
this.deps.consumeToolRunYield?.("reply_committed");
|
|
1286
1333
|
continue;
|
|
1287
1334
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1335
|
+
// Park before complete_goal so a single batch cannot launch
|
|
1336
|
+
// unfinished background work and simultaneously claim the enclosing
|
|
1337
|
+
// Goal is done.
|
|
1338
|
+
if (backgroundWait && this.deps.consumeToolRunYield?.("background_notification")) {
|
|
1339
|
+
tlog.info("turn.background_notification_wait", { cat: "turn" });
|
|
1340
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1341
|
+
return {
|
|
1342
|
+
text: finalText,
|
|
1343
|
+
reason: "completed",
|
|
1344
|
+
messages,
|
|
1345
|
+
completionKind: "background_wait",
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
if (replyCommitted && this.deps.consumeToolRunYield?.("reply_committed")) {
|
|
1349
|
+
tlog.info("turn.reply_committed_stop", { cat: "turn" });
|
|
1350
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1351
|
+
return { text: finalText, reason: "completed", messages };
|
|
1352
|
+
}
|
|
1299
1353
|
}
|
|
1300
1354
|
// Goal mode P0: explicit completion. If the model called complete_goal,
|
|
1301
1355
|
// it has DECLARED the goal done — short-circuit to "completed" WITHOUT
|
|
@@ -1496,7 +1550,7 @@ export class TurnLoop {
|
|
|
1496
1550
|
});
|
|
1497
1551
|
}
|
|
1498
1552
|
else {
|
|
1499
|
-
messages = this.
|
|
1553
|
+
messages = this.manageContextMessagesSync(messages);
|
|
1500
1554
|
}
|
|
1501
1555
|
if (this.goalControlStopRequested) {
|
|
1502
1556
|
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
@@ -1512,8 +1566,9 @@ export class TurnLoop {
|
|
|
1512
1566
|
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1513
1567
|
return { text: finalText, reason: "completed", messages };
|
|
1514
1568
|
}
|
|
1515
|
-
const
|
|
1516
|
-
this.
|
|
1569
|
+
const summaryMessages = this.prepareMessagesForModel(messages);
|
|
1570
|
+
const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, summaryMessages, [], // No tools available for summary turn
|
|
1571
|
+
this.config.onStream, this.config.signal, this.modelCallRecordingOptions(summaryMessages));
|
|
1517
1572
|
if (summaryResponse.usage?.promptTokens !== undefined) {
|
|
1518
1573
|
this.recordResponseUsage(summaryResponse.usage, "primary", false);
|
|
1519
1574
|
}
|
|
@@ -1591,7 +1646,7 @@ export class TurnLoop {
|
|
|
1591
1646
|
}
|
|
1592
1647
|
: undefined;
|
|
1593
1648
|
try {
|
|
1594
|
-
return await this.deps.model.call(this.deps.systemPrompt, messages, this.deps.tools, wrappedStream, this.config.signal, this.modelCallRecordingOptions());
|
|
1649
|
+
return await this.deps.model.call(this.deps.systemPrompt, messages, this.deps.tools, wrappedStream, this.config.signal, this.modelCallRecordingOptions(messages));
|
|
1595
1650
|
}
|
|
1596
1651
|
catch (err) {
|
|
1597
1652
|
// If it's a context or rate limit error, don't fallback — propagate
|
|
@@ -1637,7 +1692,7 @@ export class TurnLoop {
|
|
|
1637
1692
|
error: err.message,
|
|
1638
1693
|
});
|
|
1639
1694
|
// Retry without streaming
|
|
1640
|
-
return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal, this.modelCallRecordingOptions());
|
|
1695
|
+
return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal, this.modelCallRecordingOptions(messages));
|
|
1641
1696
|
}
|
|
1642
1697
|
}
|
|
1643
1698
|
get currentTurn() {
|
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.9.
|
|
6
|
+
export declare const VERSION = "0.9.4";
|
|
7
7
|
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionProjectBinding, 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, CompositionError, 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.9.
|
|
6
|
+
export const VERSION = "0.9.4";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Composition (AgentModule / ResolvedComposition) ─────────────
|