open-multi-agent-kit 0.96.2 → 0.97.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 (138) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -2
  3. package/dist/core/agent-session.d.ts +9 -1
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +84 -7
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/attachment-store.d.ts +39 -0
  8. package/dist/core/attachment-store.d.ts.map +1 -0
  9. package/dist/core/attachment-store.js +81 -0
  10. package/dist/core/attachment-store.js.map +1 -0
  11. package/dist/core/command-safety.d.ts +8 -0
  12. package/dist/core/command-safety.d.ts.map +1 -1
  13. package/dist/core/command-safety.js +140 -14
  14. package/dist/core/command-safety.js.map +1 -1
  15. package/dist/core/compaction/compaction.d.ts +6 -1
  16. package/dist/core/compaction/compaction.d.ts.map +1 -1
  17. package/dist/core/compaction/compaction.js +35 -1
  18. package/dist/core/compaction/compaction.js.map +1 -1
  19. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  20. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  21. package/dist/core/context-budget-governor-v2.js +1 -1
  22. package/dist/core/context-budget-governor-v2.js.map +1 -1
  23. package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
  24. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  25. package/dist/core/context-budget-v2-cache-keys.js +2 -0
  26. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  27. package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
  28. package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
  29. package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
  30. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  31. package/dist/core/context-budget-v2-planner.js +2 -1
  32. package/dist/core/context-budget-v2-planner.js.map +1 -1
  33. package/dist/core/context-budget-v2-scoring.d.ts +17 -0
  34. package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
  35. package/dist/core/context-budget-v2-scoring.js +25 -8
  36. package/dist/core/context-budget-v2-scoring.js.map +1 -1
  37. package/dist/core/context-budget-v2-selection.d.ts +2 -2
  38. package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
  39. package/dist/core/context-budget-v2-selection.js +18 -1
  40. package/dist/core/context-budget-v2-selection.js.map +1 -1
  41. package/dist/core/context-budget-v2-types.d.ts +17 -0
  42. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  43. package/dist/core/context-budget-v2-types.js +15 -0
  44. package/dist/core/context-budget-v2-types.js.map +1 -1
  45. package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
  46. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  47. package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
  48. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  49. package/dist/core/extensions/types.d.ts +12 -1
  50. package/dist/core/extensions/types.d.ts.map +1 -1
  51. package/dist/core/extensions/types.js.map +1 -1
  52. package/dist/core/model-registry.d.ts +8 -3
  53. package/dist/core/model-registry.d.ts.map +1 -1
  54. package/dist/core/model-registry.js +71 -1
  55. package/dist/core/model-registry.js.map +1 -1
  56. package/dist/core/package-doctor-source-scan.d.ts +9 -0
  57. package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
  58. package/dist/core/package-doctor-source-scan.js +77 -4
  59. package/dist/core/package-doctor-source-scan.js.map +1 -1
  60. package/dist/core/prompt-attachment.d.ts +51 -0
  61. package/dist/core/prompt-attachment.d.ts.map +1 -0
  62. package/dist/core/prompt-attachment.js +186 -0
  63. package/dist/core/prompt-attachment.js.map +1 -0
  64. package/dist/core/provider-resilience.d.ts +28 -0
  65. package/dist/core/provider-resilience.d.ts.map +1 -1
  66. package/dist/core/provider-resilience.js +50 -0
  67. package/dist/core/provider-resilience.js.map +1 -1
  68. package/dist/core/provider-retry.d.ts +1 -1
  69. package/dist/core/provider-retry.d.ts.map +1 -1
  70. package/dist/core/provider-retry.js +1 -1
  71. package/dist/core/provider-retry.js.map +1 -1
  72. package/dist/core/provider-usage.d.ts.map +1 -1
  73. package/dist/core/provider-usage.js +68 -2
  74. package/dist/core/provider-usage.js.map +1 -1
  75. package/dist/core/reasoning-router-promotion.d.ts +84 -0
  76. package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
  77. package/dist/core/reasoning-router-promotion.js +92 -0
  78. package/dist/core/reasoning-router-promotion.js.map +1 -0
  79. package/dist/core/session-bash-service.d.ts.map +1 -1
  80. package/dist/core/session-bash-service.js +4 -3
  81. package/dist/core/session-bash-service.js.map +1 -1
  82. package/dist/core/session-termination.d.ts +1 -1
  83. package/dist/core/session-termination.d.ts.map +1 -1
  84. package/dist/core/session-termination.js +11 -2
  85. package/dist/core/session-termination.js.map +1 -1
  86. package/dist/core/tools/bash.d.ts.map +1 -1
  87. package/dist/core/tools/bash.js +3 -7
  88. package/dist/core/tools/bash.js.map +1 -1
  89. package/dist/core/tools/grep.d.ts.map +1 -1
  90. package/dist/core/tools/grep.js +13 -48
  91. package/dist/core/tools/grep.js.map +1 -1
  92. package/dist/core/tools/read.d.ts.map +1 -1
  93. package/dist/core/tools/read.js +1 -34
  94. package/dist/core/tools/read.js.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +1 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
  100. package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
  101. package/dist/modes/interactive/components/attachment-strip.js +136 -0
  102. package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
  103. package/dist/modes/interactive/components/footer.d.ts +6 -0
  104. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  105. package/dist/modes/interactive/components/footer.js +26 -1
  106. package/dist/modes/interactive/components/footer.js.map +1 -1
  107. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  108. package/dist/modes/interactive/components/status-sidebar.js +12 -1
  109. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  110. package/dist/modes/interactive/interactive-mode.d.ts +23 -4
  111. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  112. package/dist/modes/interactive/interactive-mode.js +174 -53
  113. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  114. package/docs/compaction.md +1 -1
  115. package/docs/extensions.md +12 -3
  116. package/docs/index.md +4 -1
  117. package/docs/metrics.md +74 -2
  118. package/docs/provider-resilience.md +11 -0
  119. package/docs/usage.md +1 -1
  120. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  121. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  122. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  123. package/examples/extensions/gondolin/package-lock.json +2 -2
  124. package/examples/extensions/gondolin/package.json +1 -1
  125. package/examples/extensions/sandbox/package-lock.json +2 -2
  126. package/examples/extensions/sandbox/package.json +1 -1
  127. package/examples/extensions/with-deps/package-lock.json +2 -2
  128. package/examples/extensions/with-deps/package.json +1 -1
  129. package/npm-shrinkwrap.json +18 -18
  130. package/package.json +7 -7
  131. package/dist/core/tools/omp-pure-seams.d.ts +0 -41
  132. package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
  133. package/dist/core/tools/omp-pure-seams.js +0 -142
  134. package/dist/core/tools/omp-pure-seams.js.map +0 -1
  135. package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
  136. package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
  137. package/dist/core/tools/omp-seam-runtime.js +0 -40
  138. package/dist/core/tools/omp-seam-runtime.js.map +0 -1
@@ -1,7 +1,8 @@
1
1
  import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
- import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  const MAX_EXTENSION_FILES = 256;
4
4
  const MAX_EXTENSION_BYTES = 1_048_576;
