@sema-agent/core 2.1.0 → 2.3.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.
Files changed (61) hide show
  1. package/dist/agents/observer.d.ts +14 -0
  2. package/dist/agents/observer.js +58 -9
  3. package/dist/agents/send-message-tool.js +55 -10
  4. package/dist/agents/subagent.d.ts +1 -0
  5. package/dist/agents/subagent.js +69 -15
  6. package/dist/agents/teacher.js +51 -23
  7. package/dist/core/context-edit.js +16 -3
  8. package/dist/core/file-snapshot-store.js +10 -1
  9. package/dist/core/runner/assemble-result.d.ts +1 -0
  10. package/dist/core/runner/assemble-result.js +12 -8
  11. package/dist/core/runner/prepare-task.d.ts +1 -0
  12. package/dist/core/runner/prepare-task.js +40 -10
  13. package/dist/core/runner/runtask.js +27 -5
  14. package/dist/core/runner/synthetic-tools.d.ts +1 -0
  15. package/dist/core/runner/synthetic-tools.js +18 -15
  16. package/dist/core/runner/turn-attachments.d.ts +12 -2
  17. package/dist/core/runner/turn-attachments.js +33 -3
  18. package/dist/core/task-registry-agent.d.ts +11 -1
  19. package/dist/core/task-registry-agent.js +40 -3
  20. package/dist/core/task-registry-monitor.js +149 -29
  21. package/dist/core/task-registry-shared.d.ts +25 -2
  22. package/dist/core/task-registry-shared.js +26 -2
  23. package/dist/core/task-registry.d.ts +5 -0
  24. package/dist/core/task-registry.js +25 -26
  25. package/dist/core/tools.d.ts +2 -0
  26. package/dist/core/tools.js +9 -0
  27. package/dist/core/types.d.ts +3 -1
  28. package/dist/core/workflow-journal-store.d.ts +16 -0
  29. package/dist/core/workflow-journal-store.js +28 -0
  30. package/dist/engine/session/memory-repo.js +5 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
  34. package/dist/orchestration/workflow-size-guideline.js +19 -9
  35. package/dist/orchestration/workflow.d.ts +16 -0
  36. package/dist/orchestration/workflow.js +80 -20
  37. package/dist/prompt-assembly/assemble.js +3 -7
  38. package/dist/prompt-assembly/packs/sema-default.js +8 -5
  39. package/dist/prompts/coordinator.d.ts +1 -1
  40. package/dist/prompts/coordinator.js +45 -0
  41. package/dist/prompts/default.d.ts +4 -5
  42. package/dist/prompts/default.js +16 -18
  43. package/dist/prompts/simple-sections.d.ts +3 -1
  44. package/dist/prompts/simple-sections.js +11 -1
  45. package/dist/stores/file/workflow-journal-store.d.ts +7 -1
  46. package/dist/stores/file/workflow-journal-store.js +70 -33
  47. package/dist/tools/fs/bash-readonly-classifier.js +20 -1
  48. package/dist/tools/fs/fs-bash.d.ts +1 -0
  49. package/dist/tools/fs/fs-bash.js +10 -3
  50. package/dist/tools/fs/fs-read.js +10 -10
  51. package/dist/tools/fs/fs-search-tools.js +42 -7
  52. package/dist/tools/fs/fs-write.js +18 -6
  53. package/dist/tools/fs/index.d.ts +1 -0
  54. package/dist/tools/fs/index.js +2 -1
  55. package/dist/tools/fs/safety.d.ts +4 -0
  56. package/dist/tools/fs/safety.js +102 -6
  57. package/dist/tools/fs/search.d.ts +1 -0
  58. package/dist/tools/fs/search.js +23 -3
  59. package/dist/tools/monitor.d.ts +2 -0
  60. package/dist/tools/monitor.js +3 -1
  61. package/package.json +3 -2
@@ -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 PROC_SENSITIVE_RE = /^\/proc\/[^/]+\/(environ|cmdline|auxv|maps|mem|stat)$/;
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) || PROC_SENSITIVE_RE.test(key) || isWinReservedDeviceKey(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
- ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar", ".jar", ".war", ".ear", ".lz4", ".zst",
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", ".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv", ".flv",
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
- ".db", ".sqlite", ".sqlite3", ".mdb", ".dat", ".pdb", ".ipa", ".apk", ".dmg", ".iso", ".img",
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 it was read; read it again before editing." };
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 {
@@ -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);
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
2
2
  import { type MonitorTimers, type TaskRegistry } from "../core/task-registry.js";
3
3
  import type { TaskNotificationPayload } from "../core/task-notification.js";
4
+ import type { ToolResultStore } from "../core/tool-result-store.js";
4
5
  export interface MonitorToolOptions {
5
6
  registry: TaskRegistry;
6
7
  owner?: string;
@@ -15,5 +16,6 @@ export interface MonitorToolOptions {
15
16
  timers?: MonitorTimers;
16
17
  batchWindowMs?: number;
17
18
  maxBatchesPerMinute?: number;
19
+ toolResultStore?: ToolResultStore;
18
20
  }
19
21
  export declare function createMonitorTool(env: ExecutionEnv, opts: MonitorToolOptions): AgentTool;
@@ -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 stopped automatically (the stop notification says why).\n" +
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" +
@@ -82,6 +82,8 @@ export function createMonitorTool(env, opts) {
82
82
  ...(opts.timers !== undefined ? { timers: opts.timers } : {}),
83
83
  ...(opts.batchWindowMs !== undefined ? { batchWindowMs: opts.batchWindowMs } : {}),
84
84
  ...(opts.maxBatchesPerMinute !== undefined ? { maxBatchesPerMinute: opts.maxBatchesPerMinute } : {}),
85
+ ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
86
+ ...(opts.toolResultStore !== undefined ? { toolResultStore: opts.toolResultStore } : {}),
85
87
  });
86
88
  }
87
89
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.1.0",
3
+ "version": "2.3.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",