@visual-z/xeno 0.8.4 → 0.9.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 (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,165 +0,0 @@
1
- /**
2
- * Skill validation and the frozen-prefix fragment.
3
- *
4
- * A skill is a text resource, not a tool. The system prompt gets the name
5
- * and description so the model can decide whether to read the file; the
6
- * body stays out of the prefix because the prefix is sent on every request
7
- * (feat-skills-and-prompts design; Agent Skills progressive disclosure).
8
- */
9
-
10
- import { parseFrontmatter } from "./frontmatter.ts";
11
- import type { LogEvent } from "./events.ts";
12
-
13
- /** Agent Skills name rule. Assumed from the spec, not measured. */
14
- const MAX_NAME_LENGTH = 64;
15
-
16
- /**
17
- * 1–64 chars, lowercase / digits / hyphens, no leading, trailing, or
18
- * doubled hyphen. A name that fails this is skipped, not coerced — an
19
- * auto-slug would make two different directories collide silently.
20
- */
21
- const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
22
-
23
- export interface Skill {
24
- readonly name: string;
25
- readonly description: string;
26
- readonly filePath: string;
27
- readonly body: string;
28
- readonly disableModelInvocation: boolean;
29
- readonly userInvocable?: boolean;
30
- readonly context?: "fork";
31
- readonly allowedTools?: readonly string[];
32
- readonly disallowedTools?: readonly string[];
33
- /** Stored outside the model prefix so resource reads reproduce the loaded bytes. */
34
- readonly sourceText?: string;
35
- }
36
-
37
- export type LoadedSkill = { name: string; path: string; sha256: string; artifact: string;
38
- mode: "inline" | "fork" | "read"; origin: "user" | "model"; node: number; event: string };
39
-
40
- /** O.12: explicit capability intersection, never an approval grant. */
41
- export function skillTools(parent: readonly string[], skill: Skill): string[] {
42
- return parent.filter(name => (skill.allowedTools === undefined || skill.allowedTools.includes(name)) && !skill.disallowedTools?.includes(name));
43
- }
44
-
45
- export function deriveLoadedSkills(previous: readonly LoadedSkill[], events: readonly LogEvent[], node: number): LoadedSkill[] {
46
- const loaded = new Map(previous.map(item => [item.path, item]));
47
- for (const e of events) if (e.t === "skill_loaded") loaded.set(e.path, {
48
- name: e.name, path: e.path, sha256: e.sha256, artifact: e.artifact, mode: e.mode, origin: e.origin, node, event: e.id,
49
- });
50
- return [...loaded.values()];
51
- }
52
-
53
- export function isValidSkillName(name: string): boolean {
54
- return name.length >= 1 && name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);
55
- }
56
-
57
- export function parseSkill(input: {
58
- readonly filePath: string;
59
- readonly parentDirName: string;
60
- readonly text: string;
61
- }): { skill: Skill | null; diagnostics: readonly string[] } {
62
- const parsed = parseFrontmatter(input.text);
63
- const diagnostics = [...parsed.diagnostics];
64
- const supported = ["name", "description", "disable-model-invocation", "user-invocable", "context", "allowed-tools", "disallowed-tools", "license", "compatibility"];
65
- for (const key of Object.keys(parsed.fields)) if (!supported.includes(key)) diagnostics.push(`${input.filePath}: unsupported skill field ${key}`);
66
- for (const key of ["disable-model-invocation", "user-invocable"]) {
67
- if (parsed.fields[key] !== undefined && typeof parsed.fields[key] !== "boolean") diagnostics.push(`${input.filePath}: ${key} must be boolean`);
68
- }
69
- if (parsed.fields["context"] !== undefined && parsed.fields["context"] !== "fork") diagnostics.push(`${input.filePath}: context must be fork`);
70
- const toolNames = (key: string): string[] | undefined => {
71
- const value = parsed.fields[key];
72
- if (value === undefined) return undefined;
73
- const aliases: Record<string, string> = { Read: "read", Write: "write", Edit: "edit", Grep: "grep", Glob: "find", Bash: "bash" };
74
- const names = Array.isArray(value) ? value : typeof value === "string" ? value.split(/[\s,]+/).filter(Boolean) : [];
75
- if ((names.length === 0 && value !== "") || names.some(name => !/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))) diagnostics.push(`${input.filePath}: ${key} requires explicit tool names; permission patterns are unsupported`);
76
- return [...new Set(names.map(name => aliases[name] ?? name))];
77
- };
78
- const allowedTools = toolNames("allowed-tools"), disallowedTools = toolNames("disallowed-tools");
79
- if (diagnostics.length > 0) return { skill: null, diagnostics };
80
- const desc = parsed.fields["description"];
81
- const description = typeof desc === "string" ? desc.trim() : "";
82
- if (description.length === 0) {
83
- diagnostics.push(`${input.filePath}: missing description`);
84
- return { skill: null, diagnostics };
85
- }
86
-
87
- const named = parsed.fields["name"];
88
- const name = typeof named === "string" && named.trim().length > 0
89
- ? named.trim()
90
- : input.parentDirName;
91
- if (!isValidSkillName(name)) {
92
- diagnostics.push(`${input.filePath}: name "${name}" is not a valid skill name`);
93
- return { skill: null, diagnostics };
94
- }
95
-
96
- const disable = parsed.fields["disable-model-invocation"] === true;
97
- return {
98
- skill: {
99
- name,
100
- description,
101
- filePath: input.filePath,
102
- body: parsed.body,
103
- disableModelInvocation: disable,
104
- userInvocable: parsed.fields["user-invocable"] !== false,
105
- sourceText: input.text,
106
- ...(parsed.fields["context"] === "fork" ? { context: "fork" as const } : {}),
107
- ...(allowedTools ? { allowedTools } : {}),
108
- ...(disallowedTools ? { disallowedTools } : {}),
109
- },
110
- diagnostics,
111
- };
112
- }
113
-
114
- /**
115
- * XML fragment per the Agent Skills integrate-skills shape, so a skill
116
- * written for pi is recognised here. Location is the path the model should
117
- * `read`; the body is deliberately absent.
118
- */
119
- export function formatSkillsForPrompt(skills: readonly Skill[]): string {
120
- const visible = skills.filter((s) => !s.disableModelInvocation);
121
- if (visible.length === 0) return "";
122
- const lines = [
123
- "The following skills provide specialized instructions for specific tasks.",
124
- "Use the read tool to load a skill's file when the task matches its description.",
125
- "When a skill file references a relative path, resolve it against the skill directory.",
126
- "",
127
- "<available_skills>",
128
- ];
129
- for (const skill of visible) {
130
- lines.push(" <skill>");
131
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
132
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
133
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
134
- lines.push(" </skill>");
135
- }
136
- lines.push("</available_skills>");
137
- return lines.join("\n");
138
- }
139
-
140
- /** Append the skills section without touching the context-file assembly. */
141
- export function appendSkillsToSystem(system: string, skills: readonly Skill[]): string {
142
- const section = formatSkillsForPrompt(skills);
143
- return section.length === 0 ? system : `${system}\n\n${section}`;
144
- }
145
-
146
- /**
147
- * `/skill:name` is the user forcing a read. Wrapping the body with the
148
- * path lets relative references inside the skill resolve the same way a
149
- * model-issued `read` would.
150
- */
151
- export function skillInvocationMessage(skill: Skill, args: string): string {
152
- const wrapped =
153
- `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">\n` +
154
- `${skill.body}\n` +
155
- `</skill>`;
156
- return args.length > 0 ? `${wrapped}\n\nUser: ${args}` : wrapped;
157
- }
158
-
159
- function escapeXml(s: string): string {
160
- return s
161
- .replace(/&/g, "&amp;")
162
- .replace(/</g, "&lt;")
163
- .replace(/>/g, "&gt;")
164
- .replace(/"/g, "&quot;");
165
- }
@@ -1,211 +0,0 @@
1
- /** Q.17: an optional syntax for DataQuery, never an alternate execution path. */
2
- import { parseDataQuery, type DataQuery } from "./data-query.ts";
3
- import { isRecord } from "./event-validation.ts";
4
-
5
- // Assumed synchronous parsing budgets, not measured model or database limits.
6
- // Reject over-budget input before lexing; never execute a parsed prefix.
7
- export const MAX_SQL_CHARS = 16_384;
8
- export const MAX_SQL_TOKENS = 4_096;
9
- export const SQL_QUERY_HELP = "SQL-style subset: SELECT * or fields/COUNT/SUM/AVG/MIN/MAX(field) AS alias FROM source; optional WHERE comparisons, IS [NOT] NULL or CONTAINS(field, literal), joined by AND; GROUP BY; ORDER BY field [ASC|DESC]. No comments, joins, subqueries, OR, HAVING, DISTINCT, LIMIT/OFFSET or multiple statements. Use artifact pages for large results.";
10
-
11
- type Token = { kind: "word" | "identifier" | "string" | "number" | "symbol" | "end"; text: string; at: number };
12
- type Aggregate = NonNullable<DataQuery["aggregate"]>[number];
13
- type Predicate = NonNullable<DataQuery["where"]>[number];
14
- const OPERATIONS = ["count", "sum", "avg", "min", "max"] as const;
15
- const RESERVED = new Set(("SELECT FROM SOURCE WHERE GROUP BY ORDER ASC DESC AS AND IS NOT NULL TRUE FALSE CONTAINS " +
16
- "COUNT SUM AVG MIN MAX DISTINCT LIMIT OFFSET HAVING JOIN INNER LEFT RIGHT OUTER ON UNION WITH OR IN LIKE BETWEEN " +
17
- "INSERT UPDATE DELETE DROP CREATE ALTER INTO VALUES RETURNING").split(" "));
18
- const JSON_OPERATORS = ["where", "select", "group_by", "aggregate", "order_by"] as const;
19
-
20
- function sqlError(at: number, message: string): never {
21
- // Locations refer to UTF-16 offsets in the supplied string, without echoing data literals.
22
- throw new Error("SQL at offset " + at + ": " + message + ". " + SQL_QUERY_HELP);
23
- }
24
- function tokenize(sql: string): Token[] {
25
- if (sql.length > MAX_SQL_CHARS) sqlError(MAX_SQL_CHARS, "input exceeds " + MAX_SQL_CHARS + " characters");
26
- const tokens: Token[] = [];
27
- let at = 0;
28
- const emit = (kind: Token["kind"], text: string, start: number) => {
29
- if (tokens.length >= MAX_SQL_TOKENS) sqlError(start, "input exceeds " + MAX_SQL_TOKENS + " tokens");
30
- tokens.push({ kind, text, at: start });
31
- };
32
- while (at < sql.length) {
33
- const c = sql[at]!;
34
- if (/\s/.test(c)) { at++; continue; }
35
- const start = at;
36
- if (c === "'" || c === '"') {
37
- const quote = c;
38
- let value = "", closed = false;
39
- at++;
40
- while (at < sql.length) {
41
- const next = sql[at++]!;
42
- if (next !== quote) value += next;
43
- else if (sql[at] === quote) { value += quote; at++; }
44
- else { closed = true; break; }
45
- }
46
- if (!closed) sqlError(start, "unterminated quoted value");
47
- emit(quote === "'" ? "string" : "identifier", value, start);
48
- } else if (/[A-Za-z_]/.test(c)) {
49
- at++;
50
- while (at < sql.length && /[A-Za-z0-9_$]/.test(sql[at]!)) at++;
51
- emit("word", sql.slice(start, at), start);
52
- } else if (/[0-9+-]/.test(c) || (c === "." && /[0-9]/.test(sql[at + 1] ?? ""))) {
53
- const numeric = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/.exec(sql.slice(at));
54
- if (!numeric || !Number.isFinite(Number(numeric[0]))) sqlError(start, "expected a finite numeric literal; comments are unsupported");
55
- at += numeric[0].length;
56
- emit("number", numeric[0], start);
57
- } else {
58
- const pair = sql.slice(at, at + 2);
59
- if (["<=", ">=", "!=", "<>"].includes(pair)) { at += 2; emit("symbol", pair, start); }
60
- else if (",().*;=<>".includes(c)) { at++; emit("symbol", c, start); }
61
- else sqlError(start, "unsupported character");
62
- }
63
- }
64
- tokens.push({ kind: "end", text: "", at: sql.length });
65
- return tokens;
66
- }
67
-
68
- class SqlParser {
69
- private readonly tokens: readonly Token[];
70
- private index = 0;
71
- constructor(sql: string) { this.tokens = tokenize(sql); }
72
- private get current(): Token { return this.tokens[this.index]!; }
73
- private isWord(word: string): boolean { return this.current.kind === "word" && this.current.text.toUpperCase() === word; }
74
- private word(word: string): boolean {
75
- if (!this.isWord(word)) return false;
76
- this.index++; return true;
77
- }
78
- private symbol(text: string): boolean {
79
- if (this.current.kind !== "symbol" || this.current.text !== text) return false;
80
- this.index++; return true;
81
- }
82
- private requireWord(word: string): void {
83
- if (!this.word(word)) sqlError(this.current.at, "expected " + word);
84
- }
85
- private requireSymbol(text: string): void {
86
- if (!this.symbol(text)) sqlError(this.current.at, "expected " + text);
87
- }
88
- private identifier(): string {
89
- const token = this.current;
90
- if (token.kind !== "identifier" && (token.kind !== "word" || RESERVED.has(token.text.toUpperCase()))) {
91
- sqlError(token.at, 'expected a field or alias; quote reserved names with double quotes');
92
- }
93
- if (!token.text) sqlError(token.at, "field and alias names must be nonempty");
94
- this.index++;
95
- return token.text;
96
- }
97
- private field(): string {
98
- const parts = [this.identifier()];
99
- while (this.symbol(".")) parts.push(this.identifier());
100
- return parts.join(".");
101
- }
102
- private fields(): string[] {
103
- const fields = [this.field()];
104
- while (this.symbol(",")) fields.push(this.field());
105
- return fields;
106
- }
107
- private literal(): string | number | boolean {
108
- const token = this.current;
109
- if (token.kind === "number") { this.index++; return Number(token.text); }
110
- if (token.kind === "string") { this.index++; return token.text; }
111
- if (this.word("TRUE")) return true;
112
- if (this.word("FALSE")) return false;
113
- sqlError(token.at, "expected a string, finite number or boolean literal; use IS NULL for null checks");
114
- }
115
- private predicate(): Predicate {
116
- if (this.word("CONTAINS")) {
117
- this.requireSymbol("(");
118
- const field = this.field();
119
- this.requireSymbol(",");
120
- const value = this.literal();
121
- this.requireSymbol(")");
122
- return { field, op: "contains", value };
123
- }
124
- const field = this.field();
125
- if (this.word("IS")) {
126
- const not = this.word("NOT");
127
- this.requireWord("NULL");
128
- return { field, op: not ? "ne" : "eq", value: null };
129
- }
130
- const token = this.current;
131
- const ops: Record<string, Predicate["op"]> = { "=": "eq", "!=": "ne", "<>": "ne", "<": "lt", "<=": "lte", ">": "gt", ">=": "gte" };
132
- const op = token.kind === "symbol" ? ops[token.text] : undefined;
133
- if (op === undefined) sqlError(token.at, "expected a comparison operator or IS [NOT] NULL");
134
- this.index++;
135
- return { field, op, value: this.literal() };
136
- }
137
- parse(source: Pick<DataQuery, "format" | "pointer">): DataQuery {
138
- this.requireWord("SELECT");
139
- const wildcard = this.symbol("*");
140
- const selected: string[] = [], fields: string[] = [], aggregates: Aggregate[] = [];
141
- if (!wildcard) {
142
- do {
143
- const token = this.current;
144
- const op = OPERATIONS.find(value => token.kind === "word" && value.toUpperCase() === token.text.toUpperCase());
145
- if (op !== undefined) {
146
- this.index++;
147
- this.requireSymbol("(");
148
- const star = this.symbol("*");
149
- if (star && op !== "count") sqlError(token.at, "only COUNT accepts *");
150
- const field = star ? undefined : this.field();
151
- this.requireSymbol(")"); this.requireWord("AS");
152
- const alias = this.identifier();
153
- aggregates.push({ op, ...(field === undefined ? {} : { field }), as: alias });
154
- selected.push(alias);
155
- } else {
156
- const field = this.field();
157
- selected.push(field); fields.push(field);
158
- }
159
- } while (this.symbol(","));
160
- }
161
- this.requireWord("FROM"); this.requireWord("SOURCE");
162
- const query: DataQuery = { ...source, ...(!wildcard ? { select: selected } : {}) };
163
- if (this.word("WHERE")) {
164
- const where = [this.predicate()];
165
- while (this.word("AND")) where.push(this.predicate());
166
- query.where = where;
167
- }
168
- if (this.word("GROUP")) { this.requireWord("BY"); query.group_by = this.fields(); }
169
- if (this.word("ORDER")) {
170
- this.requireWord("BY");
171
- const order: NonNullable<DataQuery["order_by"]>[number][] = [];
172
- do {
173
- const field = this.field(), descending = this.word("DESC");
174
- if (!descending) this.word("ASC");
175
- order.push({ field, ...(descending ? { descending: true } : {}) });
176
- } while (this.symbol(","));
177
- query.order_by = order;
178
- }
179
- this.symbol(";");
180
- if (this.current.kind !== "end") sqlError(this.current.at, "unexpected clause or additional statement");
181
- if (aggregates.length) {
182
- query.aggregate = aggregates;
183
- const grouped = query.group_by ?? [];
184
- if (fields.length !== grouped.length || fields.some(field => !grouped.includes(field))) {
185
- sqlError(this.current.at, "GROUP BY must match the selected non-aggregate fields");
186
- }
187
- if (query.order_by?.some(order => !selected.includes(order.field))) {
188
- sqlError(this.current.at, "aggregate ORDER BY must name a selected group field or aggregate alias");
189
- }
190
- }
191
- // The existing algebra owns arity, duplicate names, and grouping invariants.
192
- try { return parseDataQuery(query); }
193
- catch (error) { return sqlError(this.current.at, String(error)); }
194
- }
195
- }
196
-
197
- /** Compiles one finite statement. It neither opens sources nor evaluates code. */
198
- export function compileSqlQuery(sql: string, source: Pick<DataQuery, "format" | "pointer">): DataQuery {
199
- return new SqlParser(sql).parse(parseDataQuery(source));
200
- }
201
-
202
- /** Both syntaxes converge before source I/O, permissions, execution and archival. */
203
- export function parseQueryInput(input: unknown): DataQuery {
204
- if (!isRecord(input) || !Object.hasOwn(input, "sql")) return parseDataQuery(input);
205
- if (typeof input["sql"] !== "string") throw new Error("sql must be a string");
206
- if (JSON_OPERATORS.some(key => Object.hasOwn(input, key))) {
207
- throw new Error("sql cannot be combined with where, select, group_by, aggregate or order_by");
208
- }
209
- const source = parseDataQuery({ format: input["format"], ...(input["pointer"] === undefined ? {} : { pointer: input["pointer"] }) });
210
- return compileSqlQuery(input["sql"], source);
211
- }
package/src/core/stall.ts DELETED
@@ -1,128 +0,0 @@
1
- /** N.9: restartable progress accounting independent of model-context boundaries. */
2
- import type { LogEvent } from "./events.ts";
3
- import { pendingToolCalls } from "./execution.ts";
4
- import { exchangeProgressChecks } from "./evidence.ts";
5
- import { deriveProgress, type ProgressEvidence } from "./progress.ts";
6
- import { isProcessReceipt } from "./process-receipt.ts";
7
- import { canonicalValue } from "./continuation.ts";
8
-
9
- /**
10
- * S.8: the watchdog is now an on/off switch -- 0 disables it, any other
11
- * allowed value enables it. Sessions persist this field (S.3); a stored 12
12
- * still means "on" and is never reread as "twelve repeats".
13
- */
14
- export const DEFAULT_STALL_LIMIT = 12;
15
- /**
16
- * How many times a cycle must repeat. A hypothesis, not measured: OpenCode's
17
- * doom loop, Cline and Roo use 3; OpenHands 4 counting the first, novel,
18
- * observation; Gemini 5. Lower asks too often, higher pays for more repeats.
19
- */
20
- export const CYCLE_REPEATS = 3;
21
- /** Longest cycle recognised; Gemini CLI's bound, and the user's (S.8). */
22
- export const MAX_CYCLE = 5;
23
-
24
- export interface StallExchange { readonly signature: string; readonly progressed: boolean }
25
- export interface StallState {
26
- evidence: readonly ProgressEvidence[];
27
- pending: readonly LogEvent[];
28
- pendingNode: number;
29
- /** The request's latest exchanges, oldest first, at most MAX_CYCLE × CYCLE_REPEATS. */
30
- recent: readonly StallExchange[];
31
- failures: number;
32
- emptyWaits: number;
33
- /** A cycle was already pointed out to the model; the next one goes to the user. */
34
- nudged: boolean;
35
- /** The previous exchange's tool calls, canonical; empty after real user input. */
36
- previous: string;
37
- /**
38
- * A completion review just asked the model to re-read the request and
39
- * verify. Its answer is the harness's own request, so that one exchange
40
- * stays out of the cycle window (fix-review-induced-stall).
41
- */
42
- reviewed: boolean;
43
- }
44
- export function emptyStallState(): StallState {
45
- return { evidence: [], pending: [], pendingNode: 0, recent: [], failures: 0, emptyWaits: 0, nudged: false, previous: "", reviewed: false };
46
- }
47
-
48
- /**
49
- * The shortest period k <= MAX_CYCLE such that the last k × repeats exchanges
50
- * are one k-call cycle repeated, none of which brought new evidence -- or null.
51
- * Distinct calls that each tell nothing new are not a loop: that is reading
52
- * and re-checking, which a cumulative allowance wrongly punished (S.8).
53
- */
54
- export function repeatedCycle(recent: readonly StallExchange[], repeats = CYCLE_REPEATS): number | null {
55
- for (let k = 1; k <= MAX_CYCLE; k++) {
56
- const n = k * repeats;
57
- if (recent.length < n) return null;
58
- const window = recent.slice(-n);
59
- if (window.some(item => item.progressed)) continue;
60
- if (window.every((item, i) => i < k || item.signature === window[i - k]!.signature)) return k;
61
- }
62
- return null;
63
- }
64
-
65
- /** Name and arguments of every call, order-independent: what the model asked for, not what came back. */
66
- function exchangeSignature(assistant: Extract<LogEvent, { t: "assistant" }>): string {
67
- return assistant.blocks.flatMap(block => block.type === "tool_use" ? [`${block.name}:${canonicalValue(block.input)}`] : []).sort().join("\n");
68
- }
69
-
70
- export function finishStallExchange(state: StallState): StallState {
71
- const assistant = state.pending[0];
72
- if (assistant?.t !== "assistant" || pendingToolCalls(state.pending).length > 0) return state;
73
- if (!assistant.blocks.some((block) => block.type === "tool_use")) return { ...state, pending: [] };
74
- const checks = exchangeProgressChecks(state.pending);
75
- const progress = deriveProgress(state.evidence, state.pending, state.pendingNode, checks);
76
- const signature = exchangeSignature(assistant);
77
- const waits = new Set(assistant.blocks.flatMap(block => block.type === "tool_use" && block.name === "write_stdin" ? [block.call_id] : []));
78
- // S.6 item 2: asking
79
- // for exactly what the previous exchange asked for is a rerun on an
80
- // unchanged premise, so output that differs only by timing (a failing test's
81
- // "[123ms]") is not news. Reads and edits are content-addressed and keep
82
- // their own rules; waiting on a running process is not a rerun. Only the
83
- // immediately previous exchange: an edit, in any tool, between two runs is
84
- // what makes the second one worth running (Gemini, Cline, OpenCode and Roo
85
- // detect identical consecutive calls the same way).
86
- const rerun = signature === state.previous
87
- ? deriveProgress(state.evidence, state.pending.filter(event => event.t !== "tool_result" || waits.has(event.call_id) ||
88
- (event.effect?.kind !== "command" && !isProcessReceipt(event.data))), state.pendingNode, checks).progressed
89
- : progress.progressed;
90
- const results = state.pending.filter(event => event.t === "tool_result");
91
- // Waiting on a process that is still running is not another attempt: its
92
- // own timeout bounds it and the user can interrupt (S.8). Only exchanges
93
- // that try something enter the cycle window.
94
- const waiting = results.length > 0 && results.every(result => waits.has(result.call_id) && isProcessReceipt(result.data) && result.data.running === true);
95
- const recent = state.reviewed || waiting ? state.recent : [...state.recent, { signature, progressed: rerun }].slice(-MAX_CYCLE * CYCLE_REPEATS);
96
- return { ...state, pending: [], evidence: progress.evidence, previous: signature, reviewed: false, recent,
97
- failures: state.failures + results.filter(result => !result.ok).length,
98
- emptyWaits: state.emptyWaits + results.filter(result => waits.has(result.call_id) && isProcessReceipt(result.data) &&
99
- result.data.running === true && result.data.cursor !== undefined && result.data.cursor.start === result.data.cursor.end).length };
100
- }
101
-
102
- /** Fold original events, never the retained projection where evidence can repeat. */
103
- export function advanceStall(state: StallState, entries: readonly { node: number; event: LogEvent }[]): StallState {
104
- let result = state;
105
- for (const { node, event } of entries) {
106
- const review = event.t === "guidance" && event.reason === "completion_review";
107
- const nudge = event.t === "guidance" && event.reason === "stall";
108
- if (event.t === "assistant" || event.t === "user" || event.t === "cut" || review || nudge) {
109
- result = finishStallExchange(result);
110
- if (review) result = { ...result, reviewed: true };
111
- // A nudge, or the user's "continue", starts a fresh window: only a new
112
- // full cycle after it counts, never the one already reported.
113
- if (nudge) result = { ...result, nudged: true, recent: [] };
114
- if (event.t === "user") result = { ...result, pending: [], recent: [], failures: 0, emptyWaits: 0, nudged: false, previous: "", reviewed: false };
115
- if (event.t === "assistant") result = { ...result, pending: [event], pendingNode: node };
116
- } else if (result.pending.length > 0) result = { ...result, pending: [...result.pending, event] };
117
- }
118
- return result;
119
- }
120
- export function stallDecision(state: StallState, limit: number) {
121
- if (!Number.isSafeInteger(limit) || limit < 0 || limit === 1) throw new RangeError("stall limit must be zero or an integer of at least two");
122
- const current = finishStallExchange(state);
123
- const cycle = limit > 0 ? repeatedCycle(current.recent) : null;
124
- // S.8: the first cycle is pointed out to the model; a second goes to the
125
- // user when one can answer, and otherwise ends the request.
126
- return { cycle, repeats: CYCLE_REPEATS, failures: current.failures, emptyWaits: current.emptyWaits,
127
- warn: cycle !== null && !current.nudged, stop: cycle !== null && current.nudged };
128
- }
@@ -1,68 +0,0 @@
1
- /** D8 / D11: summarize termination mechanically; assistant claims never become receipts. */
2
- import type { LogEvent } from "./events.ts";
3
- import { isProcessReceipt, latestProcessReceipt } from "./process-receipt.ts";
4
- import { exitLine } from "./session-exit.ts";
5
- import { currentRequestEvents } from "./request-state.ts";
6
-
7
- export function stopReport(events: readonly LogEvent[], requests: number, limit: number, cause: string): string {
8
- const scoped = currentRequestEvents(events);
9
- const current = scoped.length > 0 ? scoped : events;
10
- const results = current.filter(event => event.t === "tool_result");
11
- // Q.22 keeps ok:false on a recorded-but-unverified claim so the model does
12
- // not read "recorded" as "done"; to a person reading why work stopped it is
13
- // an unproven statement, not a broken tool.
14
- const unverified = results.filter(unverifiedClaim);
15
- const failures = results.filter(event => !event.ok && !unverifiedClaim(event));
16
- const exits = results.filter(event => event.effect?.kind === "command" && event.effect.exit === 0).length;
17
- const ids = new Set<number>();
18
- for (const event of current) {
19
- if (event.t === "tool_result" && isProcessReceipt(event.data)) ids.add(event.data.session_id);
20
- if (event.t === "process_exit") ids.add(event.session);
21
- }
22
- const running = [...ids].filter(id => latestProcessReceipt([{ node: 0, events: current }], id)?.receipt.running === true);
23
- return [
24
- requestLine(exitLine(cause), requests, limit),
25
- `Recorded evidence: ${results.length} tool receipts; ${exits} successful command exits; ${failures.length} failed tool receipts${unverified.length ? `; ${unverified.length} unverified claims` : ""}. Task completion is not verified by this stop.`,
26
- ...(failures.length ? [`Failed tools: ${failedTools(current, failures)}.`] : []),
27
- ...(finalReplyGiven(current) ? ["A final reply was given before this stop; read it above — this stop does not mark it wrong."] : []),
28
- ...(running.length ? [`Last observed running: ${running.slice(0, 3).join(", ")}${running.length > 3 ? ` (+${running.length - 3})` : ""}; inspect the process receipt before replaying a command.`] : []),
29
- "Next: inspect /state or the recorded tool output, then provide a focused continuation; completed commands should not be replayed just to recover their output.",
30
- ].join("\n");
31
- }
32
-
33
- /**
34
- * S.6 removed the default cap. A bare "(80 model requests)" in the same
35
- * brackets as "(3/7 model requests)" read as a ceiling that was hit
36
- * (fix-stop-report-request-count), so an uncapped run says it is a count.
37
- */
38
- function requestLine(cause: string, requests: number, limit: number): string {
39
- if (limit !== Infinity) return `Stopped: ${cause} (${requests}/${limit} model requests).`;
40
- return `Stopped: ${cause}. This request sent ${requests} model request${requests === 1 ? "" : "s"}; no request cap is set.`;
41
- }
42
-
43
- /**
44
- * The user reads this line; no command looks a receipt up by id, so ids told
45
- * them nothing (fix-stop-report-readable). Tool names say where it went wrong.
46
- */
47
- function failedTools(events: readonly LogEvent[], failures: readonly Extract<LogEvent, { t: "tool_result" }>[]): string {
48
- const names = new Map<string, string>();
49
- for (const event of events) if (event.t === "assistant") for (const block of event.blocks) if (block.type === "tool_use") names.set(block.call_id, block.name);
50
- const counts = new Map<string, number>();
51
- for (const failure of failures) { const name = names.get(failure.call_id) ?? "unknown"; counts.set(name, (counts.get(name) ?? 0) + 1); }
52
- const ranked = [...counts].sort(([a, x], [b, y]) => y - x || (a < b ? -1 : a > b ? 1 : 0));
53
- return ranked.slice(0, 3).map(([name, count]) => `${name} ×${count}`).join(", ") + (ranked.length > 3 ? ` (+${ranked.length - 3} more)` : "");
54
- }
55
-
56
- function unverifiedClaim(event: Extract<LogEvent, { t: "tool_result" }>): boolean {
57
- if (event.ok || !event.content.startsWith("{")) return false;
58
- try {
59
- const body = JSON.parse(event.content) as { recorded?: unknown; verification?: unknown };
60
- return body.recorded === true && body.verification === "unverified";
61
- } catch { return false; }
62
- }
63
-
64
- /** A stop after the model already answered must not read as "the task failed". */
65
- function finalReplyGiven(events: readonly LogEvent[]): boolean {
66
- return events.some(event => event.t === "assistant" && !event.blocks.some(block => block.type === "tool_use") &&
67
- event.blocks.some(block => block.type === "text" && block.text.trim().length > 0));
68
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Text views an edit is matched against.
3
- *
4
- * A model never sees the bytes of a file, only read's rendering of them:
5
- * no byte order mark, no carriage returns. An edit copied from that
6
- * rendering has to be matched against the same view and written back in the
7
- * file's own encoding -- otherwise every edit to a CRLF file fails, however
8
- * exactly the model copied what it was shown.
9
- */
10
-
11
- export interface TextView {
12
- /** Content with the BOM removed and every line ending as `\n`. */
13
- text: string;
14
- bom: boolean;
15
- /** Decided by the first line ending, as pi does; mixed files take the first. */
16
- ending: "\n" | "\r\n";
17
- }
18
-
19
- export function toView(raw: string): TextView {
20
- const bom = raw.startsWith("");
21
- const body = bom ? raw.slice(1) : raw;
22
- const lf = body.indexOf("\n");
23
- const ending = lf > 0 && body[lf - 1] === "\r" ? "\r\n" : "\n";
24
- return { text: toLF(body), bom, ending };
25
- }
26
-
27
- export function fromView(view: TextView, text: string): string {
28
- return (view.bom ? "" : "") + (view.ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text);
29
- }
30
-
31
- export function toLF(text: string): string {
32
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
33
- }
34
-
35
- /**
36
- * The equivalence a copied snippet is allowed to differ by.
37
- *
38
- * Each rule is a known way a model's copy drifts from the file: trailing
39
- * whitespace dropped, typographic quotes and dashes straightened, exotic
40
- * spaces turned into plain ones, compatibility forms folded (NFKC). Ported
41
- * from pi-mono `edit-diff.ts` `normalizeForFuzzyMatch` (MIT, 6f75515).
42
- *
43
- * The line count is preserved -- no rule adds or removes a `\n` -- which is
44
- * what lets a match found here be mapped back onto whole lines of the
45
- * original, leaving every untouched line byte for byte as it was.
46
- */
47
- export function normalizeLoose(text: string): string {
48
- return text
49
- .normalize("NFKC")
50
- .split("\n")
51
- .map((line) => line.trimEnd())
52
- .join("\n")
53
- .replace(/[‘’‚‛]/g, "'")
54
- .replace(/[“”„‟]/g, "\"")
55
- .replace(/[‐‑‒–—―−]/g, "-")
56
- .replace(/[  -    ]/g, " ");
57
- }
58
-
59
- /** Levenshtein distance with the row buffer reused; inputs are bounded by callers. */
60
- export function editDistance(a: string, b: string): number {
61
- if (a === b) return 0;
62
- if (a.length === 0) return b.length;
63
- if (b.length === 0) return a.length;
64
- let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
65
- let current = new Array<number>(b.length + 1);
66
- for (let i = 1; i <= a.length; i++) {
67
- current[0] = i;
68
- for (let j = 1; j <= b.length; j++) {
69
- const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
70
- current[j] = Math.min(previous[j]! + 1, current[j - 1]! + 1, previous[j - 1]! + cost);
71
- }
72
- [previous, current] = [current, previous];
73
- }
74
- return previous[b.length]!;
75
- }
76
-
77
- /** 1 for identical, 0 for nothing in common. */
78
- export function similarity(a: string, b: string): number {
79
- const longest = Math.max(a.length, b.length);
80
- return longest === 0 ? 1 : 1 - editDistance(a, b) / longest;
81
- }