5
+ const SOURCE_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
5
6
  export const PACKAGE_DOCTOR_SUPPORTED_EVENTS = new Set([
6
7
  "resources_discover",
7
8
  "session_start",
@@ -18,6 +19,7 @@ export const PACKAGE_DOCTOR_SUPPORTED_EVENTS = new Set([
18
19
  "before_agent_start",
19
20
  "agent_start",
20
21
  "agent_end",
22
+ "agent_settled",
21
23
  "turn_start",
22
24
  "turn_end",
23
25
  "message_start",
@@ -41,16 +43,85 @@ function packageRelativePath(root, path) {
41
43
  return null;
42
44
  return candidate.replace(/\\/gu, "/");
43
45
  }
46
+ /**
47
+ * True only for an existing regular file. A bare `existsSync` also matches the
48
+ * directory a specifier like `./nested` names, which would shadow its real
49
+ * `./nested/index.ts` target.
50
+ */
51
+ function isFilePath(path) {
52
+ try {
53
+ return statSync(path).isFile();
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** Candidate on-disk paths for one relative import specifier. */
60
+ function resolutionCandidates(fromFile, specifier) {
61
+ const base = resolve(dirname(fromFile), specifier);
62
+ const candidates = [base];
63
+ for (const extension of SOURCE_EXTENSIONS) {
64
+ candidates.push(`${base}${extension}`, resolve(base, `index${extension}`));
65
+ }
66
+ // TypeScript source commonly imports its own emitted specifier (`./x.js`).
67
+ const rewritten = base.replace(/\.(?:js|mjs|cjs|jsx)$/u, "");
68
+ if (rewritten !== base) {
69
+ for (const extension of SOURCE_EXTENSIONS)
70
+ candidates.push(`${rewritten}${extension}`);
71
+ }
72
+ return candidates;
73
+ }
74
+ /**
75
+ * Relative imports of `text` that resolve to a real file inside the package.
76
+ * Bare specifiers (`node:fs`, `some-package`) are dependencies, not package
77
+ * source, and anything resolving outside the root is dropped rather than read.
78
+ */
79
+ function relativeImportTargets(root, fromFile, text) {
80
+ const targets = [];
81
+ for (const specifier of importedSpecifiers(text)) {
82
+ if (!specifier.startsWith("./") && !specifier.startsWith("../"))
83
+ continue;
84
+ for (const candidate of resolutionCandidates(fromFile, specifier)) {
85
+ if (packageRelativePath(root, candidate) === null)
86
+ continue;
87
+ if (!isFilePath(candidate))
88
+ continue;
89
+ targets.push(candidate);
90
+ break;
91
+ }
92
+ }
93
+ return targets;
94
+ }
95
+ /**
96
+ * Read every package-owned extension source reachable from the manifest entries.
97
+ *
98
+ * Scanning only the declared entry files made the compatibility verdict wrong
99
+ * for any package that keeps handlers in imported modules, so the walk follows
100
+ * relative imports transitively. Package-external reads stay impossible: a
101
+ * candidate must resolve inside the root, and each open still verifies the
102
+ * realpath, refuses symlinks, and matches device/inode before reading.
103
+ */
44
104
  export function scanPackageExtensionSources(root, extensions) {
45
105
  const sources = [];
46
106
  const skipped = [];
47
107
  const realRoot = realpathSync(root);
48
- for (const path of [...new Set(extensions)].slice(0, MAX_EXTENSION_FILES)) {
108
+ const seen = new Set();
109
+ const queue = [...new Set(extensions)];
110
+ let truncated = extensions.length > MAX_EXTENSION_FILES;
111
+ while (queue.length > 0) {
112
+ if (sources.length >= MAX_EXTENSION_FILES) {
113
+ truncated = true;
114
+ break;
115
+ }
116
+ const path = queue.shift();
49
117
  const file = packageRelativePath(root, path);
50
118
  if (file === null) {
51
119
  skipped.push("<outside-package>");
52
120
  continue;
53
121
  }
122
+ if (seen.has(file))
123
+ continue;
124
+ seen.add(file);
54
125
  let fd;
55
126
  try {
56
127
  const realPath = realpathSync(path);
@@ -77,7 +148,9 @@ export function scanPackageExtensionSources(root, extensions) {
77
148
  skipped.push(file);
78
149
  continue;
79
150
  }
80
- sources.push({ file, text: buffer.subarray(0, offset).toString("utf8") });
151
+ const text = buffer.subarray(0, offset).toString("utf8");
152
+ sources.push({ file, text });
153
+ queue.push(...relativeImportTargets(root, path, text));
81
154
  }
82
155
  catch {
83
156
  skipped.push(file);
@@ -87,7 +160,7 @@ export function scanPackageExtensionSources(root, extensions) {
87
160
  closeSync(fd);
88
161
  }
89
162
  }
90
- if (extensions.length > MAX_EXTENSION_FILES)
163
+ if (truncated)
91
164
  skipped.push("<file-limit>");
92
165
  return { sources, skipped };
93
166
  }
@@ -1 +1 @@
1
- {"version":3,"file":"package-doctor-source-scan.js","sourceRoot":"","sources":["../../src/core/package-doctor-source-scan.ts"],"names":[],"mappings":"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","sourcesContent":["import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve, sep } from \"node:path\";\n\nconst MAX_EXTENSION_FILES = 256;\nconst MAX_EXTENSION_BYTES = 1_048_576;\n\nexport const PACKAGE_DOCTOR_SUPPORTED_EVENTS = new Set([\n\t\"resources_discover\",\n\t\"session_start\",\n\t\"session_before_switch\",\n\t\"session_before_fork\",\n\t\"session_before_compact\",\n\t\"session_compact\",\n\t\"session_shutdown\",\n\t\"session_before_tree\",\n\t\"session_tree\",\n\t\"context\",\n\t\"before_provider_request\",\n\t\"after_provider_response\",\n\t\"before_agent_start\",\n\t\"agent_start\",\n\t\"agent_end\",\n\t\"turn_start\",\n\t\"turn_end\",\n\t\"message_start\",\n\t\"message_update\",\n\t\"message_end\",\n\t\"tool_execution_start\",\n\t\"tool_execution_update\",\n\t\"tool_execution_end\",\n\t\"model_select\",\n\t\"thinking_level_select\",\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"user_bash\",\n\t\"input\",\n]);\n\nexport interface ScannedPackageSource {\n\tfile: string;\n\ttext: string;\n}\n\nfunction packageRelativePath(root: string, path: string): string | null {\n\tconst candidate = relative(root, resolve(path));\n\tif (!candidate || candidate === \".\") return candidate || \".\";\n\tif (candidate === \"..\" || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) return null;\n\treturn candidate.replace(/\\\\/gu, \"/\");\n}\n\nexport function scanPackageExtensionSources(\n\troot: string,\n\textensions: string[],\n): { sources: ScannedPackageSource[]; skipped: string[] } {\n\tconst sources: ScannedPackageSource[] = [];\n\tconst skipped: string[] = [];\n\tconst realRoot = realpathSync(root);\n\tfor (const path of [...new Set(extensions)].slice(0, MAX_EXTENSION_FILES)) {\n\t\tconst file = packageRelativePath(root, path);\n\t\tif (file === null) {\n\t\t\tskipped.push(\"<outside-package>\");\n\t\t\tcontinue;\n\t\t}\n\t\tlet fd: number | undefined;\n\t\ttry {\n\t\t\tconst realPath = realpathSync(path);\n\t\t\tif (packageRelativePath(realRoot, realPath) === null) {\n\t\t\t\tskipped.push(\"<outside-package>\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst expected = statSync(realPath);\n\t\t\tfd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n\t\t\tconst opened = fstatSync(fd);\n\t\t\tif (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino) {\n\t\t\t\tskipped.push(file);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst buffer = Buffer.alloc(MAX_EXTENSION_BYTES + 1);\n\t\t\tlet offset = 0;\n\t\t\twhile (offset < buffer.length) {\n\t\t\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, offset);\n\t\t\t\tif (read === 0) break;\n\t\t\t\toffset += read;\n\t\t\t}\n\t\t\tif (offset > MAX_EXTENSION_BYTES) {\n\t\t\t\tskipped.push(file);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsources.push({ file, text: buffer.subarray(0, offset).toString(\"utf8\") });\n\t\t} catch {\n\t\t\tskipped.push(file);\n\t\t} finally {\n\t\t\tif (fd !== undefined) closeSync(fd);\n\t\t}\n\t}\n\tif (extensions.length > MAX_EXTENSION_FILES) skipped.push(\"<file-limit>\");\n\treturn { sources, skipped };\n}\n\nexport function importedSpecifiers(text: string): string[] {\n\tconst specifiers = new Set<string>();\n\tconst patterns = [\n\t\t/\\b(?:import|export)\\s+(?:[^\"']+?\\s+from\\s+)?[\"']([^\"']+)[\"']/gu,\n\t\t/\\b(?:import|require)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/gu,\n\t];\n\tfor (const pattern of patterns) {\n\t\tfor (const match of text.matchAll(pattern)) {\n\t\t\tif (match[1]) specifiers.add(match[1]);\n\t\t}\n\t}\n\treturn [...specifiers];\n}\n\nexport function subscribedEvents(text: string): string[] {\n\tconst events: string[] = [];\n\tfor (const match of text.matchAll(/\\b(?:api|omk|pi)\\.on\\(\\s*[\"']([^\"']+)[\"']/gu)) {\n\t\tif (match[1] !== undefined) events.push(match[1]);\n\t}\n\treturn events;\n}\n\nexport function sourceFilesMatching(sources: ScannedPackageSource[], predicate: (text: string) => boolean): string[] {\n\tconst files: string[] = [];\n\tfor (const source of sources) {\n\t\tif (predicate(source.text)) files.push(source.file);\n\t}\n\treturn files;\n}\n"]}
1
+ {"version":3,"file":"package-doctor-source-scan.js","sourceRoot":"","sources":["../../src/core/package-doctor-source-scan.ts"],"names":[],"mappings":"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","sourcesContent":["import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from \"node:fs\";\nimport { dirname, isAbsolute, relative, resolve, sep } from \"node:path\";\n\nconst MAX_EXTENSION_FILES = 256;\nconst MAX_EXTENSION_BYTES = 1_048_576;\nconst SOURCE_EXTENSIONS = [\".ts\", \".mts\", \".cts\", \".tsx\", \".js\", \".mjs\", \".cjs\", \".jsx\"] as const;\n\nexport const PACKAGE_DOCTOR_SUPPORTED_EVENTS = new Set([\n\t\"resources_discover\",\n\t\"session_start\",\n\t\"session_before_switch\",\n\t\"session_before_fork\",\n\t\"session_before_compact\",\n\t\"session_compact\",\n\t\"session_shutdown\",\n\t\"session_before_tree\",\n\t\"session_tree\",\n\t\"context\",\n\t\"before_provider_request\",\n\t\"after_provider_response\",\n\t\"before_agent_start\",\n\t\"agent_start\",\n\t\"agent_end\",\n\t\"agent_settled\",\n\t\"turn_start\",\n\t\"turn_end\",\n\t\"message_start\",\n\t\"message_update\",\n\t\"message_end\",\n\t\"tool_execution_start\",\n\t\"tool_execution_update\",\n\t\"tool_execution_end\",\n\t\"model_select\",\n\t\"thinking_level_select\",\n\t\"tool_call\",\n\t\"tool_result\",\n\t\"user_bash\",\n\t\"input\",\n]);\n\nexport interface ScannedPackageSource {\n\tfile: string;\n\ttext: string;\n}\n\nfunction packageRelativePath(root: string, path: string): string | null {\n\tconst candidate = relative(root, resolve(path));\n\tif (!candidate || candidate === \".\") return candidate || \".\";\n\tif (candidate === \"..\" || candidate.startsWith(`..${sep}`) || isAbsolute(candidate)) return null;\n\treturn candidate.replace(/\\\\/gu, \"/\");\n}\n\n/**\n * True only for an existing regular file. A bare `existsSync` also matches the\n * directory a specifier like `./nested` names, which would shadow its real\n * `./nested/index.ts` target.\n */\nfunction isFilePath(path: string): boolean {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Candidate on-disk paths for one relative import specifier. */\nfunction resolutionCandidates(fromFile: string, specifier: string): string[] {\n\tconst base = resolve(dirname(fromFile), specifier);\n\tconst candidates = [base];\n\tfor (const extension of SOURCE_EXTENSIONS) {\n\t\tcandidates.push(`${base}${extension}`, resolve(base, `index${extension}`));\n\t}\n\t// TypeScript source commonly imports its own emitted specifier (`./x.js`).\n\tconst rewritten = base.replace(/\\.(?:js|mjs|cjs|jsx)$/u, \"\");\n\tif (rewritten !== base) {\n\t\tfor (const extension of SOURCE_EXTENSIONS) candidates.push(`${rewritten}${extension}`);\n\t}\n\treturn candidates;\n}\n\n/**\n * Relative imports of `text` that resolve to a real file inside the package.\n * Bare specifiers (`node:fs`, `some-package`) are dependencies, not package\n * source, and anything resolving outside the root is dropped rather than read.\n */\nfunction relativeImportTargets(root: string, fromFile: string, text: string): string[] {\n\tconst targets: string[] = [];\n\tfor (const specifier of importedSpecifiers(text)) {\n\t\tif (!specifier.startsWith(\"./\") && !specifier.startsWith(\"../\")) continue;\n\t\tfor (const candidate of resolutionCandidates(fromFile, specifier)) {\n\t\t\tif (packageRelativePath(root, candidate) === null) continue;\n\t\t\tif (!isFilePath(candidate)) continue;\n\t\t\ttargets.push(candidate);\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn targets;\n}\n\n/**\n * Read every package-owned extension source reachable from the manifest entries.\n *\n * Scanning only the declared entry files made the compatibility verdict wrong\n * for any package that keeps handlers in imported modules, so the walk follows\n * relative imports transitively. Package-external reads stay impossible: a\n * candidate must resolve inside the root, and each open still verifies the\n * realpath, refuses symlinks, and matches device/inode before reading.\n */\nexport function scanPackageExtensionSources(\n\troot: string,\n\textensions: string[],\n): { sources: ScannedPackageSource[]; skipped: string[] } {\n\tconst sources: ScannedPackageSource[] = [];\n\tconst skipped: string[] = [];\n\tconst realRoot = realpathSync(root);\n\tconst seen = new Set<string>();\n\tconst queue = [...new Set(extensions)];\n\tlet truncated = extensions.length > MAX_EXTENSION_FILES;\n\n\twhile (queue.length > 0) {\n\t\tif (sources.length >= MAX_EXTENSION_FILES) {\n\t\t\ttruncated = true;\n\t\t\tbreak;\n\t\t}\n\t\tconst path = queue.shift() as string;\n\t\tconst file = packageRelativePath(root, path);\n\t\tif (file === null) {\n\t\t\tskipped.push(\"<outside-package>\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (seen.has(file)) continue;\n\t\tseen.add(file);\n\t\tlet fd: number | undefined;\n\t\ttry {\n\t\t\tconst realPath = realpathSync(path);\n\t\t\tif (packageRelativePath(realRoot, realPath) === null) {\n\t\t\t\tskipped.push(\"<outside-package>\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst expected = statSync(realPath);\n\t\t\tfd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));\n\t\t\tconst opened = fstatSync(fd);\n\t\t\tif (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino) {\n\t\t\t\tskipped.push(file);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst buffer = Buffer.alloc(MAX_EXTENSION_BYTES + 1);\n\t\t\tlet offset = 0;\n\t\t\twhile (offset < buffer.length) {\n\t\t\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, offset);\n\t\t\t\tif (read === 0) break;\n\t\t\t\toffset += read;\n\t\t\t}\n\t\t\tif (offset > MAX_EXTENSION_BYTES) {\n\t\t\t\tskipped.push(file);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst text = buffer.subarray(0, offset).toString(\"utf8\");\n\t\t\tsources.push({ file, text });\n\t\t\tqueue.push(...relativeImportTargets(root, path, text));\n\t\t} catch {\n\t\t\tskipped.push(file);\n\t\t} finally {\n\t\t\tif (fd !== undefined) closeSync(fd);\n\t\t}\n\t}\n\tif (truncated) skipped.push(\"<file-limit>\");\n\treturn { sources, skipped };\n}\n\nexport function importedSpecifiers(text: string): string[] {\n\tconst specifiers = new Set<string>();\n\tconst patterns = [\n\t\t/\\b(?:import|export)\\s+(?:[^\"']+?\\s+from\\s+)?[\"']([^\"']+)[\"']/gu,\n\t\t/\\b(?:import|require)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/gu,\n\t];\n\tfor (const pattern of patterns) {\n\t\tfor (const match of text.matchAll(pattern)) {\n\t\t\tif (match[1]) specifiers.add(match[1]);\n\t\t}\n\t}\n\treturn [...specifiers];\n}\n\nexport function subscribedEvents(text: string): string[] {\n\tconst events: string[] = [];\n\tfor (const match of text.matchAll(/\\b(?:api|omk|pi)\\.on\\(\\s*[\"']([^\"']+)[\"']/gu)) {\n\t\tif (match[1] !== undefined) events.push(match[1]);\n\t}\n\treturn events;\n}\n\nexport function sourceFilesMatching(sources: ScannedPackageSource[], predicate: (text: string) => boolean): string[] {\n\tconst files: string[] = [];\n\tfor (const source of sources) {\n\t\tif (predicate(source.text)) files.push(source.file);\n\t}\n\treturn files;\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * First-class prompt image attachments.
3
+ *
4
+ * Pure validation/metadata layer between clipboard/file acquisition and
5
+ * `AgentSession.prompt(text, { images })`. Attachments are content-addressed
6
+ * (sha256), validated by magic bytes — clipboard MIME metadata is never
7
+ * trusted — and carry decoded dimensions for preview and pixel-budget checks.
8
+ */
9
+ export type PromptAttachmentSource = "clipboard" | "file" | "browser-capture";
10
+ export declare const PROMPT_IMAGE_MIME_TYPES: readonly ["image/png", "image/jpeg", "image/webp", "image/gif"];
11
+ export type PromptImageMimeType = (typeof PROMPT_IMAGE_MIME_TYPES)[number];
12
+ export interface PromptImageAttachment {
13
+ readonly id: string;
14
+ readonly kind: "image";
15
+ readonly source: PromptAttachmentSource;
16
+ readonly mimeType: PromptImageMimeType;
17
+ readonly byteLength: number;
18
+ /** 0 when dimensions cannot be parsed from the container header. */
19
+ readonly width: number;
20
+ /** 0 when dimensions cannot be parsed from the container header. */
21
+ readonly height: number;
22
+ readonly sha256: string;
23
+ readonly createdAt: number;
24
+ }
25
+ /** Defaults from the attachment roadmap (§validation). */
26
+ export declare const PROMPT_ATTACHMENT_LIMITS: {
27
+ readonly maxAttachments: 8;
28
+ readonly maxImageBytes: number;
29
+ readonly maxDraftBytes: number;
30
+ readonly maxPixels: 40000000;
31
+ };
32
+ export type PromptAttachmentValidationCode = "empty" | "unsupported" | "too-large" | "too-many-pixels" | "limit-reached";
33
+ export declare class PromptAttachmentError extends Error {
34
+ readonly code: PromptAttachmentValidationCode;
35
+ constructor(code: PromptAttachmentValidationCode, message: string);
36
+ }
37
+ /** Detect the image container from magic bytes. Clipboard metadata is not trusted. */
38
+ export declare function sniffImageMimeType(bytes: Uint8Array): PromptImageMimeType | null;
39
+ export interface PromptAttachmentLimits {
40
+ maxImageBytes?: number;
41
+ maxPixels?: number;
42
+ }
43
+ /**
44
+ * Validate raw image bytes and build the attachment record. Throws
45
+ * {@link PromptAttachmentError} with a stable code so callers can surface a
46
+ * precise message instead of silently dropping the paste.
47
+ */
48
+ export declare function createPromptImageAttachment(bytes: Uint8Array, source: PromptAttachmentSource, limits?: PromptAttachmentLimits): PromptImageAttachment;
49
+ /** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */
50
+ export declare function describePromptImageAttachment(attachment: PromptImageAttachment): string;
51
+ //# sourceMappingURL=prompt-attachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-attachment.d.ts","sourceRoot":"","sources":["../../src/core/prompt-attachment.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,MAAM,sBAAsB,GAAG,WAAW,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE9E,eAAO,MAAM,uBAAuB,iEAAkE,CAAC;AACvG,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3E,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC3B;AAED,2DAA0D;AAC1D,eAAO,MAAM,wBAAwB;;;;;CAK3B,CAAC;AAEX,MAAM,MAAM,8BAA8B,GACvC,OAAO,GACP,aAAa,GACb,WAAW,GACX,iBAAiB,GACjB,eAAe,CAAC;AAEnB,qBAAa,qBAAsB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,8BAA8B,CAAC;IAE9C,YAAY,IAAI,EAAE,8BAA8B,EAAE,OAAO,EAAE,MAAM,EAIhE;CACD;AAQD,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,mBAAmB,GAAG,IAAI,CAsChF;AAoFD,MAAM,WAAW,sBAAsB;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,sBAAsB,EAC9B,MAAM,GAAE,sBAA2B,GACjC,qBAAqB,CAwCvB;AAED,wEAAqE;AACrE,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,qBAAqB,GAAG,MAAM,CAMvF","sourcesContent":["/**\n * First-class prompt image attachments.\n *\n * Pure validation/metadata layer between clipboard/file acquisition and\n * `AgentSession.prompt(text, { images })`. Attachments are content-addressed\n * (sha256), validated by magic bytes — clipboard MIME metadata is never\n * trusted — and carry decoded dimensions for preview and pixel-budget checks.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\n\nexport type PromptAttachmentSource = \"clipboard\" | \"file\" | \"browser-capture\";\n\nexport const PROMPT_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\nexport type PromptImageMimeType = (typeof PROMPT_IMAGE_MIME_TYPES)[number];\n\nexport interface PromptImageAttachment {\n\treadonly id: string;\n\treadonly kind: \"image\";\n\treadonly source: PromptAttachmentSource;\n\treadonly mimeType: PromptImageMimeType;\n\treadonly byteLength: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly width: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly height: number;\n\treadonly sha256: string;\n\treadonly createdAt: number;\n}\n\n/** Defaults from the attachment roadmap (§validation). */\nexport const PROMPT_ATTACHMENT_LIMITS = {\n\tmaxAttachments: 8,\n\tmaxImageBytes: 10 * 1024 * 1024,\n\tmaxDraftBytes: 32 * 1024 * 1024,\n\tmaxPixels: 40_000_000,\n} as const;\n\nexport type PromptAttachmentValidationCode =\n\t| \"empty\"\n\t| \"unsupported\"\n\t| \"too-large\"\n\t| \"too-many-pixels\"\n\t| \"limit-reached\";\n\nexport class PromptAttachmentError extends Error {\n\treadonly code: PromptAttachmentValidationCode;\n\n\tconstructor(code: PromptAttachmentValidationCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"PromptAttachmentError\";\n\t\tthis.code = code;\n\t}\n}\n\ninterface ImageHeaderInfo {\n\tmimeType: PromptImageMimeType;\n\twidth: number;\n\theight: number;\n}\n\n/** Detect the image container from magic bytes. Clipboard metadata is not trusted. */\nexport function sniffImageMimeType(bytes: Uint8Array): PromptImageMimeType | null {\n\tif (bytes.length < 12) return null;\n\tif (\n\t\tbytes[0] === 0x89 &&\n\t\tbytes[1] === 0x50 &&\n\t\tbytes[2] === 0x4e &&\n\t\tbytes[3] === 0x47 &&\n\t\tbytes[4] === 0x0d &&\n\t\tbytes[5] === 0x0a &&\n\t\tbytes[6] === 0x1a &&\n\t\tbytes[7] === 0x0a\n\t) {\n\t\treturn \"image/png\";\n\t}\n\tif (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return \"image/jpeg\";\n\tif (\n\t\tbytes[0] === 0x47 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x38 &&\n\t\t(bytes[4] === 0x37 || bytes[4] === 0x39) &&\n\t\tbytes[5] === 0x61\n\t) {\n\t\treturn \"image/gif\";\n\t}\n\tif (\n\t\tbytes[0] === 0x52 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x46 &&\n\t\tbytes[8] === 0x57 &&\n\t\tbytes[9] === 0x45 &&\n\t\tbytes[10] === 0x42 &&\n\t\tbytes[11] === 0x50\n\t) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nfunction readPngDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// IHDR: width u32BE at 16, height u32BE at 20.\n\tif (bytes.length < 24) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint32(16), height: view.getUint32(20) };\n}\n\nfunction readGifDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 10) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint16(6, true), height: view.getUint16(8, true) };\n}\n\nfunction readWebpDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 30) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tconst chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);\n\tif (chunk === \"VP8X\") {\n\t\tconst width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));\n\t\tconst height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8 \") {\n\t\t// Lossy bitstream: frame tag skips 3 bytes, then 3-byte start code, then dims.\n\t\tconst width = view.getUint16(26, true) & 0x3fff;\n\t\tconst height = view.getUint16(28, true) & 0x3fff;\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8L\") {\n\t\tconst bits = view.getUint32(21, true);\n\t\tconst width = (bits & 0x3fff) + 1;\n\t\tconst height = ((bits >> 14) & 0x3fff) + 1;\n\t\treturn { width, height };\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readJpegDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// Walk SOF markers; height/width are u16BE at marker offset +5/+7.\n\tlet index = 2;\n\twhile (index + 9 < bytes.length) {\n\t\tif (bytes[index] !== 0xff) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst marker = bytes[index + 1];\n\t\tif (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n\t\t\tindex += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tconst length = (bytes[index + 2] << 8) | bytes[index + 3];\n\t\tif (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n\t\t\tconst height = (bytes[index + 5] << 8) | bytes[index + 6];\n\t\t\tconst width = (bytes[index + 7] << 8) | bytes[index + 8];\n\t\t\treturn { width, height };\n\t\t}\n\t\tindex += 2 + length;\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readImageHeader(bytes: Uint8Array, mimeType: PromptImageMimeType): ImageHeaderInfo {\n\tlet dims = { width: 0, height: 0 };\n\tswitch (mimeType) {\n\t\tcase \"image/png\":\n\t\t\tdims = readPngDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/gif\":\n\t\t\tdims = readGifDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/webp\":\n\t\t\tdims = readWebpDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/jpeg\":\n\t\t\tdims = readJpegDimensions(bytes);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn { mimeType, ...dims };\n}\n\nexport interface PromptAttachmentLimits {\n\tmaxImageBytes?: number;\n\tmaxPixels?: number;\n}\n\n/**\n * Validate raw image bytes and build the attachment record. Throws\n * {@link PromptAttachmentError} with a stable code so callers can surface a\n * precise message instead of silently dropping the paste.\n */\nexport function createPromptImageAttachment(\n\tbytes: Uint8Array,\n\tsource: PromptAttachmentSource,\n\tlimits: PromptAttachmentLimits = {},\n): PromptImageAttachment {\n\tconst maxImageBytes = limits.maxImageBytes ?? PROMPT_ATTACHMENT_LIMITS.maxImageBytes;\n\tconst maxPixels = limits.maxPixels ?? PROMPT_ATTACHMENT_LIMITS.maxPixels;\n\n\tif (bytes.length === 0) {\n\t\tthrow new PromptAttachmentError(\"empty\", \"Clipboard image is empty.\");\n\t}\n\tif (bytes.length > maxImageBytes) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-large\",\n\t\t\t`Image is ${(bytes.length / (1024 * 1024)).toFixed(1)} MiB — limit is ${(maxImageBytes / (1024 * 1024)).toFixed(0)} MiB.`,\n\t\t);\n\t}\n\tconst mimeType = sniffImageMimeType(bytes);\n\tif (!mimeType) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"unsupported\",\n\t\t\t\"Clipboard content is not a supported image (PNG, JPEG, WebP, GIF).\",\n\t\t);\n\t}\n\tconst header = readImageHeader(bytes, mimeType);\n\tconst pixels = header.width * header.height;\n\tif (pixels > maxPixels) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-many-pixels\",\n\t\t\t`Image is ${header.width}×${header.height} (${(pixels / 1_000_000).toFixed(0)} MP) — limit is ${(maxPixels / 1_000_000).toFixed(0)} MP.`,\n\t\t);\n\t}\n\tconst sha256 = createHash(\"sha256\").update(bytes).digest(\"hex\");\n\treturn {\n\t\tid: randomUUID(),\n\t\tkind: \"image\",\n\t\tsource,\n\t\tmimeType,\n\t\tbyteLength: bytes.length,\n\t\twidth: header.width,\n\t\theight: header.height,\n\t\tsha256,\n\t\tcreatedAt: Date.now(),\n\t};\n}\n\n/** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */\nexport function describePromptImageAttachment(attachment: PromptImageAttachment): string {\n\tconst dims = attachment.width > 0 && attachment.height > 0 ? `${attachment.width}×${attachment.height} · ` : \"\";\n\tconst kib = attachment.byteLength / 1024;\n\tconst size = kib >= 1024 ? `${(kib / 1024).toFixed(1)} MiB` : `${Math.round(kib)} KiB`;\n\tconst mime = attachment.mimeType.replace(\"image/\", \"\").toUpperCase();\n\treturn `${dims}${mime} · ${size}`;\n}\n"]}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * First-class prompt image attachments.
3
+ *
4
+ * Pure validation/metadata layer between clipboard/file acquisition and
5
+ * `AgentSession.prompt(text, { images })`. Attachments are content-addressed
6
+ * (sha256), validated by magic bytes — clipboard MIME metadata is never
7
+ * trusted — and carry decoded dimensions for preview and pixel-budget checks.
8
+ */
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ export const PROMPT_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
11
+ /** Defaults from the attachment roadmap (§validation). */
12
+ export const PROMPT_ATTACHMENT_LIMITS = {
13
+ maxAttachments: 8,
14
+ maxImageBytes: 10 * 1024 * 1024,
15
+ maxDraftBytes: 32 * 1024 * 1024,
16
+ maxPixels: 40_000_000,
17
+ };
18
+ export class PromptAttachmentError extends Error {
19
+ code;
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.name = "PromptAttachmentError";
23
+ this.code = code;
24
+ }
25
+ }
26
+ /** Detect the image container from magic bytes. Clipboard metadata is not trusted. */
27
+ export function sniffImageMimeType(bytes) {
28
+ if (bytes.length < 12)
29
+ return null;
30
+ if (bytes[0] === 0x89 &&
31
+ bytes[1] === 0x50 &&
32
+ bytes[2] === 0x4e &&
33
+ bytes[3] === 0x47 &&
34
+ bytes[4] === 0x0d &&
35
+ bytes[5] === 0x0a &&
36
+ bytes[6] === 0x1a &&
37
+ bytes[7] === 0x0a) {
38
+ return "image/png";
39
+ }
40
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
41
+ return "image/jpeg";
42
+ if (bytes[0] === 0x47 &&
43
+ bytes[1] === 0x49 &&
44
+ bytes[2] === 0x46 &&
45
+ bytes[3] === 0x38 &&
46
+ (bytes[4] === 0x37 || bytes[4] === 0x39) &&
47
+ bytes[5] === 0x61) {
48
+ return "image/gif";
49
+ }
50
+ if (bytes[0] === 0x52 &&
51
+ bytes[1] === 0x49 &&
52
+ bytes[2] === 0x46 &&
53
+ bytes[3] === 0x46 &&
54
+ bytes[8] === 0x57 &&
55
+ bytes[9] === 0x45 &&
56
+ bytes[10] === 0x42 &&
57
+ bytes[11] === 0x50) {
58
+ return "image/webp";
59
+ }
60
+ return null;
61
+ }
62
+ function readPngDimensions(bytes) {
63
+ // IHDR: width u32BE at 16, height u32BE at 20.
64
+ if (bytes.length < 24)
65
+ return { width: 0, height: 0 };
66
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
67
+ return { width: view.getUint32(16), height: view.getUint32(20) };
68
+ }
69
+ function readGifDimensions(bytes) {
70
+ if (bytes.length < 10)
71
+ return { width: 0, height: 0 };
72
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
73
+ return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
74
+ }
75
+ function readWebpDimensions(bytes) {
76
+ if (bytes.length < 30)
77
+ return { width: 0, height: 0 };
78
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
79
+ const chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
80
+ if (chunk === "VP8X") {
81
+ const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));
82
+ const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));
83
+ return { width, height };
84
+ }
85
+ if (chunk === "VP8 ") {
86
+ // Lossy bitstream: frame tag skips 3 bytes, then 3-byte start code, then dims.
87
+ const width = view.getUint16(26, true) & 0x3fff;
88
+ const height = view.getUint16(28, true) & 0x3fff;
89
+ return { width, height };
90
+ }
91
+ if (chunk === "VP8L") {
92
+ const bits = view.getUint32(21, true);
93
+ const width = (bits & 0x3fff) + 1;
94
+ const height = ((bits >> 14) & 0x3fff) + 1;
95
+ return { width, height };
96
+ }
97
+ return { width: 0, height: 0 };
98
+ }
99
+ function readJpegDimensions(bytes) {
100
+ // Walk SOF markers; height/width are u16BE at marker offset +5/+7.
101
+ let index = 2;
102
+ while (index + 9 < bytes.length) {
103
+ if (bytes[index] !== 0xff) {
104
+ index += 1;
105
+ continue;
106
+ }
107
+ const marker = bytes[index + 1];
108
+ if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {
109
+ index += 2;
110
+ continue;
111
+ }
112
+ const length = (bytes[index + 2] << 8) | bytes[index + 3];
113
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
114
+ const height = (bytes[index + 5] << 8) | bytes[index + 6];
115
+ const width = (bytes[index + 7] << 8) | bytes[index + 8];
116
+ return { width, height };
117
+ }
118
+ index += 2 + length;
119
+ }
120
+ return { width: 0, height: 0 };
121
+ }
122
+ function readImageHeader(bytes, mimeType) {
123
+ let dims = { width: 0, height: 0 };
124
+ switch (mimeType) {
125
+ case "image/png":
126
+ dims = readPngDimensions(bytes);
127
+ break;
128
+ case "image/gif":
129
+ dims = readGifDimensions(bytes);
130
+ break;
131
+ case "image/webp":
132
+ dims = readWebpDimensions(bytes);
133
+ break;
134
+ case "image/jpeg":
135
+ dims = readJpegDimensions(bytes);
136
+ break;
137
+ default:
138
+ break;
139
+ }
140
+ return { mimeType, ...dims };
141
+ }
142
+ /**
143
+ * Validate raw image bytes and build the attachment record. Throws
144
+ * {@link PromptAttachmentError} with a stable code so callers can surface a
145
+ * precise message instead of silently dropping the paste.
146
+ */
147
+ export function createPromptImageAttachment(bytes, source, limits = {}) {
148
+ const maxImageBytes = limits.maxImageBytes ?? PROMPT_ATTACHMENT_LIMITS.maxImageBytes;
149
+ const maxPixels = limits.maxPixels ?? PROMPT_ATTACHMENT_LIMITS.maxPixels;
150
+ if (bytes.length === 0) {
151
+ throw new PromptAttachmentError("empty", "Clipboard image is empty.");
152
+ }
153
+ if (bytes.length > maxImageBytes) {
154
+ throw new PromptAttachmentError("too-large", `Image is ${(bytes.length / (1024 * 1024)).toFixed(1)} MiB — limit is ${(maxImageBytes / (1024 * 1024)).toFixed(0)} MiB.`);
155
+ }
156
+ const mimeType = sniffImageMimeType(bytes);
157
+ if (!mimeType) {
158
+ throw new PromptAttachmentError("unsupported", "Clipboard content is not a supported image (PNG, JPEG, WebP, GIF).");
159
+ }
160
+ const header = readImageHeader(bytes, mimeType);
161
+ const pixels = header.width * header.height;
162
+ if (pixels > maxPixels) {
163
+ throw new PromptAttachmentError("too-many-pixels", `Image is ${header.width}×${header.height} (${(pixels / 1_000_000).toFixed(0)} MP) — limit is ${(maxPixels / 1_000_000).toFixed(0)} MP.`);
164
+ }
165
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
166
+ return {
167
+ id: randomUUID(),
168
+ kind: "image",
169
+ source,
170
+ mimeType,
171
+ byteLength: bytes.length,
172
+ width: header.width,
173
+ height: header.height,
174
+ sha256,
175
+ createdAt: Date.now(),
176
+ };
177
+ }
178
+ /** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */
179
+ export function describePromptImageAttachment(attachment) {
180
+ const dims = attachment.width > 0 && attachment.height > 0 ? `${attachment.width}×${attachment.height} · ` : "";
181
+ const kib = attachment.byteLength / 1024;
182
+ const size = kib >= 1024 ? `${(kib / 1024).toFixed(1)} MiB` : `${Math.round(kib)} KiB`;
183
+ const mime = attachment.mimeType.replace("image/", "").toUpperCase();
184
+ return `${dims}${mime} · ${size}`;
185
+ }
186
+ //# sourceMappingURL=prompt-attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-attachment.js","sourceRoot":"","sources":["../../src/core/prompt-attachment.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First-class prompt image attachments.\n *\n * Pure validation/metadata layer between clipboard/file acquisition and\n * `AgentSession.prompt(text, { images })`. Attachments are content-addressed\n * (sha256), validated by magic bytes — clipboard MIME metadata is never\n * trusted — and carry decoded dimensions for preview and pixel-budget checks.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\n\nexport type PromptAttachmentSource = \"clipboard\" | \"file\" | \"browser-capture\";\n\nexport const PROMPT_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\nexport type PromptImageMimeType = (typeof PROMPT_IMAGE_MIME_TYPES)[number];\n\nexport interface PromptImageAttachment {\n\treadonly id: string;\n\treadonly kind: \"image\";\n\treadonly source: PromptAttachmentSource;\n\treadonly mimeType: PromptImageMimeType;\n\treadonly byteLength: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly width: number;\n\t/** 0 when dimensions cannot be parsed from the container header. */\n\treadonly height: number;\n\treadonly sha256: string;\n\treadonly createdAt: number;\n}\n\n/** Defaults from the attachment roadmap (§validation). */\nexport const PROMPT_ATTACHMENT_LIMITS = {\n\tmaxAttachments: 8,\n\tmaxImageBytes: 10 * 1024 * 1024,\n\tmaxDraftBytes: 32 * 1024 * 1024,\n\tmaxPixels: 40_000_000,\n} as const;\n\nexport type PromptAttachmentValidationCode =\n\t| \"empty\"\n\t| \"unsupported\"\n\t| \"too-large\"\n\t| \"too-many-pixels\"\n\t| \"limit-reached\";\n\nexport class PromptAttachmentError extends Error {\n\treadonly code: PromptAttachmentValidationCode;\n\n\tconstructor(code: PromptAttachmentValidationCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"PromptAttachmentError\";\n\t\tthis.code = code;\n\t}\n}\n\ninterface ImageHeaderInfo {\n\tmimeType: PromptImageMimeType;\n\twidth: number;\n\theight: number;\n}\n\n/** Detect the image container from magic bytes. Clipboard metadata is not trusted. */\nexport function sniffImageMimeType(bytes: Uint8Array): PromptImageMimeType | null {\n\tif (bytes.length < 12) return null;\n\tif (\n\t\tbytes[0] === 0x89 &&\n\t\tbytes[1] === 0x50 &&\n\t\tbytes[2] === 0x4e &&\n\t\tbytes[3] === 0x47 &&\n\t\tbytes[4] === 0x0d &&\n\t\tbytes[5] === 0x0a &&\n\t\tbytes[6] === 0x1a &&\n\t\tbytes[7] === 0x0a\n\t) {\n\t\treturn \"image/png\";\n\t}\n\tif (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return \"image/jpeg\";\n\tif (\n\t\tbytes[0] === 0x47 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x38 &&\n\t\t(bytes[4] === 0x37 || bytes[4] === 0x39) &&\n\t\tbytes[5] === 0x61\n\t) {\n\t\treturn \"image/gif\";\n\t}\n\tif (\n\t\tbytes[0] === 0x52 &&\n\t\tbytes[1] === 0x49 &&\n\t\tbytes[2] === 0x46 &&\n\t\tbytes[3] === 0x46 &&\n\t\tbytes[8] === 0x57 &&\n\t\tbytes[9] === 0x45 &&\n\t\tbytes[10] === 0x42 &&\n\t\tbytes[11] === 0x50\n\t) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nfunction readPngDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// IHDR: width u32BE at 16, height u32BE at 20.\n\tif (bytes.length < 24) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint32(16), height: view.getUint32(20) };\n}\n\nfunction readGifDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 10) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\treturn { width: view.getUint16(6, true), height: view.getUint16(8, true) };\n}\n\nfunction readWebpDimensions(bytes: Uint8Array): { width: number; height: number } {\n\tif (bytes.length < 30) return { width: 0, height: 0 };\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n\tconst chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);\n\tif (chunk === \"VP8X\") {\n\t\tconst width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16));\n\t\tconst height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16));\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8 \") {\n\t\t// Lossy bitstream: frame tag skips 3 bytes, then 3-byte start code, then dims.\n\t\tconst width = view.getUint16(26, true) & 0x3fff;\n\t\tconst height = view.getUint16(28, true) & 0x3fff;\n\t\treturn { width, height };\n\t}\n\tif (chunk === \"VP8L\") {\n\t\tconst bits = view.getUint32(21, true);\n\t\tconst width = (bits & 0x3fff) + 1;\n\t\tconst height = ((bits >> 14) & 0x3fff) + 1;\n\t\treturn { width, height };\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readJpegDimensions(bytes: Uint8Array): { width: number; height: number } {\n\t// Walk SOF markers; height/width are u16BE at marker offset +5/+7.\n\tlet index = 2;\n\twhile (index + 9 < bytes.length) {\n\t\tif (bytes[index] !== 0xff) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst marker = bytes[index + 1];\n\t\tif (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n\t\t\tindex += 2;\n\t\t\tcontinue;\n\t\t}\n\t\tconst length = (bytes[index + 2] << 8) | bytes[index + 3];\n\t\tif (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {\n\t\t\tconst height = (bytes[index + 5] << 8) | bytes[index + 6];\n\t\t\tconst width = (bytes[index + 7] << 8) | bytes[index + 8];\n\t\t\treturn { width, height };\n\t\t}\n\t\tindex += 2 + length;\n\t}\n\treturn { width: 0, height: 0 };\n}\n\nfunction readImageHeader(bytes: Uint8Array, mimeType: PromptImageMimeType): ImageHeaderInfo {\n\tlet dims = { width: 0, height: 0 };\n\tswitch (mimeType) {\n\t\tcase \"image/png\":\n\t\t\tdims = readPngDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/gif\":\n\t\t\tdims = readGifDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/webp\":\n\t\t\tdims = readWebpDimensions(bytes);\n\t\t\tbreak;\n\t\tcase \"image/jpeg\":\n\t\t\tdims = readJpegDimensions(bytes);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tbreak;\n\t}\n\treturn { mimeType, ...dims };\n}\n\nexport interface PromptAttachmentLimits {\n\tmaxImageBytes?: number;\n\tmaxPixels?: number;\n}\n\n/**\n * Validate raw image bytes and build the attachment record. Throws\n * {@link PromptAttachmentError} with a stable code so callers can surface a\n * precise message instead of silently dropping the paste.\n */\nexport function createPromptImageAttachment(\n\tbytes: Uint8Array,\n\tsource: PromptAttachmentSource,\n\tlimits: PromptAttachmentLimits = {},\n): PromptImageAttachment {\n\tconst maxImageBytes = limits.maxImageBytes ?? PROMPT_ATTACHMENT_LIMITS.maxImageBytes;\n\tconst maxPixels = limits.maxPixels ?? PROMPT_ATTACHMENT_LIMITS.maxPixels;\n\n\tif (bytes.length === 0) {\n\t\tthrow new PromptAttachmentError(\"empty\", \"Clipboard image is empty.\");\n\t}\n\tif (bytes.length > maxImageBytes) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-large\",\n\t\t\t`Image is ${(bytes.length / (1024 * 1024)).toFixed(1)} MiB — limit is ${(maxImageBytes / (1024 * 1024)).toFixed(0)} MiB.`,\n\t\t);\n\t}\n\tconst mimeType = sniffImageMimeType(bytes);\n\tif (!mimeType) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"unsupported\",\n\t\t\t\"Clipboard content is not a supported image (PNG, JPEG, WebP, GIF).\",\n\t\t);\n\t}\n\tconst header = readImageHeader(bytes, mimeType);\n\tconst pixels = header.width * header.height;\n\tif (pixels > maxPixels) {\n\t\tthrow new PromptAttachmentError(\n\t\t\t\"too-many-pixels\",\n\t\t\t`Image is ${header.width}×${header.height} (${(pixels / 1_000_000).toFixed(0)} MP) — limit is ${(maxPixels / 1_000_000).toFixed(0)} MP.`,\n\t\t);\n\t}\n\tconst sha256 = createHash(\"sha256\").update(bytes).digest(\"hex\");\n\treturn {\n\t\tid: randomUUID(),\n\t\tkind: \"image\",\n\t\tsource,\n\t\tmimeType,\n\t\tbyteLength: bytes.length,\n\t\twidth: header.width,\n\t\theight: header.height,\n\t\tsha256,\n\t\tcreatedAt: Date.now(),\n\t};\n}\n\n/** Human summary for preview footers: `1280×720 · PNG · 312 KiB`. */\nexport function describePromptImageAttachment(attachment: PromptImageAttachment): string {\n\tconst dims = attachment.width > 0 && attachment.height > 0 ? `${attachment.width}×${attachment.height} · ` : \"\";\n\tconst kib = attachment.byteLength / 1024;\n\tconst size = kib >= 1024 ? `${(kib / 1024).toFixed(1)} MiB` : `${Math.round(kib)} KiB`;\n\tconst mime = attachment.mimeType.replace(\"image/\", \"\").toUpperCase();\n\treturn `${dims}${mime} · ${size}`;\n}\n"]}
@@ -39,6 +39,34 @@ export declare function isContentSafetyStopMessage(text: string | undefined): bo
39
39
  export declare function isQuotaExhaustionMessage(text: string | undefined): boolean;
40
40
  /** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */
41
41
  export declare function isOrphanToolCallIdError(text: string | undefined): boolean;
42
+ /**
43
+ * Transient upstream availability failures: gateway 5xx passthroughs
44
+ * ("503 Upstream request failed", "Endpoint is unavailable") and dropped
45
+ * streams that never carried a finish reason ("Stream ended without
46
+ * finish_reason"). Transport problems, not transcript-shape problems — they
47
+ * heal by backoff/retry or switching model, never by transcript sanitization.
48
+ */
49
+ export declare function isUpstreamUnavailableMessage(text: string | undefined): boolean;
50
+ /** Family key for a model route, or undefined when it has no known alias group. */
51
+ export declare function modelRouteFamily(model: {
52
+ readonly provider?: string | undefined;
53
+ readonly id: string;
54
+ readonly name?: string | undefined;
55
+ }): string | undefined;
56
+ /**
57
+ * Alternative provider routes serving the same model family as
58
+ * {@link failed}, in registry order, excluding the failed route itself.
59
+ * Callers filter by authentication and per-turn failure bookkeeping.
60
+ */
61
+ export declare function sameModelRouteCandidates<T extends {
62
+ readonly provider: string;
63
+ readonly id: string;
64
+ readonly name?: string | undefined;
65
+ }>(failed: {
66
+ readonly provider: string;
67
+ readonly id: string;
68
+ readonly name?: string | undefined;
69
+ }, available: readonly T[]): T[];
42
70
  /**
43
71
  * Errors the agent loop may auto-retry (after optional failover / message sanitize).
44
72
  * Kept in one place so agent-session and tests share the same contract.
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
1
+ {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Claude lines that must stay on-model when they refuse. No DeepSeek hop. */\nexport const NO_SAFETY_FAILOVER_MODEL_RE = /(?:claude-)?(?:fable|opus|sonnet)/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"xai\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\n/** Fable / Opus / Sonnet stay on the requested id after a safety stop. */\nexport function isNoSafetyFailoverModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (NO_SAFETY_FAILOVER_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && NO_SAFETY_FAILOVER_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Billing/quota exhaustion (\"usage limit for this billing cycle\",\n * \"insufficient_quota\", zero balance, ...). Same-model retry is useless until\n * the cycle resets, but the turn can still be saved by failing over to another\n * candidate — so these classify as retryable-with-failover, not auth-fatal.\n */\nexport function isQuotaExhaustionMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /usage limit|GoUsageLimitError|FreeUsageLimitError|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Transient upstream availability failures: gateway 5xx passthroughs\n * (\"503 Upstream request failed\", \"Endpoint is unavailable\") and dropped\n * streams that never carried a finish reason (\"Stream ended without\n * finish_reason\"). Transport problems, not transcript-shape problems — they\n * heal by backoff/retry or switching model, never by transcript sanitization.\n */\nexport function isUpstreamUnavailableMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /upstream(?: request)? failed|upstream connect|endpoint (?:is )?unavailable|stream ended without finish_reason|\\b50[0234]\\b/i.test(\n\t\ttext,\n\t);\n}\n\n/**\n * Route families: the same underlying model served by several provider\n * routes. Matched on the model id OR display name so alias routes like\n * `openrouter/stealth/ox-alpha`, `opencode-go/ox-alpha-free`, and\n * `opencode/x-preview-f-free` (\"Ox Alpha Free (Unlimited)\") rotate as one\n * ox-alpha family.\n */\nconst MODEL_ROUTE_FAMILIES: readonly { readonly pattern: RegExp; readonly family: string }[] = [\n\t{ pattern: /(?:^|[-_/])ox[-_ ]?alpha(?:[-_/\\s(]|$)/i, family: \"ox-alpha\" },\n];\n\n/** Family key for a model route, or undefined when it has no known alias group. */\nexport function modelRouteFamily(model: {\n\treadonly provider?: string | undefined;\n\treadonly id: string;\n\treadonly name?: string | undefined;\n}): string | undefined {\n\tfor (const { pattern, family } of MODEL_ROUTE_FAMILIES) {\n\t\tif (pattern.test(model.id)) return family;\n\t\tif (model.name && pattern.test(model.name)) return family;\n\t}\n\treturn undefined;\n}\n\n/**\n * Alternative provider routes serving the same model family as\n * {@link failed}, in registry order, excluding the failed route itself.\n * Callers filter by authentication and per-turn failure bookkeeping.\n */\nexport function sameModelRouteCandidates<\n\tT extends { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n>(\n\tfailed: { readonly provider: string; readonly id: string; readonly name?: string | undefined },\n\tavailable: readonly T[],\n): T[] {\n\tconst family = modelRouteFamily(failed);\n\tif (!family) return [];\n\tconst candidates: T[] = [];\n\tfor (const model of available) {\n\t\tif (model.provider === failed.provider && model.id === failed.id) continue;\n\t\tif (modelRouteFamily(model) === family) candidates.push(model);\n\t}\n\treturn candidates;\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error|json error injected into sse stream|injected into sse/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\n/** Explicit `--model` / `--provider` pins stay on the requested model. */\nexport function shouldHonorSafetyFailover(options: {\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly modelPinned: boolean;\n\treadonly modelId?: string;\n\treadonly provider?: string;\n}): boolean {\n\tif (!options.autoFailoverOnSafetyStop || options.modelPinned) return false;\n\treturn !isNoSafetyFailoverModel(options.modelId, options.provider);\n}\n\n/** Sticky-model eject still applies unless the user pinned that model. */\nexport function shouldEjectStickySafetyModel(options: {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly modelPinned: boolean;\n}): boolean {\n\treturn options.blockStickySafetyModels && !options.modelPinned;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
@@ -65,6 +65,56 @@ export function isOrphanToolCallIdError(text) {
65
65
  return false;
66
66
  return /tool_call_id\s+is\s+not\s+found|tool_call_id\s+not\s+found|unknown\s+tool_call_id/i.test(text);
67
67
  }
68
+ /**
69
+ * Transient upstream availability failures: gateway 5xx passthroughs
70
+ * ("503 Upstream request failed", "Endpoint is unavailable") and dropped
71
+ * streams that never carried a finish reason ("Stream ended without
72
+ * finish_reason"). Transport problems, not transcript-shape problems — they
73
+ * heal by backoff/retry or switching model, never by transcript sanitization.
74
+ */
75
+ export function isUpstreamUnavailableMessage(text) {
76
+ if (!text)
77
+ return false;
78
+ return /upstream(?: request)? failed|upstream connect|endpoint (?:is )?unavailable|stream ended without finish_reason|\b50[0234]\b/i.test(text);
79
+ }
80
+ /**
81
+ * Route families: the same underlying model served by several provider
82
+ * routes. Matched on the model id OR display name so alias routes like
83
+ * `openrouter/stealth/ox-alpha`, `opencode-go/ox-alpha-free`, and
84
+ * `opencode/x-preview-f-free` ("Ox Alpha Free (Unlimited)") rotate as one
85
+ * ox-alpha family.
86
+ */
87
+ const MODEL_ROUTE_FAMILIES = [
88
+ { pattern: /(?:^|[-_/])ox[-_ ]?alpha(?:[-_/\s(]|$)/i, family: "ox-alpha" },
89
+ ];
90
+ /** Family key for a model route, or undefined when it has no known alias group. */
91
+ export function modelRouteFamily(model) {
92
+ for (const { pattern, family } of MODEL_ROUTE_FAMILIES) {
93
+ if (pattern.test(model.id))
94
+ return family;
95
+ if (model.name && pattern.test(model.name))
96
+ return family;
97
+ }
98
+ return undefined;
99
+ }
100
+ /**
101
+ * Alternative provider routes serving the same model family as
102
+ * {@link failed}, in registry order, excluding the failed route itself.
103
+ * Callers filter by authentication and per-turn failure bookkeeping.
104
+ */
105
+ export function sameModelRouteCandidates(failed, available) {
106
+ const family = modelRouteFamily(failed);
107
+ if (!family)
108
+ return [];
109
+ const candidates = [];
110
+ for (const model of available) {
111
+ if (model.provider === failed.provider && model.id === failed.id)
112
+ continue;
113
+ if (modelRouteFamily(model) === family)
114
+ candidates.push(model);
115
+ }
116
+ return candidates;
117
+ }
68
118
  /**
69
119
  * Errors the agent loop may auto-retry (after optional failover / message sanitize).
70
120
  * Kept in one place so agent-session and tests share the same contract.