@turingflow/agent-kit 0.0.0-stage → 0.1.0
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/README.md +546 -2
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +31 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +105 -0
- package/dist/handlers.d.ts +92 -0
- package/dist/handlers.js +211 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/sdk.d.ts +106 -0
- package/dist/sdk.js +309 -0
- package/dist/server/agent.d.ts +27 -0
- package/dist/server/agent.js +31 -0
- package/dist/server/artifact-hooks.d.ts +6 -0
- package/dist/server/artifact-hooks.js +37 -0
- package/dist/server/artifact-tool-tracker.d.ts +24 -0
- package/dist/server/artifact-tool-tracker.js +347 -0
- package/dist/server/artifacts.d.ts +227 -0
- package/dist/server/artifacts.js +1061 -0
- package/dist/server/auth/config.d.ts +34 -0
- package/dist/server/auth/config.js +82 -0
- package/dist/server/auth/db.d.ts +159 -0
- package/dist/server/auth/db.js +680 -0
- package/dist/server/auth/handlers.d.ts +27 -0
- package/dist/server/auth/handlers.js +297 -0
- package/dist/server/auth/index.d.ts +15 -0
- package/dist/server/auth/index.js +14 -0
- package/dist/server/auth/login-options.d.ts +14 -0
- package/dist/server/auth/login-options.js +50 -0
- package/dist/server/auth/providers/dingtalk.d.ts +16 -0
- package/dist/server/auth/providers/dingtalk.js +177 -0
- package/dist/server/auth/providers/feishu.d.ts +15 -0
- package/dist/server/auth/providers/feishu.js +100 -0
- package/dist/server/auth/providers/index.d.ts +13 -0
- package/dist/server/auth/providers/index.js +24 -0
- package/dist/server/auth/session.d.ts +48 -0
- package/dist/server/auth/session.js +108 -0
- package/dist/server/auth/types.d.ts +118 -0
- package/dist/server/auth/types.js +7 -0
- package/dist/server/browser-agent-mcp.d.ts +67 -0
- package/dist/server/browser-agent-mcp.js +608 -0
- package/dist/server/cli.d.ts +6 -0
- package/dist/server/cli.js +112 -0
- package/dist/server/debug-log.d.ts +25 -0
- package/dist/server/debug-log.js +86 -0
- package/dist/server/handlers.d.ts +141 -0
- package/dist/server/handlers.js +523 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.js +28 -0
- package/dist/server/kit-api.d.ts +23 -0
- package/dist/server/kit-api.js +153 -0
- package/dist/server/model-sources.d.ts +32 -0
- package/dist/server/model-sources.js +58 -0
- package/dist/server/oa.d.ts +87 -0
- package/dist/server/oa.js +397 -0
- package/dist/server/providers/openrouter.d.ts +51 -0
- package/dist/server/providers/openrouter.js +283 -0
- package/dist/server/render-markdown.d.ts +2 -0
- package/dist/server/render-markdown.js +50 -0
- package/dist/server/sdk.d.ts +259 -0
- package/dist/server/sdk.js +1991 -0
- package/dist/server/session-access.d.ts +5 -0
- package/dist/server/session-access.js +68 -0
- package/dist/server/session-fork.d.ts +10 -0
- package/dist/server/session-fork.js +142 -0
- package/dist/server/session-store.d.ts +56 -0
- package/dist/server/session-store.js +148 -0
- package/dist/server/sessions.d.ts +24 -0
- package/dist/server/sessions.js +541 -0
- package/dist/server/share.d.ts +15 -0
- package/dist/server/share.js +433 -0
- package/dist/server/tool-activity.d.ts +2 -0
- package/dist/server/tool-activity.js +26 -0
- package/dist/server/transcription/xiaomi-mimo.d.ts +8 -0
- package/dist/server/transcription/xiaomi-mimo.js +86 -0
- package/dist/server/upload.d.ts +13 -0
- package/dist/server/upload.js +81 -0
- package/dist/server/user-files.d.ts +28 -0
- package/dist/server/user-files.js +311 -0
- package/dist/server/voice.d.ts +25 -0
- package/dist/server/voice.js +61 -0
- package/dist/server/web-fetch.d.ts +71 -0
- package/dist/server/web-fetch.js +330 -0
- package/dist/styles.css +3354 -0
- package/dist/transcription/xiaomi-mimo.d.ts +8 -0
- package/dist/transcription/xiaomi-mimo.js +86 -0
- package/dist/ui/index.d.ts +352 -0
- package/dist/ui/index.js +7730 -0
- package/dist/upload.d.ts +13 -0
- package/dist/upload.js +81 -0
- package/dist/voice.d.ts +25 -0
- package/dist/voice.js +61 -0
- package/package.json +74 -4
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute file changes to actual tool executions. Explicit file tools retain a
|
|
3
|
+
* before image; shell tools compare bounded filesystem snapshots around the call.
|
|
4
|
+
* This is deliberately independent of command syntax (Python, redirects, scripts,
|
|
5
|
+
* and PowerShell all work), and never sweeps the server's implicit process.cwd().
|
|
6
|
+
*
|
|
7
|
+
* Shell attribution covers the supplied workspace/cwd and this session's output
|
|
8
|
+
* directories only. Dependencies, builds, caches, logs, staging and symlinks are
|
|
9
|
+
* excluded. Concurrent external writes inside those roots cannot be distinguished
|
|
10
|
+
* from a shell's writes; background commands that finish after flush are likewise
|
|
11
|
+
* outside the observation window. Explicit file tools do not have these scan limits.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { capturePreWriteSnapshot, isIntermediateRel, isTextDiffable, normalizeArtifactAbsPath, registerArtifactPath, sessionDirCandidates, } from "./artifacts.js";
|
|
17
|
+
const FILE_TOOLS = new Set(["write", "edit", "multiedit", "notebookedit"]);
|
|
18
|
+
const SHELL_TOOLS = new Set(["bash", "powershell"]);
|
|
19
|
+
const SKIP_DIRECTORIES = new Set([
|
|
20
|
+
".agent", ".git", ".hg", ".svn", ".codebuddy", ".workbuddy", ".browser-agent-profile",
|
|
21
|
+
".web-fetch-profile", ".shots", "node_modules", "vendor", ".venv", "venv", "__pycache__",
|
|
22
|
+
".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".next", ".nuxt", ".turbo",
|
|
23
|
+
".vite", "coverage", "dist", "build", "out", "target", "bin", "obj", "logs", "log",
|
|
24
|
+
"tmp", "temp", "_tmp", "_drafts", "_intermediate", "attachments", "tool-results", "subagents",
|
|
25
|
+
]);
|
|
26
|
+
const SKIP_FILES = /(?:\.(?:log|tmp|temp|swp|swo|pid|tsbuildinfo)|(?:^|[.])DS_Store|^Thumbs\.db)$/i;
|
|
27
|
+
const CONTENT_LIMIT = 512 * 1024;
|
|
28
|
+
const ABSENT = { exists: false, mtimeNs: 0n, size: 0, content: null };
|
|
29
|
+
const DEFAULT_LIMITS = {
|
|
30
|
+
maxEntries: 20_000,
|
|
31
|
+
maxFiles: 8_000,
|
|
32
|
+
maxDirectories: 1_500,
|
|
33
|
+
maxDepth: 24,
|
|
34
|
+
maxTotalContentBytes: 32 * 1024 * 1024,
|
|
35
|
+
maxDurationMs: 5_000,
|
|
36
|
+
};
|
|
37
|
+
function isWithin(abs, root) {
|
|
38
|
+
const rel = path.relative(root, abs);
|
|
39
|
+
return rel === "" || (!path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + path.sep));
|
|
40
|
+
}
|
|
41
|
+
/** Unknown (unreadable/truncated/excluded) is not the same as absent. */
|
|
42
|
+
function snapshotStamp(snapshot, abs) {
|
|
43
|
+
const known = snapshot.files.get(abs);
|
|
44
|
+
if (known)
|
|
45
|
+
return known;
|
|
46
|
+
for (const root of snapshot.missingRoots)
|
|
47
|
+
if (isWithin(abs, root))
|
|
48
|
+
return ABSENT;
|
|
49
|
+
let child = abs;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const parent = path.dirname(child);
|
|
52
|
+
if (child === parent)
|
|
53
|
+
return undefined;
|
|
54
|
+
const names = snapshot.directories.get(parent);
|
|
55
|
+
if (names && !names.has(path.basename(child)))
|
|
56
|
+
return ABSENT;
|
|
57
|
+
child = parent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function sameStamp(before, after) {
|
|
61
|
+
if (before.exists === false || after.exists === false)
|
|
62
|
+
return before.exists === after.exists;
|
|
63
|
+
if (before.digest && after.digest)
|
|
64
|
+
return before.digest === after.digest;
|
|
65
|
+
if (before.content != null && after.content != null)
|
|
66
|
+
return before.content === after.content;
|
|
67
|
+
return before.mtimeNs === after.mtimeNs && before.size === after.size;
|
|
68
|
+
}
|
|
69
|
+
function fingerprint(input) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(input);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function extractPaths(input) {
|
|
78
|
+
const paths = new Set();
|
|
79
|
+
const add = (record) => {
|
|
80
|
+
for (const key of ["file_path", "path", "notebook_path"]) {
|
|
81
|
+
if (typeof record[key] === "string" && record[key].trim())
|
|
82
|
+
paths.add(record[key]);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// MultiEdit normally has ONE top-level file_path, with only replacement pairs
|
|
86
|
+
// inside edits; some providers instead supply a path on every edit.
|
|
87
|
+
add(input);
|
|
88
|
+
if (Array.isArray(input.edits)) {
|
|
89
|
+
for (const edit of input.edits)
|
|
90
|
+
if (edit && typeof edit === "object")
|
|
91
|
+
add(edit);
|
|
92
|
+
}
|
|
93
|
+
return [...paths];
|
|
94
|
+
}
|
|
95
|
+
export function createArtifactToolTracker(options) {
|
|
96
|
+
const { sessionId, workspaceDir, cwd, preWrite } = options;
|
|
97
|
+
const limits = { ...DEFAULT_LIMITS, ...options.scanLimits };
|
|
98
|
+
const warnings = new Set();
|
|
99
|
+
const warn = (reason) => {
|
|
100
|
+
if (warnings.has(reason))
|
|
101
|
+
return;
|
|
102
|
+
warnings.add(reason);
|
|
103
|
+
const message = `[artifacts] ${reason}`;
|
|
104
|
+
try {
|
|
105
|
+
(options.onWarning ?? console.warn)(message);
|
|
106
|
+
}
|
|
107
|
+
catch { /* Observability must not stop a tool. */ }
|
|
108
|
+
};
|
|
109
|
+
const roots = [];
|
|
110
|
+
for (const supplied of [workspaceDir, cwd]) {
|
|
111
|
+
if (!supplied?.trim())
|
|
112
|
+
continue;
|
|
113
|
+
const abs = path.resolve(supplied.trim());
|
|
114
|
+
// Even an explicitly configured drive root is not a useful scan boundary.
|
|
115
|
+
if (abs === path.parse(abs).root) {
|
|
116
|
+
warn("Shell artifact scanning skipped a filesystem root; use a specific workspace directory.");
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!roots.some((root) => root.abs === abs))
|
|
120
|
+
roots.push({ abs, session: false });
|
|
121
|
+
}
|
|
122
|
+
// The normal workspace walk skips all .agent directories. Add only this
|
|
123
|
+
// session's roots explicitly, including nested documents and sandbox outputs.
|
|
124
|
+
for (const candidate of sessionDirCandidates(sessionId, workspaceDir)) {
|
|
125
|
+
const abs = path.resolve(candidate);
|
|
126
|
+
const existing = roots.find((root) => root.abs === abs);
|
|
127
|
+
if (existing)
|
|
128
|
+
existing.session = true;
|
|
129
|
+
else
|
|
130
|
+
roots.push({ abs, session: true });
|
|
131
|
+
}
|
|
132
|
+
const scan = async () => {
|
|
133
|
+
const result = { files: new Map(), directories: new Map(), missingRoots: new Set() };
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
let entries = 0;
|
|
136
|
+
let directories = 0;
|
|
137
|
+
let contentBytes = 0;
|
|
138
|
+
let exhausted = false;
|
|
139
|
+
const atLimit = () => {
|
|
140
|
+
if (exhausted)
|
|
141
|
+
return true;
|
|
142
|
+
if (entries >= limits.maxEntries || result.files.size >= limits.maxFiles ||
|
|
143
|
+
directories >= limits.maxDirectories || Date.now() - started >= limits.maxDurationMs) {
|
|
144
|
+
exhausted = true;
|
|
145
|
+
warn("Shell artifact scan reached its entry/file/directory/time limit; some changes may be untracked. Narrow the workspace or use explicit file tools.");
|
|
146
|
+
}
|
|
147
|
+
return exhausted;
|
|
148
|
+
};
|
|
149
|
+
const visit = async (abs, root, depth) => {
|
|
150
|
+
if (result.directories.has(abs) || atLimit())
|
|
151
|
+
return;
|
|
152
|
+
if (depth > limits.maxDepth) {
|
|
153
|
+
warn("Shell artifact scan reached its directory depth limit; deeper files may be untracked.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const names = new Set();
|
|
157
|
+
const children = [];
|
|
158
|
+
try {
|
|
159
|
+
// opendir bounds memory even when a directory contains many entries.
|
|
160
|
+
const dir = await fs.opendir(abs);
|
|
161
|
+
directories++;
|
|
162
|
+
let complete = true;
|
|
163
|
+
for await (const entry of dir) {
|
|
164
|
+
if (atLimit()) {
|
|
165
|
+
complete = false;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
entries++;
|
|
169
|
+
names.add(entry.name);
|
|
170
|
+
const child = path.join(abs, entry.name);
|
|
171
|
+
if (entry.isSymbolicLink())
|
|
172
|
+
continue;
|
|
173
|
+
const rel = path.relative(root.abs, child);
|
|
174
|
+
if (root.session && isIntermediateRel(rel))
|
|
175
|
+
continue;
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
if (!SKIP_DIRECTORIES.has(entry.name.toLowerCase()))
|
|
178
|
+
children.push(child);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (!entry.isFile() || SKIP_FILES.test(entry.name))
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
const stat = await fs.lstat(child, { bigint: true });
|
|
185
|
+
if (!stat.isFile())
|
|
186
|
+
continue;
|
|
187
|
+
const size = Number(stat.size);
|
|
188
|
+
let data;
|
|
189
|
+
if (size <= CONTENT_LIMIT) {
|
|
190
|
+
if (contentBytes + size <= limits.maxTotalContentBytes) {
|
|
191
|
+
contentBytes += size;
|
|
192
|
+
data = await fs.readFile(child).catch(() => undefined);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
warn("Shell artifact snapshots reached their content budget; additional files retain metadata but may have no text diff.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
result.files.set(child, {
|
|
199
|
+
exists: true,
|
|
200
|
+
mtimeNs: stat.mtimeNs,
|
|
201
|
+
size,
|
|
202
|
+
content: data && isTextDiffable(child) && !data.includes(0) ? data.toString("utf8") : null,
|
|
203
|
+
digest: data ? createHash("sha256").update(data).digest("hex") : undefined,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code ?? "")) {
|
|
208
|
+
warn("Shell artifact scan could not read some files; their changes may be untracked.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (complete)
|
|
213
|
+
result.directories.set(abs, names);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
const code = error.code;
|
|
217
|
+
if (depth === 0 && (code === "ENOENT" || code === "ENOTDIR"))
|
|
218
|
+
result.missingRoots.add(abs);
|
|
219
|
+
else if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
220
|
+
warn("Shell artifact scan could not read some directories; their changes may be untracked.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const child of children) {
|
|
224
|
+
if (atLimit())
|
|
225
|
+
break;
|
|
226
|
+
await visit(child, root, depth + 1);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
// User-facing session documents get priority over a potentially large repo.
|
|
230
|
+
for (const root of [...roots].sort((a, b) => Number(b.session) - Number(a.session))) {
|
|
231
|
+
if (atLimit())
|
|
232
|
+
break;
|
|
233
|
+
try {
|
|
234
|
+
const stat = await fs.lstat(root.abs);
|
|
235
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
240
|
+
result.missingRoots.add(root.abs);
|
|
241
|
+
else
|
|
242
|
+
warn("Shell artifact scan could not inspect one of its roots; some changes may be untracked.");
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
await visit(root.abs, root, 0);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
};
|
|
249
|
+
const pending = [];
|
|
250
|
+
let queue = Promise.resolve();
|
|
251
|
+
const serialized = (work) => {
|
|
252
|
+
const next = queue.then(work);
|
|
253
|
+
queue = next.catch((error) => { warn(`Artifact tool tracking failed: ${error instanceof Error ? error.message : String(error)}`); });
|
|
254
|
+
return queue;
|
|
255
|
+
};
|
|
256
|
+
const remember = (abs, stamp) => {
|
|
257
|
+
if (preWrite.has(abs))
|
|
258
|
+
return;
|
|
259
|
+
// When tools overlap, an earlier shell may already have written this path
|
|
260
|
+
// before a later explicit tool captures it. Preserve the earliest observation.
|
|
261
|
+
for (const tool of pending) {
|
|
262
|
+
const earlier = tool.before && snapshotStamp(tool.before, abs);
|
|
263
|
+
if (earlier) {
|
|
264
|
+
preWrite.set(abs, earlier);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
preWrite.set(abs, stamp);
|
|
269
|
+
};
|
|
270
|
+
const register = async (abs, before) => {
|
|
271
|
+
remember(abs, before);
|
|
272
|
+
await registerArtifactPath(sessionId, abs, workspaceDir, cwd, "model", preWrite.get(abs));
|
|
273
|
+
};
|
|
274
|
+
const complete = async (tool, shellAfter) => {
|
|
275
|
+
if (tool.before) {
|
|
276
|
+
const after = shellAfter ?? await scan();
|
|
277
|
+
const paths = new Set([...tool.before.files.keys(), ...after.files.keys()]);
|
|
278
|
+
for (const abs of paths) {
|
|
279
|
+
const beforeStamp = snapshotStamp(tool.before, abs);
|
|
280
|
+
const afterStamp = snapshotStamp(after, abs);
|
|
281
|
+
// A partial scan cannot establish absence. Never turn its omissions into
|
|
282
|
+
// fabricated additions or deletions.
|
|
283
|
+
if (beforeStamp && afterStamp && !sameStamp(beforeStamp, afterStamp))
|
|
284
|
+
await register(abs, beforeStamp);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const current = new Map();
|
|
289
|
+
for (const abs of tool.paths) {
|
|
290
|
+
await capturePreWriteSnapshot(abs, current);
|
|
291
|
+
const before = tool.explicitBefore?.get(abs);
|
|
292
|
+
const after = current.get(abs);
|
|
293
|
+
if (before && after && !sameStamp(before, after))
|
|
294
|
+
await register(abs, before);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
before(toolName, input, toolUseId) {
|
|
300
|
+
return serialized(async () => {
|
|
301
|
+
const name = toolName.toLowerCase();
|
|
302
|
+
if (!FILE_TOOLS.has(name) && !SHELL_TOOLS.has(name))
|
|
303
|
+
return;
|
|
304
|
+
if (toolUseId && pending.some((tool) => tool.id === toolUseId))
|
|
305
|
+
return;
|
|
306
|
+
const tool = { id: toolUseId, name, fingerprint: fingerprint(input), paths: [] };
|
|
307
|
+
if (SHELL_TOOLS.has(name)) {
|
|
308
|
+
tool.before = await scan();
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
tool.paths = [...new Set(extractPaths(input).map((raw) => normalizeArtifactAbsPath(raw, workspaceDir, cwd)).filter((abs) => !!abs))];
|
|
312
|
+
tool.explicitBefore = new Map();
|
|
313
|
+
for (const abs of tool.paths) {
|
|
314
|
+
await capturePreWriteSnapshot(abs, tool.explicitBefore);
|
|
315
|
+
const stamp = tool.explicitBefore.get(abs);
|
|
316
|
+
if (stamp)
|
|
317
|
+
remember(abs, stamp);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
pending.push(tool);
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
after(toolName, input, toolUseId) {
|
|
324
|
+
return serialized(async () => {
|
|
325
|
+
const name = toolName.toLowerCase();
|
|
326
|
+
const index = pending.findIndex((tool) => toolUseId ? tool.id === toolUseId : tool.name === name && tool.fingerprint === fingerprint(input));
|
|
327
|
+
if (index === -1)
|
|
328
|
+
return;
|
|
329
|
+
const tool = pending[index];
|
|
330
|
+
// Keep this snapshot available to remember() until its changed paths have
|
|
331
|
+
// been registered. This also handles missing-file stamps for new outputs.
|
|
332
|
+
await complete(tool);
|
|
333
|
+
pending.splice(index, 1);
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
/** Called at turn end/abort, including partial writes from failed tools. */
|
|
337
|
+
flush() {
|
|
338
|
+
return serialized(async () => {
|
|
339
|
+
const after = pending.some((tool) => tool.before) ? await scan() : undefined;
|
|
340
|
+
while (pending.length) {
|
|
341
|
+
await complete(pending[0], after);
|
|
342
|
+
pending.shift();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-kit · 产物(Artifact)管理
|
|
3
|
+
*
|
|
4
|
+
* 问题背景:模型用 Write 工具生成的 HTML / Markdown / 图片等「供用户查看的文件」,
|
|
5
|
+
* 原来会落到任意磁盘位置(如 apps/web/),且模型常在回复里复述真实路径,
|
|
6
|
+
* 既泄露服务器目录结构,前端也无法直接渲染。
|
|
7
|
+
*
|
|
8
|
+
* 本模块提供:
|
|
9
|
+
* - 会话隔离目录:<产物根>/.agent/<sessionId>/(选了项目 → 项目根/.agent/;无项目 → ~/.agent/),
|
|
10
|
+
* 每个会话只能看到自己的产出;
|
|
11
|
+
* - 模型只接触虚拟路径 /agent-output/<sessionId>/...,真实目录永不进入模型上下文;
|
|
12
|
+
* - 产物登记表(registry):模型按用户指示写到「任意位置」(如用户指定目录)的文件,
|
|
13
|
+
* 在执行层登记真实路径后即成为本会话产物——原位置即产物,不复制、不搬移进 .agent;
|
|
14
|
+
* 用户未指定位置时仍默认落 .agent/<sessionId>(见 buildArtifactPrompt);
|
|
15
|
+
* - 基线/增量扫描:一轮对话后找出新文件,作为产物推给前端;
|
|
16
|
+
* - 安全读取:readArtifact 严格限制在 session 目录内与已登记的产物路径上,任何穿越尝试返回 null;
|
|
17
|
+
* - 脱敏:redactOutputPaths 抹掉模型可能复述的 /agent-output 标记。
|
|
18
|
+
*/
|
|
19
|
+
/** 真实磁盘产物目录名(隐藏目录:选了项目在项目根、无项目在 ~ 下),靠 .gitignore 排除 */
|
|
20
|
+
export declare const ARTIFACT_DIR_NAME = ".agent";
|
|
21
|
+
/** 模型上下文里的虚拟输出根:模型写文件用 /agent-output/<sessionId>/<name>(保持稳定,不改) */
|
|
22
|
+
export declare const ARTIFACT_VIRTUAL_ROOT = "/agent-output";
|
|
23
|
+
/** 产物根目录(真实磁盘位置,宿主侧):无工作区时统一放用户目录 ~/.agent */
|
|
24
|
+
export declare function artifactRoot(): string;
|
|
25
|
+
/**
|
|
26
|
+
* CodeBuddy CLI 沙箱项目根:<homedir>/.codebuddy/projects/<cwd转写>/。
|
|
27
|
+
* 模型用 Bash/Python 等工具写文件时,默认落在沙箱会话目录 <project>/<sessionId>/(而非宿主 .agent),
|
|
28
|
+
* 因此产物收集与读取必须同时覆盖两个位置。转写规则:X:\a\b → x-a-b。
|
|
29
|
+
* cwdOverride:工作区模式下传工作目录(CLI cwd 已是工作目录,沙箱根随 cwd 变化);
|
|
30
|
+
* 目录不存在时返回 null(非 CodeBuddy CLI 环境不启用)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function sandboxProjectRoot(cwdOverride?: string): string | null;
|
|
33
|
+
/** 某会话的候选产物目录:产物根/.agent/<sid> + 沙箱 <project>/<sid>(存在时),产物根优先 */
|
|
34
|
+
export declare function sessionDirCandidates(sessionId: string, workspaceDir?: string): string[];
|
|
35
|
+
/**
|
|
36
|
+
* 取会话「基 id」:编辑重发/重新生成时,前端会把发给服务端的 SDK transcript id
|
|
37
|
+
* 换成 `${sid}_${uuid}`(让 CLI 不加载旧 transcript,见 ChatWindow 的 rt.sdkSid),
|
|
38
|
+
* 产物目录因此跟着换成一个「分叉目录」。但产物**归属仍是原会话**——前端卡片与
|
|
39
|
+
* 预览请求一律带基 sid(activeSid)。会话 id 是 UUID 本身不含 '_',故 '_' 前段即基 id。
|
|
40
|
+
*/
|
|
41
|
+
export declare function baseSessionId(sessionId: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* 同源分叉目录列表:某基会话下所有 `<base>_<uuid>` 目录(宿主根 + 沙箱根)。
|
|
44
|
+
*
|
|
45
|
+
* 为什么需要:模型写文件时用的是当轮真实的 SDK transcript id(编辑重发 = 新 transcript),
|
|
46
|
+
* 于是文件落在分叉目录里;而读取/列举一律按基会话走。不把分叉目录并进来就会出现
|
|
47
|
+
* 「卡片在、点开 404」的错觉(文件明明在磁盘上)。按基 id 前缀扫一层目录即可覆盖,
|
|
48
|
+
* 根目录条目数量级为会话数,可接受。
|
|
49
|
+
*/
|
|
50
|
+
export declare function forkSessionDirs(sessionId: string, workspaceDir?: string): Promise<string[]>;
|
|
51
|
+
/** 读取用:基目录候选 + 全部分叉目录(去重,保持「宿主根优先」的既有次序)。 */
|
|
52
|
+
export declare function readDirCandidates(sessionId: string, workspaceDir?: string): Promise<string[]>;
|
|
53
|
+
/** 只允许会话 ID 使用安全字符,杜绝路径穿越 */
|
|
54
|
+
export declare function sanitizeSessionId(s: string): string;
|
|
55
|
+
/** 清洗文件名:去除前导分隔符、去除 . 与 .. 段,防穿越;过长截断 */
|
|
56
|
+
export declare function sanitizeArtifactName(name: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* 中间产物目录名(相对会话目录的**第一段**命中即视为中间产物,不向用户展示):
|
|
59
|
+
* - _drafts / _intermediate / _tmp:模型写草稿 / 临时 / 半成品文件的约定目录(见 buildArtifactPrompt);
|
|
60
|
+
* - tool-results:CodeBuddy CLI 沙箱会话目录里的内部工具执行记录(chatcmpl-tool-*-render.txt、
|
|
61
|
+
* mcp-web-fetch-*.txt 等),不是用户可见产物;
|
|
62
|
+
* - subagents:CLI 子代理(subagent)的 transcript / 结果目录(位于会话目录内 subagents/<id>/),
|
|
63
|
+
* 同样属于 SDK 内部文件,不是用户交互产物;
|
|
64
|
+
* - attachments:**用户上传附件的 staging 目录**(服务端把用户文件复制到 CLI cwd 的
|
|
65
|
+
* attachments/<rel>,见 handlers.ts stageVirtualCopy)。用户的文件永远不是模型生成的产物,
|
|
66
|
+
* 收集/列表必须整体跳过——否则用户截图会出现在「产物」面板里。
|
|
67
|
+
* 只有会话目录根下(及非保留子目录)的文件才是「最终产物」。
|
|
68
|
+
* 点开头(隐藏文件/目录)同样视为中间产物,避免 .DS_Store/.gitkeep 之类进入产物列表。
|
|
69
|
+
*/
|
|
70
|
+
export declare const INTERMEDIATE_DIR_NAMES: ReadonlySet<string>;
|
|
71
|
+
/** 判断一个相对路径是否属于中间产物:第一段命中保留目录名 / 以点开头;
|
|
72
|
+
* 或**任意路径段**命中 CodeBuddy CLI 内部目录(tool-results / subagents / .codebuddy)——
|
|
73
|
+
* subagents/<id>/tool-results/xxx 这类 SDK 内部文件即使不在第一段也必须过滤。 */
|
|
74
|
+
export declare function isIntermediateRel(rel: string): boolean;
|
|
75
|
+
/** CLI 内部绝对路径判定(registry 登记条目用):路径任意段命中 tool-results/subagents/.codebuddy
|
|
76
|
+
* → CodeBuddy SDK 内部文件,不是用户交互产物,不展示。 */
|
|
77
|
+
export declare function isCliInternalAbsPath(abs: string): boolean;
|
|
78
|
+
/** 登记条目:absPath 是唯一权威路径,lastSeen/戳 用于 added/modified/deleted 判定 */
|
|
79
|
+
export interface RegistryEntry {
|
|
80
|
+
/** 稳定唯一 id:会话目录内 = 相对路径(与扫描产物一致);工作目录根 = 文件名(兼容历史);
|
|
81
|
+
* 其他任意路径(用户指定目录)= ext-<hash8>-<basename> */
|
|
82
|
+
id: string;
|
|
83
|
+
/** 展示名(文件 basename) */
|
|
84
|
+
name: string;
|
|
85
|
+
/** 真实绝对路径(原位置,不复制) */
|
|
86
|
+
absPath: string;
|
|
87
|
+
type: ArtifactType;
|
|
88
|
+
/** 上次收集时的 mtimeNs(bigint 转字符串);从未成功收集则无 */
|
|
89
|
+
mtimeNs?: string;
|
|
90
|
+
/** 上次收集时的文件字节数 */
|
|
91
|
+
size?: number;
|
|
92
|
+
/** 登记来源:model=模型工具写的真产物;scan=列表兜底扫描顺带的工程文件(需再判定) */
|
|
93
|
+
origin?: RegistryOrigin;
|
|
94
|
+
/** 登记时刻 ms(scan 条目用它:登记之后没被改写过 = 本来就存在的工程文件 → 不展示) */
|
|
95
|
+
registeredAt?: number;
|
|
96
|
+
/** 上次收集时文件是否存在于磁盘(false 且仍不存在 → 登记但写入失败,不报 deleted) */
|
|
97
|
+
lastSeen: boolean;
|
|
98
|
+
/** 首次写入前不存在才是本会话产出;编辑已有文件只进入变更列表。 */
|
|
99
|
+
createdBySession?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** 读取会话登记表(损坏/不存在 → 空数组)。
|
|
102
|
+
* 含编辑重发/重新生成产生的分叉登记表(`<base>_<uuid>`):模型那一轮写的是分叉
|
|
103
|
+
* transcript id,登记项落在分叉登记表里,而读取一律按基会话走 → 必须合并,
|
|
104
|
+
* 否则「模型写到用户指定目录」的文件会出现同样的卡片 404。 */
|
|
105
|
+
export declare function loadRegistry(sessionId: string, workspaceDir?: string): Promise<RegistryEntry[]>;
|
|
106
|
+
/** 原子替换登记表,读取列表不会看到半截 JSON。 */
|
|
107
|
+
export declare function saveRegistry(sessionId: string, entries: RegistryEntry[], workspaceDir?: string): Promise<void>;
|
|
108
|
+
/** 把模型工具入参里的路径归一化为真实绝对路径:
|
|
109
|
+
* - /agent-output/<sid>/x → <产物根>/<sid>/x(与 pathAliases 同一映射,登记层拿原始入参时自行解析)
|
|
110
|
+
* - bash 风格 /d/a/b → D:\a\b(Windows)
|
|
111
|
+
* - 相对路径 → 相对执行 cwd 解析
|
|
112
|
+
* 无法解析返回 null。 */
|
|
113
|
+
export declare function normalizeArtifactAbsPath(rawPath: string, workspaceDir?: string, cwd?: string): string | null;
|
|
114
|
+
/**
|
|
115
|
+
* 登记一个产物路径(幂等:absPath 已登记则跳过)。id 规则:
|
|
116
|
+
* - 在会话目录候选内 → 相对会话目录路径(与扫描产物 id 一致,天然去重);
|
|
117
|
+
* - 工作目录根散落文件 → 文件名(与历史 collectWorkFiles id 一致,旧卡片可继续预览);
|
|
118
|
+
* - 其他任意路径(用户指定目录)→ ext-<hash8>-<basename>。
|
|
119
|
+
*/
|
|
120
|
+
export declare function registerArtifactPath(sessionId: string, rawPath: string, workspaceDir?: string, cwd?: string, origin?: RegistryOrigin, before?: ArtifactStamp): Promise<void>;
|
|
121
|
+
export type ArtifactType = "html" | "markdown" | "image" | "pdf" | "text" | "word" | "other";
|
|
122
|
+
export declare function artifactType(name: string): ArtifactType;
|
|
123
|
+
export declare function mimeFor(name: string): string;
|
|
124
|
+
export interface ArtifactMeta {
|
|
125
|
+
id: string;
|
|
126
|
+
name: string;
|
|
127
|
+
type: ArtifactType;
|
|
128
|
+
/** 文件字节数(前端展示大小) */
|
|
129
|
+
size?: number;
|
|
130
|
+
/** 文件所在真实目录(「打开文件夹」跟随文件用):会话目录内 = 会话目录;
|
|
131
|
+
* 登记产物(模型写到任意位置) = 该文件自己的父目录。前端桌面壳据此 shell.openPath。 */
|
|
132
|
+
dir?: string;
|
|
133
|
+
}
|
|
134
|
+
/** 确保会话目录存在并返回其路径(工作区模式下为 <工作区>/.agent/<sid>,无工作区 ~/.agent/<sid>) */
|
|
135
|
+
export declare function ensureSessionDir(sessionId: string, workspaceDir?: string): Promise<string>;
|
|
136
|
+
/** 文件是否参与文本 diff(扩展名在白名单内) */
|
|
137
|
+
export declare function isTextDiffable(name: string): boolean;
|
|
138
|
+
/** 登记条目的来源:model=模型工具写的(真产物);scan=列表兜底扫描顺带的工程文件(待判定) */
|
|
139
|
+
export type RegistryOrigin = "model" | "scan";
|
|
140
|
+
/** 产物文件戳:记录修改时间(纳秒 BigInt)与大小,用于识别「新增」与「内容更新」(同名覆盖)。
|
|
141
|
+
* content:文本文件(isTextDiffable 且 ≤ 上限)在本轮开始前的内容快照,供本轮 diff 展示旧内容;
|
|
142
|
+
* 二进制/超大文件为 null(只标记变更类型,不参与内容对比)。
|
|
143
|
+
* 注意:bigint 模式下 fs.stat 的 mtimeNs 是 bigint、size 是 bigint,统一在存取处转 Number(size)。 */
|
|
144
|
+
export interface ArtifactStamp {
|
|
145
|
+
mtimeNs: bigint;
|
|
146
|
+
size: number;
|
|
147
|
+
content?: string | null;
|
|
148
|
+
/** false 是已确认的不存在状态,必须保留,不能让后续编辑覆盖为已有文件。 */
|
|
149
|
+
exists?: boolean;
|
|
150
|
+
digest?: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 基线:记录某会话「开始前」已存在的产物(宿主 + 沙箱两个位置的会话目录,以及产物根下散落文件),
|
|
154
|
+
* 返回 key("s:"/"t:" + 文件名)→ 文件戳,用于增量与更新识别。
|
|
155
|
+
*/
|
|
156
|
+
export declare function baselineArtifacts(sessionId: string, workspaceDir?: string): Promise<Map<string, ArtifactStamp>>;
|
|
157
|
+
/** 产物变更:本轮对会话目录内文件的新增/修改/删除,带新旧内容(文本文件)供前端 diff 展示。 */
|
|
158
|
+
export interface ArtifactChange extends ArtifactMeta {
|
|
159
|
+
/** 本轮变更类型:added=新增、modified=内容更新、deleted=被删除 */
|
|
160
|
+
change: "added" | "modified" | "deleted";
|
|
161
|
+
/** 变更前内容(文本基线快照;deleted 时为被删文件的最后内容;二进制/超限为 null) */
|
|
162
|
+
oldContent?: string | null;
|
|
163
|
+
/** 变更后内容(added/modified 的当前内容;deleted 无此字段) */
|
|
164
|
+
newContent?: string | null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 写工具执行前采集:目标文件在本轮编辑前是否已存在 + 文本内容快照(供 diff 旧内容)。
|
|
168
|
+
* 写入 into(键 = 归一化绝对路径)。同一文件本轮多次编辑只保留首次快照(= 本轮开始前状态)。
|
|
169
|
+
*
|
|
170
|
+
* 用途:模型用 Edit/Write 改「已有文件」(如工作区里的工程源码)时,登记表(registry)新建
|
|
171
|
+
* 条目的 lastSeen 恒为 false,光靠 collectArtifacts 无法区分「新建文件」与「编辑已有文件」,
|
|
172
|
+
* 会把编辑误判为 added(→ 只进「产物」不进「变更」,且缺旧内容无法 diff)。执行前若文件已存在,
|
|
173
|
+
* 不存在也保留快照:本轮先 Write 新建再 Edit 时,仍按新建文件归类。
|
|
174
|
+
*/
|
|
175
|
+
export declare function capturePreWriteSnapshot(absPath: string, into: Map<string, ArtifactStamp>): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* 一轮对话结束后收集本轮「变更」(新增、内容更新、删除):
|
|
178
|
+
* - 会话子目录(宿主 .agent 与 CodeBuddy 沙箱两处)里的新文件/更新文件记为变更(added/modified),
|
|
179
|
+
* 递归扫描子目录(模型常把产物写进 brand-icons/ 等子目录,此前只扫顶层导致卡片缺失);
|
|
180
|
+
* - 中间产物目录(_drafts/、_intermediate/、_tmp/ 及点开头隐藏项)一律不收集,
|
|
181
|
+
* 只有会话目录根下(及非保留子目录)的文件才作为「最终产物」呈现给用户;
|
|
182
|
+
* - 宿主根目录下「散落」的新**文件**(模型忘记带 sessionId 子目录)移入会话子目录,保证隔离;
|
|
183
|
+
* **根目录下的目录不做任何搬移/扫描**——它可能是其他会话自己的目录,搬入会造成跨会话产物混入;
|
|
184
|
+
* - 同名文件被重新写入(覆盖/更新,mtime 或大小变化)记为 modified;
|
|
185
|
+
* - 删除检测:会话子目录内基线存在、本轮结束后磁盘已无 → deleted(根目录散落文件不判,
|
|
186
|
+
* 它们可能被搬移进会话目录或属其他会话,误判风险高);
|
|
187
|
+
* - 文本文件(isTextDiffable 且 ≤ 上限)携带 oldContent/newContent 供前端左右 diff;
|
|
188
|
+
* 二进制/超大文件只标记变更类型。
|
|
189
|
+
* 返回变更清单(新增/修改在前、删除在后,按 id 去重)。id 为相对会话目录的 posix 路径。
|
|
190
|
+
*/
|
|
191
|
+
export declare function collectArtifacts(sessionId: string, baseline: Map<string, ArtifactStamp>, workspaceDir?: string,
|
|
192
|
+
/** 写工具执行前的目标文件快照(键 = 归一化绝对路径):区分「编辑已有文件」与「新建文件」并提供 diff 旧内容 */
|
|
193
|
+
preWrite?: Map<string, ArtifactStamp>): Promise<ArtifactChange[]>;
|
|
194
|
+
/**
|
|
195
|
+
* 列出某会话的全部产物(递归,含非保留子目录),用于「查看所有产物 (N)」按钮与右侧列表面板。
|
|
196
|
+
* 与 collectArtifacts 不同:不做基线增量,直接返回磁盘上当前存在的全部文件;
|
|
197
|
+
* 中间产物目录(_drafts/ 等)与隐藏文件不进入列表,只呈现最终产物。
|
|
198
|
+
* 会话目录不存在时返回空数组。
|
|
199
|
+
*/
|
|
200
|
+
export declare function listSessionArtifacts(sessionId: string, workspaceDir?: string): Promise<ArtifactMeta[]>;
|
|
201
|
+
/** 安全读取产物:在产物根与沙箱候选会话目录内查找(产物根优先),任何穿越尝试返回 null;
|
|
202
|
+
* 会话目录未命中时,回查登记表(registry):模型写到任意位置的文件按登记 id/name 读原路径,
|
|
203
|
+
* 仅限已登记的真实路径(任意 id 无法读取未登记文件)。 */
|
|
204
|
+
export declare function readArtifact(sessionId: string, id: string, workspaceDir?: string): Promise<{
|
|
205
|
+
buf: Buffer;
|
|
206
|
+
mime: string;
|
|
207
|
+
} | null>;
|
|
208
|
+
/** 构造前端拉取产物的隔离 URL(session 与 file 均经编码,服务端二次校验) */
|
|
209
|
+
export declare function artifactUrl(basePath: string, sessionId: string, id: string): string;
|
|
210
|
+
/**
|
|
211
|
+
* 脱敏(第一道):把「真实产物根目录」替换成虚拟标记 /agent-output。
|
|
212
|
+
* 同时处理 Windows 盘符路径(D:\...\agent-output)与 CLI 使用的 bash 风格路径(/d/.../agent-output),
|
|
213
|
+
* 避免任一真实路径残留在模型输出中。
|
|
214
|
+
*/
|
|
215
|
+
export declare function redactArtifactPaths(text: string): string;
|
|
216
|
+
/**
|
|
217
|
+
* 脱敏:抹掉模型可能复述的虚拟输出路径标记(/agent-output...),
|
|
218
|
+
* 避免内部约定外泄。配合系统提示(不要提路径)作为安全网。
|
|
219
|
+
*/
|
|
220
|
+
export declare function redactOutputPaths(text: string): string;
|
|
221
|
+
/**
|
|
222
|
+
* 清洗助手文本里的限流噪音(服务端源头,防止 429 错误进入回复正文/存库):
|
|
223
|
+
* - CLI transcript 会给消息加 trace 前缀((32hex/uuid)),剥离前缀、保留正文;
|
|
224
|
+
* - 429 频率限制错误整段(可能带 trace 前缀/后缀)直接丢弃;
|
|
225
|
+
* - 内容若清洗后为空(纯错误轮次),返回空串,前端不展示空气泡。
|
|
226
|
+
*/
|
|
227
|
+
export declare function sanitizeAssistantText(text: string): string;
|