@cjhyy/code-shell-core 0.9.3 → 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/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/panel-apps/manifest.d.ts +12 -12
- package/dist/profile/types.d.ts +26 -26
- package/dist/session/session-manager.js +30 -1
- 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/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) ─────────────
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-neutral prompt-cache planning.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the semantic cache policy (session affinity and reusable
|
|
5
|
+
* prefix boundaries). Provider clients only translate the plan to their wire
|
|
6
|
+
* format: OpenAI `prompt_cache_*` fields or Anthropic `cache_control` blocks.
|
|
7
|
+
*/
|
|
8
|
+
export type PromptCacheStrategy = "openai-explicit" | "openai-implicit" | "anthropic-explicit" | "provider-managed";
|
|
9
|
+
export type PromptCacheBreakpoint = "system" | "tools" | "stable-history" | "rolling-history";
|
|
10
|
+
export interface PromptCacheRequestContext {
|
|
11
|
+
/** Stable run/session namespace used for provider cache affinity. */
|
|
12
|
+
scopeId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Number of source messages before the first volatile context message.
|
|
15
|
+
* Providers use this to retain a reusable durable-history breakpoint while
|
|
16
|
+
* also advancing a rolling breakpoint over the append-only in-run tail.
|
|
17
|
+
*/
|
|
18
|
+
stablePrefixMessageCount?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface PromptCachePolicy {
|
|
21
|
+
strategy: PromptCacheStrategy;
|
|
22
|
+
layoutVersion: string;
|
|
23
|
+
breakpoints: readonly PromptCacheBreakpoint[];
|
|
24
|
+
/** Opaque and <=64 chars, as required by OpenAI's prompt_cache_key. */
|
|
25
|
+
cacheKey?: string;
|
|
26
|
+
/** GPT-5.6+ explicit-cache request mode. */
|
|
27
|
+
promptCacheOptions?: {
|
|
28
|
+
mode: "explicit";
|
|
29
|
+
ttl: "30m";
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface ResolvePromptCachePolicyInput {
|
|
33
|
+
provider: string;
|
|
34
|
+
providerKind?: string;
|
|
35
|
+
model: string;
|
|
36
|
+
request?: PromptCacheRequestContext;
|
|
37
|
+
/** Sticky compatibility fallback after an endpoint rejects explicit fields. */
|
|
38
|
+
explicitDisabled?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Produce a privacy-preserving stable affinity key without leaking a raw
|
|
42
|
+
* session id to the provider. The prefix plus 48 hex chars is 51 characters.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createPromptCacheKey(scopeId: string, namespace: string): string;
|
|
45
|
+
/** Resolve one cache policy from the actual provider route and model family. */
|
|
46
|
+
export declare function resolvePromptCachePolicy(input: ResolvePromptCachePolicyInput): PromptCachePolicy;
|
|
47
|
+
/** Deduplicate semantic boundaries while preserving their left-to-right order. */
|
|
48
|
+
export declare function uniquePromptCacheBreakpointIndexes(indexes: readonly (number | undefined)[]): number[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-neutral prompt-cache planning.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the semantic cache policy (session affinity and reusable
|
|
5
|
+
* prefix boundaries). Provider clients only translate the plan to their wire
|
|
6
|
+
* format: OpenAI `prompt_cache_*` fields or Anthropic `cache_control` blocks.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
const OPENAI_EXPLICIT_BREAKPOINTS = ["system", "stable-history", "rolling-history"];
|
|
10
|
+
const ANTHROPIC_BREAKPOINTS = ["system", "tools", "stable-history", "rolling-history"];
|
|
11
|
+
const OPENROUTER_ANTHROPIC_BREAKPOINTS = ["system", "stable-history", "rolling-history"];
|
|
12
|
+
function normalizedModel(model) {
|
|
13
|
+
return model.replace(/^~/, "");
|
|
14
|
+
}
|
|
15
|
+
function isAnthropicModel(model) {
|
|
16
|
+
return /^anthropic\/claude-/i.test(normalizedModel(model));
|
|
17
|
+
}
|
|
18
|
+
function isOpenAIModel(model) {
|
|
19
|
+
const normalized = normalizedModel(model);
|
|
20
|
+
return /^openai\//i.test(normalized) || /^(?:gpt-|o\d)/i.test(normalized);
|
|
21
|
+
}
|
|
22
|
+
/** GPT-5.6 and later 5.x releases support explicit prompt-cache breakpoints. */
|
|
23
|
+
function supportsOpenAIExplicitCaching(model) {
|
|
24
|
+
const normalized = normalizedModel(model).replace(/^openai\//i, "");
|
|
25
|
+
const match = /^gpt-5\.(\d+)(?:[-.]|$)/i.exec(normalized);
|
|
26
|
+
return match !== null && Number(match[1]) >= 6;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Produce a privacy-preserving stable affinity key without leaking a raw
|
|
30
|
+
* session id to the provider. The prefix plus 48 hex chars is 51 characters.
|
|
31
|
+
*/
|
|
32
|
+
export function createPromptCacheKey(scopeId, namespace) {
|
|
33
|
+
const digest = createHash("sha256")
|
|
34
|
+
.update("codeshell-prompt-cache-v1\0")
|
|
35
|
+
.update(namespace)
|
|
36
|
+
.update("\0")
|
|
37
|
+
.update(scopeId)
|
|
38
|
+
.digest("hex")
|
|
39
|
+
.slice(0, 48);
|
|
40
|
+
return `cs:${digest}`;
|
|
41
|
+
}
|
|
42
|
+
/** Resolve one cache policy from the actual provider route and model family. */
|
|
43
|
+
export function resolvePromptCachePolicy(input) {
|
|
44
|
+
const kind = (input.providerKind ?? input.provider).toLowerCase();
|
|
45
|
+
const model = normalizedModel(input.model);
|
|
46
|
+
const key = input.request?.scopeId
|
|
47
|
+
? createPromptCacheKey(input.request.scopeId, `${kind}:${model}`)
|
|
48
|
+
: undefined;
|
|
49
|
+
if (input.provider === "anthropic" || kind === "anthropic") {
|
|
50
|
+
return {
|
|
51
|
+
strategy: "anthropic-explicit",
|
|
52
|
+
layoutVersion: "system-tools-stable-rolling-v2",
|
|
53
|
+
breakpoints: ANTHROPIC_BREAKPOINTS,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (kind === "openrouter" && isAnthropicModel(model)) {
|
|
57
|
+
return {
|
|
58
|
+
strategy: "anthropic-explicit",
|
|
59
|
+
// OpenRouter/Anthropic includes tools in the system-prefix cache entry,
|
|
60
|
+
// so a separate tool marker is unnecessary and preserves one slot.
|
|
61
|
+
layoutVersion: "system-stable-rolling-v2",
|
|
62
|
+
breakpoints: OPENROUTER_ANTHROPIC_BREAKPOINTS,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const openAIRoute = kind === "openai" || (kind === "openrouter" && isOpenAIModel(model));
|
|
66
|
+
if (openAIRoute && supportsOpenAIExplicitCaching(model) && input.explicitDisabled !== true) {
|
|
67
|
+
return {
|
|
68
|
+
strategy: "openai-explicit",
|
|
69
|
+
layoutVersion: "system-stable-rolling-v1",
|
|
70
|
+
breakpoints: OPENAI_EXPLICIT_BREAKPOINTS,
|
|
71
|
+
...(key ? { cacheKey: key } : {}),
|
|
72
|
+
promptCacheOptions: { mode: "explicit", ttl: "30m" },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (openAIRoute) {
|
|
76
|
+
return {
|
|
77
|
+
strategy: "openai-implicit",
|
|
78
|
+
layoutVersion: "implicit-affinity-v1",
|
|
79
|
+
breakpoints: [],
|
|
80
|
+
...(key ? { cacheKey: key } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
strategy: "provider-managed",
|
|
85
|
+
layoutVersion: "append-only-v1",
|
|
86
|
+
breakpoints: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Deduplicate semantic boundaries while preserving their left-to-right order. */
|
|
90
|
+
export function uniquePromptCacheBreakpointIndexes(indexes) {
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const result = [];
|
|
93
|
+
for (const index of indexes) {
|
|
94
|
+
if (index === undefined || index < 0 || seen.has(index))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(index);
|
|
97
|
+
result.push(index);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
@@ -16,6 +16,7 @@ export declare class AnthropicClient extends LLMClientBase {
|
|
|
16
16
|
*/
|
|
17
17
|
private _capability;
|
|
18
18
|
private get capability();
|
|
19
|
+
private promptCachePolicy;
|
|
19
20
|
getPromptCacheConfigIdentity(): Readonly<Record<string, unknown>>;
|
|
20
21
|
/**
|
|
21
22
|
* Translate the resolved ReasoningSetting into Anthropic's `thinking` field,
|
|
@@ -44,7 +45,9 @@ export declare class AnthropicClient extends LLMClientBase {
|
|
|
44
45
|
private nonStreamMessage;
|
|
45
46
|
private streamMessage;
|
|
46
47
|
private processResponse;
|
|
48
|
+
private buildSystem;
|
|
47
49
|
private buildMessages;
|
|
50
|
+
private markAnthropicCacheBreakpoint;
|
|
48
51
|
private convertTools;
|
|
49
52
|
private handleApiError;
|
|
50
53
|
}
|