@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1
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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
|
@@ -149,6 +149,12 @@ export interface DropOversizedResult {
|
|
|
149
149
|
droppedCount: number;
|
|
150
150
|
}
|
|
151
151
|
export declare function dropOversizedImages(images: readonly ParsedImage[]): DropOversizedResult;
|
|
152
|
+
export interface ImagePolicyByteInput {
|
|
153
|
+
name: string;
|
|
154
|
+
mime: string;
|
|
155
|
+
bytes: number;
|
|
156
|
+
}
|
|
157
|
+
export declare function enforceImageBytePolicy(images: readonly ImagePolicyByteInput[]): ImagePolicyVerdict;
|
|
152
158
|
export declare function enforceImagePolicy(images: readonly ParsedImage[]): ImagePolicyVerdict;
|
|
153
159
|
/**
|
|
154
160
|
* Collect the workspace file paths of any attached images that came from a
|
|
@@ -107,15 +107,13 @@ export function dropOversizedImages(images) {
|
|
|
107
107
|
if (dropped.length === 0) {
|
|
108
108
|
return { kept, placeholder: "", droppedCount: 0 };
|
|
109
109
|
}
|
|
110
|
-
const list = dropped
|
|
111
|
-
.map((d) => `「${d.name}」(~${fmtMB(d.bytes)})`)
|
|
112
|
-
.join("、");
|
|
110
|
+
const list = dropped.map((d) => `「${d.name}」(~${fmtMB(d.bytes)})`).join("、");
|
|
113
111
|
const placeholder = `[已自动跳过 ${dropped.length} 张超大图片:${list};` +
|
|
114
112
|
`单图上限 ${fmtMB(IMAGE_LIMITS.maxBytesPerImage)}。` +
|
|
115
113
|
`图片未进入对话历史,本轮其余内容继续。]`;
|
|
116
114
|
return { kept, placeholder, droppedCount: dropped.length };
|
|
117
115
|
}
|
|
118
|
-
export function
|
|
116
|
+
export function enforceImageBytePolicy(images) {
|
|
119
117
|
if (images.length === 0)
|
|
120
118
|
return { ok: true };
|
|
121
119
|
if (images.length > IMAGE_LIMITS.maxImagesPerTurn) {
|
|
@@ -126,13 +124,13 @@ export function enforceImagePolicy(images) {
|
|
|
126
124
|
`本次有 ${images.length} 张。请删掉一些再发。`,
|
|
127
125
|
totals: {
|
|
128
126
|
imageCount: images.length,
|
|
129
|
-
totalBytes: images.reduce((s, i) => s +
|
|
127
|
+
totalBytes: images.reduce((s, i) => s + i.bytes, 0),
|
|
130
128
|
},
|
|
131
129
|
};
|
|
132
130
|
}
|
|
133
131
|
let totalBytes = 0;
|
|
134
132
|
for (const img of images) {
|
|
135
|
-
const bytes =
|
|
133
|
+
const bytes = img.bytes;
|
|
136
134
|
if (bytes > IMAGE_LIMITS.maxBytesPerImage) {
|
|
137
135
|
return {
|
|
138
136
|
ok: false,
|
|
@@ -163,6 +161,13 @@ export function enforceImagePolicy(images) {
|
|
|
163
161
|
}
|
|
164
162
|
return { ok: true };
|
|
165
163
|
}
|
|
164
|
+
export function enforceImagePolicy(images) {
|
|
165
|
+
return enforceImageBytePolicy(images.map((img) => ({
|
|
166
|
+
name: img.name,
|
|
167
|
+
mime: img.mime,
|
|
168
|
+
bytes: byteLengthFromBase64(img.base64),
|
|
169
|
+
})));
|
|
170
|
+
}
|
|
166
171
|
/**
|
|
167
172
|
* Collect the workspace file paths of any attached images that came from a
|
|
168
173
|
* real file (the desktop composer's path-attach flow sets ParsedImage.name to
|
|
@@ -178,6 +183,12 @@ export function enforceImagePolicy(images) {
|
|
|
178
183
|
export function collectAttachedImagePaths(images, resolve, exists) {
|
|
179
184
|
const out = [];
|
|
180
185
|
for (const img of images) {
|
|
186
|
+
const path = img.path?.trim();
|
|
187
|
+
if (path) {
|
|
188
|
+
if (exists(resolve(path)))
|
|
189
|
+
out.push(path);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
181
192
|
const name = img.name?.trim();
|
|
182
193
|
if (!name)
|
|
183
194
|
continue;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InputAttachmentMeta } from "../protocol/types.js";
|
|
2
|
+
import type { ParsedImage } from "./parse-task.js";
|
|
3
|
+
export interface InputAttachmentContext {
|
|
4
|
+
text: string;
|
|
5
|
+
images: ParsedImage[];
|
|
6
|
+
errors: string[];
|
|
7
|
+
hasStructuredImageAttachments: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface BuildInputAttachmentContextOptions {
|
|
10
|
+
includeImageBytes?: boolean;
|
|
11
|
+
expectedSessionId?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildInputAttachmentContext(attachments: readonly InputAttachmentMeta[] | undefined, cwd: string, options?: BuildInputAttachmentContextOptions): Promise<InputAttachmentContext>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { classifyPath } from "../tool-system/path-policy.js";
|
|
5
|
+
import { enforceImageBytePolicy } from "./image-policy.js";
|
|
6
|
+
const IMAGE_MIME_BY_EXT = {
|
|
7
|
+
".png": "image/png",
|
|
8
|
+
".jpg": "image/jpeg",
|
|
9
|
+
".jpeg": "image/jpeg",
|
|
10
|
+
".gif": "image/gif",
|
|
11
|
+
".webp": "image/webp",
|
|
12
|
+
};
|
|
13
|
+
const MAX_DIRECTORY_TREE_ENTRIES = 200;
|
|
14
|
+
const MAX_DIRECTORY_TREE_DEPTH = 2;
|
|
15
|
+
export async function buildInputAttachmentContext(attachments, cwd, options = {}) {
|
|
16
|
+
if (!attachments || attachments.length === 0) {
|
|
17
|
+
return { text: "", images: [], errors: [], hasStructuredImageAttachments: false };
|
|
18
|
+
}
|
|
19
|
+
const cwdReal = await realpath(cwd);
|
|
20
|
+
const includeImageBytes = options.includeImageBytes ?? true;
|
|
21
|
+
const expectedSessionId = options.expectedSessionId;
|
|
22
|
+
const textBlocks = [];
|
|
23
|
+
const images = [];
|
|
24
|
+
const errors = [];
|
|
25
|
+
const pendingImages = [];
|
|
26
|
+
let hasStructuredImageAttachments = false;
|
|
27
|
+
if (!expectedSessionId) {
|
|
28
|
+
return {
|
|
29
|
+
text: "",
|
|
30
|
+
images: [],
|
|
31
|
+
errors: ["input attachments require an expected sessionId"],
|
|
32
|
+
hasStructuredImageAttachments: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!isSafeSessionPathSegment(expectedSessionId)) {
|
|
36
|
+
return {
|
|
37
|
+
text: "",
|
|
38
|
+
images: [],
|
|
39
|
+
errors: [`expected sessionId "${expectedSessionId}" is not a safe path segment`],
|
|
40
|
+
hasStructuredImageAttachments: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
for (const attachment of attachments) {
|
|
44
|
+
if (!attachment || typeof attachment !== "object")
|
|
45
|
+
continue;
|
|
46
|
+
const displayPath = attachment.path || attachment.relPath || attachment.absPath;
|
|
47
|
+
if (!displayPath) {
|
|
48
|
+
errors.push(`attachment ${attachment.id || "(unknown)"} has no path`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (attachment.sessionId !== expectedSessionId) {
|
|
52
|
+
errors.push(`attachment ${attachment.id || displayPath} session mismatch: expected ${expectedSessionId}, got ${attachment.sessionId}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const resolved = resolveAttachmentPath(attachment, cwd);
|
|
56
|
+
let info;
|
|
57
|
+
try {
|
|
58
|
+
info = await stat(resolved);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
errors.push(`attachment ${attachment.id || displayPath} stat failed: ${err.message}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const policy = classifyPath(resolved, { workspaceRoot: cwdReal, operation: "read" });
|
|
65
|
+
if (policy.decision !== "allow") {
|
|
66
|
+
errors.push(`attachment ${attachment.id || displayPath} blocked by path policy: ${policy.reason}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const realPath = policy.resolvedPath;
|
|
70
|
+
const stagedPathError = await validateStagedAttachmentPath(attachment, realPath, cwdReal, expectedSessionId, displayPath);
|
|
71
|
+
if (stagedPathError) {
|
|
72
|
+
errors.push(stagedPathError);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (attachment.kind === "directory" || info.isDirectory()) {
|
|
76
|
+
const tree = await directoryTree(realPath, cwdReal).catch((err) => ({
|
|
77
|
+
lines: [`(directory tree unavailable: ${err.message})`],
|
|
78
|
+
truncated: true,
|
|
79
|
+
entryCount: 0,
|
|
80
|
+
}));
|
|
81
|
+
textBlocks.push([
|
|
82
|
+
`<attached-directory path="${escapeText(displayPath)}" entries="${tree.entryCount}" truncated="${tree.truncated}">`,
|
|
83
|
+
...tree.lines,
|
|
84
|
+
`</attached-directory>`,
|
|
85
|
+
].join("\n"));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!info.isFile()) {
|
|
89
|
+
errors.push(`attachment ${attachment.id || displayPath} is not a regular file or directory`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const mime = attachment.mime || IMAGE_MIME_BY_EXT[extname(realPath).toLowerCase()];
|
|
93
|
+
if (attachment.kind === "image") {
|
|
94
|
+
hasStructuredImageAttachments = true;
|
|
95
|
+
if (!includeImageBytes || attachment.vision?.include === false) {
|
|
96
|
+
textBlocks.push(formatFileMetadata(attachment, displayPath, realPath, info.size, mime));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!mime ||
|
|
100
|
+
!mime.startsWith("image/") ||
|
|
101
|
+
!IMAGE_MIME_BY_EXT[extname(realPath).toLowerCase()]) {
|
|
102
|
+
errors.push(`image attachment ${attachment.id || displayPath} has unsupported image type`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
pendingImages.push({
|
|
106
|
+
attachment,
|
|
107
|
+
displayPath,
|
|
108
|
+
realPath,
|
|
109
|
+
mime,
|
|
110
|
+
size: info.size,
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
textBlocks.push(formatFileMetadata(attachment, displayPath, realPath, info.size, mime));
|
|
115
|
+
}
|
|
116
|
+
if (errors.length === 0 && includeImageBytes && pendingImages.length > 0) {
|
|
117
|
+
const verdict = enforceImageBytePolicy(pendingImages.map((image) => ({
|
|
118
|
+
name: image.attachment.originalName || image.displayPath,
|
|
119
|
+
mime: image.mime,
|
|
120
|
+
bytes: image.size,
|
|
121
|
+
})));
|
|
122
|
+
if (!verdict.ok) {
|
|
123
|
+
errors.push(`image attachment size policy failed: ${verdict.message}`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
for (const image of pendingImages) {
|
|
127
|
+
let bytes;
|
|
128
|
+
try {
|
|
129
|
+
bytes = await readFile(image.realPath);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
errors.push(`image attachment ${image.attachment.id || image.displayPath} read failed: ${err.message}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
136
|
+
const base64 = bytes.toString("base64");
|
|
137
|
+
images.push({
|
|
138
|
+
mime: image.mime,
|
|
139
|
+
name: image.attachment.originalName || image.displayPath,
|
|
140
|
+
dataUrl: `data:${image.mime};base64,${base64}`,
|
|
141
|
+
base64,
|
|
142
|
+
path: image.displayPath,
|
|
143
|
+
hash: `sha256:${sha256}`,
|
|
144
|
+
size: image.size,
|
|
145
|
+
origin: image.attachment.origin,
|
|
146
|
+
sessionId: image.attachment.sessionId,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { text: textBlocks.join("\n\n"), images, errors, hasStructuredImageAttachments };
|
|
152
|
+
}
|
|
153
|
+
function resolveAttachmentPath(attachment, cwd) {
|
|
154
|
+
const candidate = attachment.vision?.mediaPath || attachment.absPath || attachment.relPath || attachment.path;
|
|
155
|
+
if (isAbsolute(candidate))
|
|
156
|
+
return candidate;
|
|
157
|
+
return resolve(cwd, candidate);
|
|
158
|
+
}
|
|
159
|
+
async function validateStagedAttachmentPath(attachment, realPath, cwdReal, expectedSessionId, displayPath) {
|
|
160
|
+
if (!isStagedAttachmentReference(attachment, realPath, cwdReal))
|
|
161
|
+
return undefined;
|
|
162
|
+
const expectedDir = resolve(cwdReal, ".code-shell", "attachments", expectedSessionId);
|
|
163
|
+
let expectedDirReal;
|
|
164
|
+
try {
|
|
165
|
+
expectedDirReal = await realpath(expectedDir);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return `attachment ${attachment.id || displayPath} staged session directory is unavailable: ${err.message}`;
|
|
169
|
+
}
|
|
170
|
+
if (isPathInside(realPath, expectedDirReal))
|
|
171
|
+
return undefined;
|
|
172
|
+
return `attachment ${attachment.id || displayPath} staged path is outside .code-shell/attachments/${expectedSessionId}`;
|
|
173
|
+
}
|
|
174
|
+
function isStagedAttachmentReference(attachment, realPath, cwdReal) {
|
|
175
|
+
const fields = [
|
|
176
|
+
attachment.path,
|
|
177
|
+
attachment.absPath,
|
|
178
|
+
attachment.relPath,
|
|
179
|
+
attachment.vision?.mediaPath,
|
|
180
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
181
|
+
if (fields.some((value) => looksLikeStagedAttachmentPath(value)))
|
|
182
|
+
return true;
|
|
183
|
+
return isPathInside(realPath, resolve(cwdReal, ".code-shell", "attachments"));
|
|
184
|
+
}
|
|
185
|
+
function looksLikeStagedAttachmentPath(value) {
|
|
186
|
+
const normalized = normalizePath(value);
|
|
187
|
+
return (normalized === ".code-shell/attachments" ||
|
|
188
|
+
normalized.startsWith(".code-shell/attachments/") ||
|
|
189
|
+
normalized.includes("/.code-shell/attachments/"));
|
|
190
|
+
}
|
|
191
|
+
function isPathInside(child, parent) {
|
|
192
|
+
const rel = relative(parent, child);
|
|
193
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
194
|
+
}
|
|
195
|
+
function isSafeSessionPathSegment(sessionId) {
|
|
196
|
+
if (!sessionId)
|
|
197
|
+
return false;
|
|
198
|
+
return (!sessionId.includes("/") && !sessionId.includes("\\") && sessionId !== "." && sessionId !== "..");
|
|
199
|
+
}
|
|
200
|
+
function normalizePath(value) {
|
|
201
|
+
return value.replace(/\\/g, "/");
|
|
202
|
+
}
|
|
203
|
+
function formatFileMetadata(attachment, displayPath, realPath, size, mime) {
|
|
204
|
+
const lines = [
|
|
205
|
+
`<attached-file path="${escapeText(displayPath)}">`,
|
|
206
|
+
`absolutePath: ${realPath}`,
|
|
207
|
+
...(mime ? [`mime: ${mime}`] : []),
|
|
208
|
+
`size: ${size}`,
|
|
209
|
+
`sha256: ${attachment.sha256}`,
|
|
210
|
+
`origin: ${attachment.origin}`,
|
|
211
|
+
`</attached-file>`,
|
|
212
|
+
];
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
async function directoryTree(dir, cwdReal) {
|
|
216
|
+
const lines = [];
|
|
217
|
+
let entryCount = 0;
|
|
218
|
+
let truncated = false;
|
|
219
|
+
async function walk(current, depth) {
|
|
220
|
+
if (entryCount >= MAX_DIRECTORY_TREE_ENTRIES) {
|
|
221
|
+
truncated = true;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
225
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (entryCount >= MAX_DIRECTORY_TREE_ENTRIES) {
|
|
228
|
+
truncated = true;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
232
|
+
continue;
|
|
233
|
+
const abs = join(current, entry.name);
|
|
234
|
+
const real = await realpath(abs).catch(() => null);
|
|
235
|
+
if (!real)
|
|
236
|
+
continue;
|
|
237
|
+
const rel = relative(cwdReal, real);
|
|
238
|
+
if (rel === ".." || rel.startsWith(`..${sep}`))
|
|
239
|
+
continue;
|
|
240
|
+
entryCount += 1;
|
|
241
|
+
lines.push(`${" ".repeat(depth)}${entry.isDirectory() ? "dir " : "file "}${slash(rel)}`);
|
|
242
|
+
if (entry.isDirectory() && depth + 1 < MAX_DIRECTORY_TREE_DEPTH) {
|
|
243
|
+
await walk(real, depth + 1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
await walk(dir, 0);
|
|
248
|
+
return { lines, truncated, entryCount };
|
|
249
|
+
}
|
|
250
|
+
function escapeText(value) {
|
|
251
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
252
|
+
}
|
|
253
|
+
function slash(value) {
|
|
254
|
+
return sep === "\\" ? value.replace(/\\/g, "/") : value;
|
|
255
|
+
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import type { LLMClientBase } from "../llm/client-base.js";
|
|
5
5
|
import type { Message, ToolDefinition, LLMResponse, StreamCallback } from "../types.js";
|
|
6
6
|
import { Transcript } from "../session/transcript.js";
|
|
7
|
+
export interface ModelCallRecordingOptions {
|
|
8
|
+
sensitiveToolResultRedactions?: ReadonlyMap<string, string>;
|
|
9
|
+
}
|
|
7
10
|
/**
|
|
8
11
|
* Prompt-cache hit rate for one request, as CC computes it:
|
|
9
12
|
* cacheRead / (cacheRead + cacheCreation + uncachedInput)
|
|
@@ -18,11 +21,11 @@ export declare class ModelFacade {
|
|
|
18
21
|
private readonly client;
|
|
19
22
|
private readonly transcript;
|
|
20
23
|
constructor(client: LLMClientBase, transcript: Transcript);
|
|
21
|
-
call(systemPrompt: string, messages: Message[], tools: ToolDefinition[], onStream?: StreamCallback, signal?: AbortSignal): Promise<LLMResponse>;
|
|
24
|
+
call(systemPrompt: string, messages: Message[], tools: ToolDefinition[], onStream?: StreamCallback, signal?: AbortSignal, recordingOptions?: ModelCallRecordingOptions): Promise<LLMResponse>;
|
|
22
25
|
/**
|
|
23
26
|
* Call without streaming — used as fallback when streaming fails.
|
|
24
27
|
*/
|
|
25
|
-
callWithoutStreaming(systemPrompt: string, messages: Message[], tools: ToolDefinition[], signal?: AbortSignal): Promise<LLMResponse>;
|
|
28
|
+
callWithoutStreaming(systemPrompt: string, messages: Message[], tools: ToolDefinition[], signal?: AbortSignal, recordingOptions?: ModelCallRecordingOptions): Promise<LLMResponse>;
|
|
26
29
|
/** Record API timing and token usage to bootstrap state. */
|
|
27
30
|
private recordUsage;
|
|
28
31
|
/** Optional summarize function for tool use summaries. */
|
|
@@ -5,6 +5,7 @@ import { logger, getCurrentSid } from "../logging/logger.js";
|
|
|
5
5
|
import { recordLLMError, recordLLMRequest, recordLLMResponse, } from "../logging/session-recorder.js";
|
|
6
6
|
import { sanitizeMessages } from "../logging/sanitize-messages.js";
|
|
7
7
|
import { addAPIDuration, addToModelUsage, addInputTokens, addOutputTokens } from "../state.js";
|
|
8
|
+
import { cacheHitRateFromUsage } from "./session-usage.js";
|
|
8
9
|
let _reqSeq = 0;
|
|
9
10
|
function nextReqId() {
|
|
10
11
|
_reqSeq += 1;
|
|
@@ -20,18 +21,7 @@ function nextReqId() {
|
|
|
20
21
|
* any caching tuning (docs/todo/prompt-cache-optimization.md §四 step 1).
|
|
21
22
|
*/
|
|
22
23
|
export function cacheHitRate(usage) {
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
const read = usage.cacheReadTokens ?? 0;
|
|
26
|
-
const creation = usage.cacheCreationTokens ?? 0;
|
|
27
|
-
if (read === 0 && creation === 0)
|
|
28
|
-
return undefined;
|
|
29
|
-
const prompt = usage.promptTokens ?? 0;
|
|
30
|
-
const uncached = Math.max(0, prompt - read - creation);
|
|
31
|
-
const denom = read + creation + uncached;
|
|
32
|
-
if (denom === 0)
|
|
33
|
-
return undefined;
|
|
34
|
-
return read / denom;
|
|
24
|
+
return cacheHitRateFromUsage(usage);
|
|
35
25
|
}
|
|
36
26
|
export class ModelFacade {
|
|
37
27
|
client;
|
|
@@ -40,7 +30,7 @@ export class ModelFacade {
|
|
|
40
30
|
this.client = client;
|
|
41
31
|
this.transcript = transcript;
|
|
42
32
|
}
|
|
43
|
-
async call(systemPrompt, messages, tools, onStream, signal) {
|
|
33
|
+
async call(systemPrompt, messages, tools, onStream, signal, recordingOptions) {
|
|
44
34
|
const startMs = Date.now();
|
|
45
35
|
const msgCount = messages.length;
|
|
46
36
|
const sid = getCurrentSid();
|
|
@@ -52,7 +42,7 @@ export class ModelFacade {
|
|
|
52
42
|
// Image base64 payloads stay out of recorded prompts — see
|
|
53
43
|
// logging/sanitize-messages.ts. Transcripts keep the full bytes
|
|
54
44
|
// (needed for replay); logs do not.
|
|
55
|
-
messages: sanitizeMessages(messages),
|
|
45
|
+
messages: sanitizeMessages(messages, recordingOptions),
|
|
56
46
|
tools,
|
|
57
47
|
systemPrompt,
|
|
58
48
|
}, reqId);
|
|
@@ -121,7 +111,7 @@ export class ModelFacade {
|
|
|
121
111
|
/**
|
|
122
112
|
* Call without streaming — used as fallback when streaming fails.
|
|
123
113
|
*/
|
|
124
|
-
async callWithoutStreaming(systemPrompt, messages, tools, signal) {
|
|
114
|
+
async callWithoutStreaming(systemPrompt, messages, tools, signal, recordingOptions) {
|
|
125
115
|
const startMs = Date.now();
|
|
126
116
|
const msgCount = messages.length;
|
|
127
117
|
const sid = getCurrentSid();
|
|
@@ -132,7 +122,7 @@ export class ModelFacade {
|
|
|
132
122
|
stream: false,
|
|
133
123
|
// Image base64 payloads stay out of recorded prompts — same rule
|
|
134
124
|
// as the streaming path above.
|
|
135
|
-
messages: sanitizeMessages(messages),
|
|
125
|
+
messages: sanitizeMessages(messages, recordingOptions),
|
|
136
126
|
tools,
|
|
137
127
|
systemPrompt,
|
|
138
128
|
}, reqId);
|
|
@@ -29,6 +29,16 @@ export interface ParsedImage {
|
|
|
29
29
|
dataUrl: string;
|
|
30
30
|
/** Just the base64 payload, with the data-URL prefix stripped. */
|
|
31
31
|
base64: string;
|
|
32
|
+
/** Stable on-disk path, usually cwd-relative for staged desktop attachments. */
|
|
33
|
+
path?: string;
|
|
34
|
+
/** Wire hash attr, usually `sha256:<hex>`. */
|
|
35
|
+
hash?: string;
|
|
36
|
+
/** Size in bytes from wire metadata. Core must still verify with stat when reading. */
|
|
37
|
+
size?: number;
|
|
38
|
+
/** Source channel that produced this attachment. */
|
|
39
|
+
origin?: string;
|
|
40
|
+
/** Session directory that owns the staged attachment. */
|
|
41
|
+
sessionId?: string;
|
|
32
42
|
}
|
|
33
43
|
export interface ParsedTask {
|
|
34
44
|
/** Remaining plain-text portion (image blocks removed, surrounding whitespace trimmed). */
|
|
@@ -117,6 +117,11 @@ export function parseTaskWithImages(task) {
|
|
|
117
117
|
name: attrs.name ?? "",
|
|
118
118
|
dataUrl: `data:${finalMime};base64,${base64}`,
|
|
119
119
|
base64,
|
|
120
|
+
...(attrs.path ? { path: attrs.path } : {}),
|
|
121
|
+
...(attrs.hash ? { hash: attrs.hash } : {}),
|
|
122
|
+
...(attrs.size && Number.isFinite(Number(attrs.size)) ? { size: Number(attrs.size) } : {}),
|
|
123
|
+
...(attrs.origin ? { origin: attrs.origin } : {}),
|
|
124
|
+
...(attrs.sessionid ? { sessionId: attrs.sessionid } : {}),
|
|
120
125
|
});
|
|
121
126
|
return "";
|
|
122
127
|
});
|
package/dist/engine/query.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { TurnLoop } from "./turn-loop.js";
|
|
11
11
|
import { logger } from "../logging/logger.js";
|
|
12
|
+
import { messageHasBase64ImagePayload } from "../context/compaction.js";
|
|
12
13
|
// ─── Query generator ────────────────────────────────────────────────
|
|
13
14
|
/**
|
|
14
15
|
* Run an agentic query and yield stream events as they occur.
|
|
@@ -37,6 +38,7 @@ export async function* query(params) {
|
|
|
37
38
|
maxToolCallsPerTurn,
|
|
38
39
|
onStream,
|
|
39
40
|
signal,
|
|
41
|
+
freshImageMessages: messages.filter(messageHasBase64ImagePayload),
|
|
40
42
|
};
|
|
41
43
|
// Local overhead store — query() is a standalone entry point without a real
|
|
42
44
|
// session id, so per-call in-memory state is enough.
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { TokenUsage } from "../types.js";
|
|
2
2
|
import type { LLMUsageTracker } from "../llm/types.js";
|
|
3
|
+
export interface CumulativeUsageCounters {
|
|
4
|
+
cumulativePromptTokens: number;
|
|
5
|
+
cumulativeCacheReadTokens: number;
|
|
6
|
+
cumulativeCacheCreationTokens: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function emptyCumulativeUsageCounters(): CumulativeUsageCounters;
|
|
9
|
+
export declare function normalizeCumulativeUsageCounters(counters: Partial<CumulativeUsageCounters> | undefined, legacyUsage?: TokenUsage): CumulativeUsageCounters;
|
|
10
|
+
export declare function addCumulativeUsage(counters: Partial<CumulativeUsageCounters> | undefined, usage: TokenUsage): CumulativeUsageCounters;
|
|
11
|
+
export declare function addTokenUsage(left: TokenUsage, right: TokenUsage): TokenUsage;
|
|
12
|
+
export declare function cacheHitRateFromTokens(promptTokens: number, cacheReadTokens: number | undefined, cacheCreationTokens: number | undefined): number | undefined;
|
|
13
|
+
export declare function cacheHitRateFromUsage(usage: TokenUsage | undefined): number | undefined;
|
|
14
|
+
export declare function cumulativeCacheHitRate(counters: CumulativeUsageCounters): number | undefined;
|
|
3
15
|
/**
|
|
4
16
|
* Fold one run's cumulative usage onto a session baseline, producing the new
|
|
5
17
|
* session-cumulative TokenUsage.
|
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
export function emptyCumulativeUsageCounters() {
|
|
2
|
+
return {
|
|
3
|
+
cumulativePromptTokens: 0,
|
|
4
|
+
cumulativeCacheReadTokens: 0,
|
|
5
|
+
cumulativeCacheCreationTokens: 0,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function normalizeCumulativeUsageCounters(counters, legacyUsage) {
|
|
9
|
+
return {
|
|
10
|
+
cumulativePromptTokens: typeof counters?.cumulativePromptTokens === "number"
|
|
11
|
+
? counters.cumulativePromptTokens
|
|
12
|
+
: (legacyUsage?.promptTokens ?? 0),
|
|
13
|
+
cumulativeCacheReadTokens: typeof counters?.cumulativeCacheReadTokens === "number"
|
|
14
|
+
? counters.cumulativeCacheReadTokens
|
|
15
|
+
: (legacyUsage?.cacheReadTokens ?? 0),
|
|
16
|
+
cumulativeCacheCreationTokens: typeof counters?.cumulativeCacheCreationTokens === "number"
|
|
17
|
+
? counters.cumulativeCacheCreationTokens
|
|
18
|
+
: (legacyUsage?.cacheCreationTokens ?? 0),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function addCumulativeUsage(counters, usage) {
|
|
22
|
+
const current = normalizeCumulativeUsageCounters(counters);
|
|
23
|
+
return {
|
|
24
|
+
cumulativePromptTokens: current.cumulativePromptTokens + (usage.promptTokens ?? 0),
|
|
25
|
+
cumulativeCacheReadTokens: current.cumulativeCacheReadTokens + (usage.cacheReadTokens ?? 0),
|
|
26
|
+
cumulativeCacheCreationTokens: current.cumulativeCacheCreationTokens + (usage.cacheCreationTokens ?? 0),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function addTokenUsage(left, right) {
|
|
30
|
+
return {
|
|
31
|
+
promptTokens: left.promptTokens + (right.promptTokens ?? 0),
|
|
32
|
+
completionTokens: left.completionTokens + (right.completionTokens ?? 0),
|
|
33
|
+
totalTokens: left.totalTokens + (right.totalTokens ?? 0),
|
|
34
|
+
cacheReadTokens: (left.cacheReadTokens ?? 0) + (right.cacheReadTokens ?? 0),
|
|
35
|
+
cacheCreationTokens: (left.cacheCreationTokens ?? 0) + (right.cacheCreationTokens ?? 0),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function cacheHitRateFromTokens(promptTokens, cacheReadTokens, cacheCreationTokens) {
|
|
39
|
+
const read = cacheReadTokens ?? 0;
|
|
40
|
+
const creation = cacheCreationTokens ?? 0;
|
|
41
|
+
if (read === 0 && creation === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
const uncached = Math.max(0, promptTokens - read - creation);
|
|
44
|
+
const denom = read + creation + uncached;
|
|
45
|
+
if (denom === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return read / denom;
|
|
48
|
+
}
|
|
49
|
+
export function cacheHitRateFromUsage(usage) {
|
|
50
|
+
if (!usage)
|
|
51
|
+
return undefined;
|
|
52
|
+
return cacheHitRateFromTokens(usage.promptTokens ?? 0, usage.cacheReadTokens, usage.cacheCreationTokens);
|
|
53
|
+
}
|
|
54
|
+
export function cumulativeCacheHitRate(counters) {
|
|
55
|
+
return cacheHitRateFromTokens(counters.cumulativePromptTokens, counters.cumulativeCacheReadTokens, counters.cumulativeCacheCreationTokens);
|
|
56
|
+
}
|
|
1
57
|
/**
|
|
2
58
|
* Fold one run's cumulative usage onto a session baseline, producing the new
|
|
3
59
|
* session-cumulative TokenUsage.
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
export interface SteerItem {
|
|
10
10
|
id: string;
|
|
11
11
|
text: string;
|
|
12
|
+
clientMessageId?: string;
|
|
12
13
|
}
|
|
13
14
|
/** Append a steer entry. Blank text is dropped (returns the list unchanged). */
|
|
14
|
-
export declare function enqueueSteerItem(list: SteerItem[], id: string, text: string): SteerItem[];
|
|
15
|
+
export declare function enqueueSteerItem(list: SteerItem[], id: string, text: string, clientMessageId?: string): SteerItem[];
|
|
15
16
|
/**
|
|
16
17
|
* Take everything currently queued and clear the list. Returns the drained
|
|
17
18
|
* entries (in order) and the now-empty remainder. The turn loop calls this at
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Append a steer entry. Blank text is dropped (returns the list unchanged). */
|
|
2
|
-
export function enqueueSteerItem(list, id, text) {
|
|
2
|
+
export function enqueueSteerItem(list, id, text, clientMessageId) {
|
|
3
3
|
const t = text?.trim();
|
|
4
4
|
if (!id || !t)
|
|
5
5
|
return list;
|
|
6
|
-
return [...list, { id, text: t }];
|
|
6
|
+
return [...list, { id, text: t, ...(clientMessageId ? { clientMessageId } : {}) }];
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Take everything currently queued and clear the list. Returns the drained
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tool execution queue for a completed model response.
|
|
3
|
+
*
|
|
4
|
+
* Despite the historical name, TurnLoop currently enqueues tool calls only
|
|
5
|
+
* after `callModelWithFallback()` returns a complete LLMResponse. The queue's
|
|
6
|
+
* job is concurrency policy inside that post-response batch: start
|
|
7
|
+
* concurrency-safe tools as soon as they are enqueued, keep unsafe tools
|
|
8
|
+
* sequential, and return results in original tool-call order.
|
|
5
9
|
*
|
|
6
10
|
* Usage:
|
|
7
11
|
* const queue = new StreamingToolQueue(executor);
|
|
8
|
-
* //
|
|
12
|
+
* // After the full LLMResponse is available:
|
|
9
13
|
* queue.enqueue(toolCall);
|
|
10
|
-
* // After streaming completes:
|
|
11
14
|
* const results = await queue.drain();
|
|
12
15
|
*/
|
|
13
16
|
import type { ToolCall, ToolResult } from "../types.js";
|
|
@@ -21,8 +24,9 @@ export declare class StreamingToolQueue {
|
|
|
21
24
|
private draining;
|
|
22
25
|
constructor(executor: ToolExecutor);
|
|
23
26
|
/**
|
|
24
|
-
* Enqueue a tool
|
|
25
|
-
* unsafe tools are queued for
|
|
27
|
+
* Enqueue a tool from the completed response. Concurrency-safe tools start
|
|
28
|
+
* immediately within this post-response phase; unsafe tools are queued for
|
|
29
|
+
* sequential execution during drain().
|
|
26
30
|
*/
|
|
27
31
|
enqueue(call: ToolCall): void;
|
|
28
32
|
/**
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tool execution queue for a completed model response.
|
|
3
|
+
*
|
|
4
|
+
* Despite the historical name, TurnLoop currently enqueues tool calls only
|
|
5
|
+
* after `callModelWithFallback()` returns a complete LLMResponse. The queue's
|
|
6
|
+
* job is concurrency policy inside that post-response batch: start
|
|
7
|
+
* concurrency-safe tools as soon as they are enqueued, keep unsafe tools
|
|
8
|
+
* sequential, and return results in original tool-call order.
|
|
5
9
|
*
|
|
6
10
|
* Usage:
|
|
7
11
|
* const queue = new StreamingToolQueue(executor);
|
|
8
|
-
* //
|
|
12
|
+
* // After the full LLMResponse is available:
|
|
9
13
|
* queue.enqueue(toolCall);
|
|
10
|
-
* // After streaming completes:
|
|
11
14
|
* const results = await queue.drain();
|
|
12
15
|
*/
|
|
13
16
|
export class StreamingToolQueue {
|
|
@@ -21,8 +24,9 @@ export class StreamingToolQueue {
|
|
|
21
24
|
this.executor = executor;
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
|
-
* Enqueue a tool
|
|
25
|
-
* unsafe tools are queued for
|
|
27
|
+
* Enqueue a tool from the completed response. Concurrency-safe tools start
|
|
28
|
+
* immediately within this post-response phase; unsafe tools are queued for
|
|
29
|
+
* sequential execution during drain().
|
|
26
30
|
*/
|
|
27
31
|
enqueue(call) {
|
|
28
32
|
this.callOrder.push(call.id);
|