@sema-agent/core 2.1.0 → 2.2.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/dist/agents/observer.d.ts +14 -0
- package/dist/agents/observer.js +58 -9
- package/dist/agents/send-message-tool.js +55 -10
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +69 -15
- package/dist/core/context-edit.js +16 -3
- package/dist/core/file-snapshot-store.js +10 -1
- package/dist/core/runner/prepare-task.d.ts +1 -0
- package/dist/core/runner/prepare-task.js +29 -5
- package/dist/core/runner/runtask.js +9 -0
- package/dist/core/runner/synthetic-tools.d.ts +1 -0
- package/dist/core/runner/synthetic-tools.js +18 -15
- package/dist/core/runner/turn-attachments.d.ts +12 -2
- package/dist/core/runner/turn-attachments.js +33 -3
- package/dist/core/task-registry-agent.d.ts +9 -1
- package/dist/core/task-registry-agent.js +23 -2
- package/dist/core/task-registry-monitor.js +79 -24
- package/dist/core/task-registry-shared.d.ts +13 -1
- package/dist/core/task-registry-shared.js +21 -0
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/task-registry.js +24 -26
- package/dist/core/types.d.ts +3 -1
- package/dist/engine/session/memory-repo.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
- package/dist/orchestration/workflow-size-guideline.js +19 -9
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +18 -2
- package/dist/prompt-assembly/assemble.js +3 -7
- package/dist/prompt-assembly/packs/sema-default.js +8 -5
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/coordinator.js +45 -0
- package/dist/prompts/default.d.ts +4 -5
- package/dist/prompts/default.js +16 -18
- package/dist/prompts/simple-sections.d.ts +3 -1
- package/dist/prompts/simple-sections.js +11 -1
- package/dist/stores/file/workflow-journal-store.d.ts +7 -1
- package/dist/stores/file/workflow-journal-store.js +70 -33
- package/dist/tools/fs/bash-readonly-classifier.js +20 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -0
- package/dist/tools/fs/fs-bash.js +10 -3
- package/dist/tools/fs/fs-read.js +10 -10
- package/dist/tools/fs/fs-search-tools.js +42 -7
- package/dist/tools/fs/fs-write.js +18 -6
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +2 -1
- package/dist/tools/fs/safety.d.ts +4 -0
- package/dist/tools/fs/safety.js +102 -6
- package/dist/tools/fs/search.d.ts +1 -0
- package/dist/tools/fs/search.js +23 -3
- package/dist/tools/monitor.js +1 -1
- package/package.json +3 -2
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
3
|
import { resolveKey, violationText } from "./safety.js";
|
|
4
|
-
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern } from "./search.js";
|
|
4
|
+
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
|
|
5
5
|
export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
6
6
|
return defineTool({
|
|
7
7
|
name: "Grep",
|
|
8
8
|
contract: { contractId: "core.grep@1", implementationRevision: "1" },
|
|
9
9
|
description: "Content search built on ripgrep. Prefer this over `grep`/`rg` via Bash — results integrate with the permission UI and file links.\n" +
|
|
10
|
+
"\n" +
|
|
10
11
|
'- Full regex syntax (e.g. "log.*Error", "function\\s+\\w+"). Ripgrep, not grep — escape literal braces (`interface\\{\\}`).\n' +
|
|
11
12
|
'- Filter with `glob` (e.g. "**/*.tsx") or `type` (e.g. "js", "py", "rust").\n' +
|
|
12
13
|
'- `output_mode`: "content" (matching lines), "files_with_matches" (paths only, default), or "count".\n' +
|
|
13
14
|
"- `multiline: true` for patterns that span lines.\n" +
|
|
14
15
|
"- Uses ripgrep when available (respects .gitignore, skips binary files and VCS directories), otherwise a JS fallback that skips node_modules/build/… and parses .gitignore. Hidden files/directories ARE searched.\n" +
|
|
15
|
-
"- Use Agent tool for open-ended searches requiring multiple rounds",
|
|
16
|
+
"- Use Agent tool (if available) for open-ended searches requiring multiple rounds",
|
|
16
17
|
parameters: Type.Object({
|
|
17
18
|
pattern: Type.String({ description: "The regular expression pattern to search for in file contents" }),
|
|
18
|
-
path: Type.Optional(Type.String({ description: "
|
|
19
|
+
path: Type.Optional(Type.String({ description: "File or directory to search in (rg PATH). Relative paths resolve against the root; defaults to the whole root." })),
|
|
19
20
|
glob: Type.Optional(Type.String({ description: 'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")' })),
|
|
20
21
|
type: Type.Optional(Type.String({ description: 'File type to search (rg --type): "js", "py", "rust", "go", "java", etc. More efficient than glob for standard file types.' })),
|
|
21
22
|
output_mode: Type.Optional(Type.Union([Type.Literal("content"), Type.Literal("files_with_matches"), Type.Literal("count")], {
|
|
@@ -41,6 +42,23 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
41
42
|
effect: "read",
|
|
42
43
|
execute: async (args, ctx) => {
|
|
43
44
|
const a = args;
|
|
45
|
+
for (const [name, value] of [
|
|
46
|
+
["head_limit", a.head_limit],
|
|
47
|
+
["offset", a.offset],
|
|
48
|
+
]) {
|
|
49
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
|
50
|
+
return errorResult(`Error (Grep): ${name} must be a whole number of 0 or more, got ${value}.${name === "head_limit" ? " Pass 0 for unlimited." : ""}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (typeof a.glob === "string" && a.glob.length > 0) {
|
|
54
|
+
const badGlobs = invalidGlobTokens(a.glob);
|
|
55
|
+
if (badGlobs.length > 0) {
|
|
56
|
+
return errorResult(`Error (Grep): rejected the glob ${badGlobs.map((g) => JSON.stringify(g)).join(", ")} without searching — ` +
|
|
57
|
+
`an unterminated \`[\` class or \`{\` group cannot be compiled as a glob, and matching it literally would ` +
|
|
58
|
+
`silently narrow the search to a file with that exact name (an empty result would NOT mean "no matches"). ` +
|
|
59
|
+
`Close the bracket/brace, or drop \`glob\` and filter with \`path\`/\`type\` instead.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
44
62
|
let scoped = a.path;
|
|
45
63
|
if (a.path !== undefined) {
|
|
46
64
|
const r = await resolveKey(env, rootCanonical, a.path, ctx.signal, rootCanonical, additionalRoots);
|
|
@@ -77,9 +95,14 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
77
95
|
const m = /^(.*?):(\d+):/.exec(l);
|
|
78
96
|
return m ? m[1] : l;
|
|
79
97
|
};
|
|
98
|
+
const capMarker = /^…\[capped at (\d+) of (\d+)\]$/m.exec(text);
|
|
99
|
+
const cappedAt = capMarker ? Number(capMarker[1]) : undefined;
|
|
100
|
+
const capTotal = capMarker ? Number(capMarker[2]) : undefined;
|
|
101
|
+
const appliedOffset = typeof a.offset === "number" && a.offset > 0 ? { appliedOffset: a.offset } : {};
|
|
102
|
+
const appliedLimit = cappedAt !== undefined ? { appliedLimit: cappedAt } : {};
|
|
80
103
|
let detailFields;
|
|
81
104
|
if (mode === "files_with_matches") {
|
|
82
|
-
detailFields = { filenames: rows, numFiles: rows.length };
|
|
105
|
+
detailFields = { filenames: rows, numFiles: rows.length, totalFiles: capTotal ?? rows.length, ...appliedLimit, ...appliedOffset };
|
|
83
106
|
}
|
|
84
107
|
else if (mode === "count") {
|
|
85
108
|
const filenames = [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
@@ -92,7 +115,14 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
92
115
|
else
|
|
93
116
|
malformed = true;
|
|
94
117
|
}
|
|
95
|
-
detailFields = {
|
|
118
|
+
detailFields = {
|
|
119
|
+
filenames,
|
|
120
|
+
numFiles: filenames.length,
|
|
121
|
+
content: rows.join("\n"),
|
|
122
|
+
...(malformed ? {} : { numMatches }),
|
|
123
|
+
...appliedLimit,
|
|
124
|
+
...appliedOffset,
|
|
125
|
+
};
|
|
96
126
|
}
|
|
97
127
|
else {
|
|
98
128
|
const filenames = [...new Set(rows.map(contentPathOf))];
|
|
@@ -103,6 +133,9 @@ export function createGrepTool(env, rootCanonical, additionalRoots) {
|
|
|
103
133
|
numFiles: filenames.length,
|
|
104
134
|
content: joined.length > GREP_CONTENT_PREVIEW_CHARS ? `${joined.slice(0, GREP_CONTENT_PREVIEW_CHARS)}\n…[truncated — full text in the tool output]` : joined,
|
|
105
135
|
numLines: rows.length,
|
|
136
|
+
totalLines: capTotal ?? rows.length,
|
|
137
|
+
...appliedLimit,
|
|
138
|
+
...appliedOffset,
|
|
106
139
|
};
|
|
107
140
|
}
|
|
108
141
|
return {
|
|
@@ -126,10 +159,12 @@ export function createGlobTool(env, rootCanonical, additionalRoots) {
|
|
|
126
159
|
"- On environments that do not report modification times, results fall back to alphabetical order\n" +
|
|
127
160
|
"- Paths are RELATIVE to the root; ignored trees (node_modules/build/.gitignore) are skipped unless your pattern names them explicitly (e.g. `dist/**`)\n" +
|
|
128
161
|
"- Use this tool when you need to find files by name patterns; use `path` to scope to a sub-directory\n" +
|
|
129
|
-
"- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead",
|
|
162
|
+
"- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead (if available)",
|
|
130
163
|
parameters: Type.Object({
|
|
131
164
|
pattern: Type.String({ description: "Glob pattern (`*`, `**`, `?`)." }),
|
|
132
|
-
path: Type.Optional(Type.String({
|
|
165
|
+
path: Type.Optional(Type.String({
|
|
166
|
+
description: 'Scope the search to this sub-directory (relative to root). IMPORTANT: Omit this field to search the whole root. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.',
|
|
167
|
+
})),
|
|
133
168
|
max_results: Type.Optional(Type.Number({ description: "Cap results (default 500)." })),
|
|
134
169
|
}),
|
|
135
170
|
effect: "read",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
4
|
-
import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, WRITE_ENCODING_DEADLOCK_ESCAPE_HINT, } from "./safety.js";
|
|
4
|
+
import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, WRITE_ENCODING_DEADLOCK_ESCAPE_HINT, } from "./safety.js";
|
|
5
5
|
import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
|
|
6
6
|
import { MAX_EDIT_BYTES, formatByteSize, decodeEditBytes, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
|
|
7
7
|
async function gateToolWrite(hook, tool, path, key, content) {
|
|
@@ -54,12 +54,12 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
54
54
|
const path = fileArgPath(args);
|
|
55
55
|
if (path === undefined)
|
|
56
56
|
return errorResult(`Error (Edit): file_path is required.`);
|
|
57
|
-
const ipynb = ipynbRedirect("Edit", path);
|
|
58
|
-
if (ipynb)
|
|
59
|
-
return errorResult(ipynb);
|
|
60
57
|
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
61
58
|
if (!r.ok)
|
|
62
59
|
return errorResult(violationText("Edit", r.violation));
|
|
60
|
+
if (!batch && a.old_string === a.new_string) {
|
|
61
|
+
return errorResult(violationText("Edit", { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." }));
|
|
62
|
+
}
|
|
63
63
|
const singleOld = batch ? undefined : a.old_string;
|
|
64
64
|
const exists = await env.exists(r.key, ctx.signal);
|
|
65
65
|
if (!exists.ok)
|
|
@@ -132,6 +132,9 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
132
132
|
details: { type: "edit", filePath: path, originalFile: preDec.text, oldString: "", newString: newContent, replaceAll: false, edits: [{ oldString: "", newString: newContent, replaceAll: false }] },
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
+
const ipynb = ipynbRedirect("Edit", path);
|
|
136
|
+
if (ipynb)
|
|
137
|
+
return errorResult(ipynb);
|
|
135
138
|
const notRead = requireRead(state, r.key);
|
|
136
139
|
if (notRead)
|
|
137
140
|
return errorResult(await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal));
|
|
@@ -170,9 +173,18 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
170
173
|
oldS = resolved;
|
|
171
174
|
}
|
|
172
175
|
}
|
|
176
|
+
if (oldS !== "" && !working.includes(oldS)) {
|
|
177
|
+
const resolvedEsc = resolveEscapeMatch(working, oldS);
|
|
178
|
+
if (resolvedEsc !== undefined && resolvedEsc !== oldS) {
|
|
179
|
+
newS = adaptNewStringEscapes(oldS, resolvedEsc, newS);
|
|
180
|
+
oldS = resolvedEsc;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
173
183
|
const match = checkEditMatch(working, oldS, e.replace_all === true, entry.truncated);
|
|
174
|
-
if (match)
|
|
175
|
-
|
|
184
|
+
if (match) {
|
|
185
|
+
const escNote = match.code === "ambiguous_edit" && escapeMatchWasAttempted(e.old_string) && !working.includes(oldS) ? ESCAPE_MATCH_MISS_NOTE : "";
|
|
186
|
+
return errorResult(batch ? `Error (Edit): ${where}${match.message}${escNote} (no changes written — the batch is atomic).` : `${violationText("Edit", match)}${escNote}`);
|
|
187
|
+
}
|
|
176
188
|
replacements += e.replace_all === true ? countOccurrences(working, oldS) : 1;
|
|
177
189
|
const effOld = e.replace_all === true ? oldS : deletionOldString(working, oldS, newS);
|
|
178
190
|
working = e.replace_all === true ? working.split(effOld).join(newS) : working.replace(effOld, () => newS);
|
package/dist/tools/fs/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface HandsToolkitOptions {
|
|
|
29
29
|
}) => void;
|
|
30
30
|
detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
|
|
31
31
|
execClamp?: ExecClampOption;
|
|
32
|
+
oneShot?: boolean;
|
|
32
33
|
autoBackgroundOnTimeout?: boolean;
|
|
33
34
|
readImageDownsampler?: ReadImageDownsamplerOption;
|
|
34
35
|
pdfModelCapabilities?: PdfModelCapabilities;
|
package/dist/tools/fs/index.js
CHANGED
|
@@ -45,11 +45,12 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
|
|
|
45
45
|
detachHub: opts.detachHub,
|
|
46
46
|
execClamp: opts.execClamp,
|
|
47
47
|
...(opts.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: opts.autoBackgroundOnTimeout } : {}),
|
|
48
|
+
...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}),
|
|
48
49
|
}));
|
|
49
50
|
if (!readOnly && mountBackgroundTaskTools && hasBackgroundShell(env)) {
|
|
50
51
|
const sessionAxis = opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {};
|
|
51
52
|
tools.push(opts.taskRegistry
|
|
52
|
-
? createTaskOutputTool({ registry: opts.taskRegistry, owner: opts.taskOwner, scope: opts.taskScope, ...sessionAxis })
|
|
53
|
+
? createTaskOutputTool({ registry: opts.taskRegistry, owner: opts.taskOwner, scope: opts.taskScope, ...sessionAxis, ...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}) })
|
|
53
54
|
: createEnvTaskOutputTool(env), opts.taskRegistry
|
|
54
55
|
? createTaskStopTool({ registry: opts.taskRegistry, owner: opts.taskOwner, scope: opts.taskScope, ...sessionAxis })
|
|
55
56
|
: createEnvTaskStopTool(env));
|
|
@@ -53,6 +53,10 @@ export declare function countOccurrences(haystack: string, needle: string): numb
|
|
|
53
53
|
export declare function similarNameSuggestion(siblingNames: readonly string[], missingName: string): string | undefined;
|
|
54
54
|
export declare function normalizeQuotes(s: string): string;
|
|
55
55
|
export declare function resolveQuoteMatch(content: string, oldString: string): string | undefined;
|
|
56
|
+
export declare function resolveEscapeMatch(content: string, oldString: string): string | undefined;
|
|
57
|
+
export declare function adaptNewStringEscapes(oldString: string, matchedOld: string, newString: string): string;
|
|
58
|
+
export declare function escapeMatchWasAttempted(oldString: string): boolean;
|
|
59
|
+
export declare const ESCAPE_MATCH_MISS_NOTE = "\n(note: Edit also tried swapping \\uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)";
|
|
56
60
|
export declare function adaptNewStringQuotes(matchedOld: string, newString: string): string;
|
|
57
61
|
export declare function deletionOldString(content: string, oldString: string, newString: string): string;
|
|
58
62
|
export declare function checkEditMatch(content: string, oldString: string, replaceAll: boolean, truncated: boolean): FsViolation | undefined;
|
package/dist/tools/fs/safety.js
CHANGED
|
@@ -41,7 +41,10 @@ const BLOCKED_DEVICE_PATHS = new Set([
|
|
|
41
41
|
function isProcStdioFd(key) {
|
|
42
42
|
return key.startsWith("/proc/") && (key.endsWith("/fd/0") || key.endsWith("/fd/1") || key.endsWith("/fd/2"));
|
|
43
43
|
}
|
|
44
|
-
const
|
|
44
|
+
const PROC_SENSITIVE_SUFFIXES = ["/environ", "/cmdline", "/auxv", "/maps", "/mem", "/stat"];
|
|
45
|
+
function isProcSensitiveFile(key) {
|
|
46
|
+
return key.startsWith("/proc/") && PROC_SENSITIVE_SUFFIXES.some((suf) => key.endsWith(suf));
|
|
47
|
+
}
|
|
45
48
|
const WIN_RESERVED_RE = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.[^\\/]*)?$/i;
|
|
46
49
|
function isWinFormPath(p) {
|
|
47
50
|
return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
|
|
@@ -54,7 +57,7 @@ function isWinReservedDeviceKey(key) {
|
|
|
54
57
|
return WIN_RESERVED_RE.test(base);
|
|
55
58
|
}
|
|
56
59
|
export function isBlockedDevicePath(key) {
|
|
57
|
-
return BLOCKED_DEVICE_PATHS.has(key) || isProcStdioFd(key) ||
|
|
60
|
+
return BLOCKED_DEVICE_PATHS.has(key) || isProcStdioFd(key) || isProcSensitiveFile(key) || isWinReservedDeviceKey(key);
|
|
58
61
|
}
|
|
59
62
|
export function normalizeAbsPathLexically(p) {
|
|
60
63
|
if (!p.startsWith("/"))
|
|
@@ -73,12 +76,16 @@ export function normalizeAbsPathLexically(p) {
|
|
|
73
76
|
}
|
|
74
77
|
const BINARY_EXTENSIONS = new Set([
|
|
75
78
|
".exe", ".dll", ".so", ".dylib", ".o", ".a", ".obj", ".lib", ".bin", ".class", ".pyc", ".pyo", ".wasm",
|
|
76
|
-
".
|
|
79
|
+
".app", ".msi", ".deb", ".rpm", ".node", ".rlib",
|
|
80
|
+
".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".jar", ".war", ".ear", ".lz4", ".zst", ".z",
|
|
77
81
|
".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".ico", ".heic", ".avif",
|
|
78
|
-
".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".
|
|
82
|
+
".mp3", ".wav", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".aiff", ".opus",
|
|
83
|
+
".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv", ".flv", ".m4v", ".mpeg", ".mpg",
|
|
79
84
|
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp",
|
|
80
85
|
".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
81
|
-
".
|
|
86
|
+
".psd", ".ai", ".eps", ".sketch", ".fig", ".xd", ".blend", ".3ds", ".max", ".swf", ".fla",
|
|
87
|
+
".db", ".sqlite", ".sqlite3", ".mdb", ".idx", ".dat", ".data", ".lockb",
|
|
88
|
+
".pdb", ".ipa", ".apk", ".dmg", ".iso", ".img",
|
|
82
89
|
]);
|
|
83
90
|
export function hasBinaryExtension(path) {
|
|
84
91
|
const dot = path.lastIndexOf(".");
|
|
@@ -256,7 +263,7 @@ export function checkNoChange(oldString, newString) {
|
|
|
256
263
|
}
|
|
257
264
|
export function checkStale(entry, currentHash) {
|
|
258
265
|
if (entry.hash !== currentHash) {
|
|
259
|
-
return { code: "stale", message: "File has been modified since
|
|
266
|
+
return { code: "stale", message: "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it." };
|
|
260
267
|
}
|
|
261
268
|
return undefined;
|
|
262
269
|
}
|
|
@@ -292,6 +299,95 @@ export function resolveQuoteMatch(content, oldString) {
|
|
|
292
299
|
return undefined;
|
|
293
300
|
return content.substring(idx, idx + oldString.length);
|
|
294
301
|
}
|
|
302
|
+
const UNICODE_ESCAPE_RE = /\\u[0-9a-fA-F]{4}/;
|
|
303
|
+
const NON_ASCII_RE = /[\u0080-\uffff]/;
|
|
304
|
+
function decodeUnicodeEscapes(s) {
|
|
305
|
+
return s.replace(/(\\\\)|\\u([0-9a-fA-F]{4})/g, (m, dbl, hex) => (dbl !== undefined ? m : String.fromCharCode(parseInt(hex, 16))));
|
|
306
|
+
}
|
|
307
|
+
function escapedSpellingPattern(s) {
|
|
308
|
+
let out = "";
|
|
309
|
+
for (let i = 0; i < s.length; i++) {
|
|
310
|
+
const code = s.charCodeAt(i);
|
|
311
|
+
if (code >= 128) {
|
|
312
|
+
out += "\\\\u";
|
|
313
|
+
for (const d of code.toString(16).padStart(4, "0"))
|
|
314
|
+
out += d >= "a" ? `[${d}${d.toUpperCase()}]` : d;
|
|
315
|
+
}
|
|
316
|
+
else
|
|
317
|
+
out += s[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
export function resolveEscapeMatch(content, oldString) {
|
|
322
|
+
if (oldString === "")
|
|
323
|
+
return undefined;
|
|
324
|
+
if (UNICODE_ESCAPE_RE.test(oldString)) {
|
|
325
|
+
const decoded = decodeUnicodeEscapes(oldString);
|
|
326
|
+
if (decoded !== oldString && content.includes(decoded))
|
|
327
|
+
return decoded;
|
|
328
|
+
}
|
|
329
|
+
if (NON_ASCII_RE.test(oldString)) {
|
|
330
|
+
let hit = null;
|
|
331
|
+
try {
|
|
332
|
+
hit = content.match(new RegExp(escapedSpellingPattern(oldString)));
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
hit = null;
|
|
336
|
+
}
|
|
337
|
+
if (hit)
|
|
338
|
+
return hit[0];
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
export function adaptNewStringEscapes(oldString, matchedOld, newString) {
|
|
343
|
+
if (oldString === matchedOld)
|
|
344
|
+
return newString;
|
|
345
|
+
if (NON_ASCII_RE.test(oldString)) {
|
|
346
|
+
let full;
|
|
347
|
+
try {
|
|
348
|
+
full = new RegExp(`^${escapedSpellingPattern(oldString)}$`);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
full = undefined;
|
|
352
|
+
}
|
|
353
|
+
if (full?.test(matchedOld)) {
|
|
354
|
+
const hexOf = new Map();
|
|
355
|
+
let upper = 0;
|
|
356
|
+
let lower = 0;
|
|
357
|
+
for (let i = 0, at = 0; i < oldString.length; i++) {
|
|
358
|
+
const code = oldString.charCodeAt(i);
|
|
359
|
+
if (code >= 128) {
|
|
360
|
+
const hex = matchedOld.slice(at + 2, at + 6);
|
|
361
|
+
hexOf.set(code, hex);
|
|
362
|
+
for (const d of hex) {
|
|
363
|
+
if (d >= "a" && d <= "f")
|
|
364
|
+
lower++;
|
|
365
|
+
else if (d >= "A" && d <= "F")
|
|
366
|
+
upper++;
|
|
367
|
+
}
|
|
368
|
+
at += 6;
|
|
369
|
+
}
|
|
370
|
+
else
|
|
371
|
+
at += 1;
|
|
372
|
+
}
|
|
373
|
+
return newString.replace(/[\u0080-\uffff]/g, (ch) => {
|
|
374
|
+
const code = ch.charCodeAt(0);
|
|
375
|
+
const known = hexOf.get(code);
|
|
376
|
+
if (known !== undefined)
|
|
377
|
+
return `\\u${known}`;
|
|
378
|
+
const hex = code.toString(16).padStart(4, "0");
|
|
379
|
+
return `\\u${upper > lower ? hex.toUpperCase() : hex}`;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (UNICODE_ESCAPE_RE.test(oldString) && decodeUnicodeEscapes(oldString) === matchedOld)
|
|
384
|
+
return decodeUnicodeEscapes(newString);
|
|
385
|
+
return newString;
|
|
386
|
+
}
|
|
387
|
+
export function escapeMatchWasAttempted(oldString) {
|
|
388
|
+
return UNICODE_ESCAPE_RE.test(oldString) || NON_ASCII_RE.test(oldString);
|
|
389
|
+
}
|
|
390
|
+
export const ESCAPE_MATCH_MISS_NOTE = "\n(note: Edit also tried swapping \\uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)";
|
|
295
391
|
function isQuoteOpenerPosition(chars, i) {
|
|
296
392
|
if (i === 0)
|
|
297
393
|
return true;
|
|
@@ -21,6 +21,7 @@ export declare function splitAbsoluteGlobPattern(pattern: string): {
|
|
|
21
21
|
relativePattern: string;
|
|
22
22
|
} | undefined;
|
|
23
23
|
export declare function shellQuote(s: string): string;
|
|
24
|
+
export declare function invalidGlobTokens(glob: string): string[];
|
|
24
25
|
export declare function gitignoreMatcher(content: string): (rel: string, isDir: boolean) => boolean;
|
|
25
26
|
export declare function buildIgnore(env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<(rel: string, isDir: boolean) => boolean>;
|
|
26
27
|
export interface WalkResult {
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -76,7 +76,7 @@ export function splitAbsoluteGlobPattern(pattern) {
|
|
|
76
76
|
export function shellQuote(s) {
|
|
77
77
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
78
78
|
}
|
|
79
|
-
function globTokenToRegExp(token, anchored, glob = false) {
|
|
79
|
+
function globTokenToRegExp(token, anchored, glob = false, report) {
|
|
80
80
|
const charToRe = (ch) => ch === "*" ? "[^/]*" : ch === "?" ? "[^/]" : ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
81
81
|
let re = "";
|
|
82
82
|
for (let i = 0; i < token.length; i++) {
|
|
@@ -100,8 +100,11 @@ function globTokenToRegExp(token, anchored, glob = false) {
|
|
|
100
100
|
re += `(?:${alts.join("|")})`;
|
|
101
101
|
i = close;
|
|
102
102
|
}
|
|
103
|
-
else
|
|
103
|
+
else {
|
|
104
104
|
re += "\\{";
|
|
105
|
+
if (report)
|
|
106
|
+
report.degraded = true;
|
|
107
|
+
}
|
|
105
108
|
}
|
|
106
109
|
else if (glob && c === "[") {
|
|
107
110
|
let j = i + 1;
|
|
@@ -129,14 +132,27 @@ function globTokenToRegExp(token, anchored, glob = false) {
|
|
|
129
132
|
re += cls;
|
|
130
133
|
i = close;
|
|
131
134
|
}
|
|
132
|
-
else
|
|
135
|
+
else {
|
|
133
136
|
re += "\\[";
|
|
137
|
+
if (report)
|
|
138
|
+
report.degraded = true;
|
|
139
|
+
}
|
|
134
140
|
}
|
|
135
141
|
else
|
|
136
142
|
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
137
143
|
}
|
|
138
144
|
return new RegExp(`${anchored ? "^" : "(^|/)"}${re}$`);
|
|
139
145
|
}
|
|
146
|
+
export function invalidGlobTokens(glob) {
|
|
147
|
+
const bad = [];
|
|
148
|
+
for (const token of splitGlobParam(glob)) {
|
|
149
|
+
const report = { degraded: false };
|
|
150
|
+
globTokenToRegExp(normalizeGlobToken(token), globIsAnchored(token), true, report);
|
|
151
|
+
if (report.degraded)
|
|
152
|
+
bad.push(token);
|
|
153
|
+
}
|
|
154
|
+
return bad;
|
|
155
|
+
}
|
|
140
156
|
function normalizeGlobToken(token) {
|
|
141
157
|
let t = token;
|
|
142
158
|
while (t.startsWith("./"))
|
|
@@ -1095,6 +1111,10 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
|
|
|
1095
1111
|
const error = `Error (Glob): path ${JSON.stringify(opts.path)} does not exist — check the path, or omit \`path\` to search the whole root.`;
|
|
1096
1112
|
return { text: error, error, filenames: [], numFiles: 0, truncated: false, durationMs: Date.now() - t0, totalMatches: 0, countIsComplete: false };
|
|
1097
1113
|
}
|
|
1114
|
+
if (probe.ok && probe.value.kind === "file") {
|
|
1115
|
+
const error = `Error (Glob): path ${JSON.stringify(opts.path)} is not a directory — \`path\` scopes the search to a directory; pass its parent directory (and name the file in \`pattern\`), or omit \`path\` to search the whole root.`;
|
|
1116
|
+
return { text: error, error, filenames: [], numFiles: 0, truncated: false, durationMs: Date.now() - t0, totalMatches: 0, countIsComplete: false };
|
|
1117
|
+
}
|
|
1098
1118
|
}
|
|
1099
1119
|
const pat = normalizeGlobToken(pattern);
|
|
1100
1120
|
const anchored = globIsAnchored(pattern);
|
package/dist/tools/monitor.js
CHANGED
|
@@ -10,7 +10,7 @@ const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its
|
|
|
10
10
|
`exceeds its timeout is killed and reported (default ${MONITOR_DEFAULT_TIMEOUT_MS}ms, max ${MONITOR_MAX_TIMEOUT_MS}ms).\n` +
|
|
11
11
|
"- stderr does NOT trigger notifications, but it is captured — read it any time with TaskOutput(task_id), " +
|
|
12
12
|
"which also serves the full re-readable stdout spool.\n" +
|
|
13
|
-
"- A monitor that produces too many events is
|
|
13
|
+
"- A monitor that produces too many events is rate-limited: excess batches are suppressed (a notification tells you how many), and a watch that stays over the limit is stopped automatically.\n" +
|
|
14
14
|
"- Set persistent: true for a session-resident watch with no watch timeout of its own; it still ends when " +
|
|
15
15
|
"the execution environment's background time budget expires, when you stop it with TaskStop(task_id), or " +
|
|
16
16
|
"when the session is released.\n" +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Stateless, task-oriented AI agent core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"smoke": "tsx src/examples/smoke.ts",
|
|
57
57
|
"eval": "tsx eval/run.ts",
|
|
58
58
|
"prepare": "npm run build",
|
|
59
|
-
"prepublishOnly": "npm run build && npm run test"
|
|
59
|
+
"prepublishOnly": "npm run build && npm run test",
|
|
60
|
+
"gate:blackbox": "node scripts/run-blackbox-gate.mjs"
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
63
|
"@modelcontextprotocol/sdk": "1.29.0",
|