@visual-z/xeno 0.2.1 → 0.4.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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { parseFrontmatter } from "./frontmatter.ts";
11
+ import type { LogEvent } from "./events.ts";
11
12
 
12
13
  /** Agent Skills name rule. Assumed from the spec, not measured. */
13
14
  const MAX_NAME_LENGTH = 64;
@@ -25,6 +26,28 @@ export interface Skill {
25
26
  readonly filePath: string;
26
27
  readonly body: string;
27
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()];
28
51
  }
29
52
 
30
53
  export function isValidSkillName(name: string): boolean {
@@ -38,6 +61,22 @@ export function parseSkill(input: {
38
61
  }): { skill: Skill | null; diagnostics: readonly string[] } {
39
62
  const parsed = parseFrontmatter(input.text);
40
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 };
41
80
  const desc = parsed.fields["description"];
42
81
  const description = typeof desc === "string" ? desc.trim() : "";
43
82
  if (description.length === 0) {
@@ -62,6 +101,11 @@ export function parseSkill(input: {
62
101
  filePath: input.filePath,
63
102
  body: parsed.body,
64
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 } : {}),
65
109
  },
66
110
  diagnostics,
67
111
  };
@@ -0,0 +1,211 @@
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/theme.ts CHANGED
@@ -37,7 +37,7 @@ export const THEME_TOKENS = [
37
37
  export type ThemeToken = (typeof THEME_TOKENS)[number];
38
38
 
39
39
  export type ThemeColors = { readonly [K in ThemeToken]: string } & Readonly<Record<string, string>>;
40
- const PI_THEME_TOKENS: readonly string[] = ["accent","border","borderAccent","borderMuted","success","error","warning","muted","dim","text","thinkingText","scrollbarTrack","scrollbarThumb","searchMatchText","userMessageText","customMessageText","customMessageLabel","toolTitle","toolOutput","mdHeading","mdLink","mdLinkUrl","mdCode","mdCodeBlock","mdCodeBlockBorder","mdQuote","mdQuoteBorder","mdHr","mdListBullet","toolDiffAdded","toolDiffRemoved","toolDiffContext","syntaxComment","syntaxKeyword","syntaxFunction","syntaxVariable","syntaxString","syntaxNumber","syntaxType","syntaxOperator","syntaxPunctuation","thinkingOff","thinkingMinimal","thinkingLow","thinkingMedium","thinkingHigh","thinkingXhigh","thinkingMax","bashMode","selectedBg","searchMatchBg","userMessageBg","customMessageBg","toolPendingBg","toolSuccessBg","toolErrorBg"];
40
+ const PI_THEME_TOKENS: readonly string[] = ["background","accent","border","borderAccent","borderMuted","success","error","warning","muted","dim","text","thinkingText","scrollbarTrack","scrollbarThumb","searchMatchText","userMessageText","customMessageText","customMessageLabel","toolTitle","toolOutput","mdHeading","mdLink","mdLinkUrl","mdCode","mdCodeBlock","mdCodeBlockBorder","mdQuote","mdQuoteBorder","mdHr","mdListBullet","toolDiffAdded","toolDiffRemoved","toolDiffContext","syntaxComment","syntaxKeyword","syntaxFunction","syntaxVariable","syntaxString","syntaxNumber","syntaxType","syntaxOperator","syntaxPunctuation","thinkingOff","thinkingMinimal","thinkingLow","thinkingMedium","thinkingHigh","thinkingXhigh","thinkingMax","bashMode","selectedBg","searchMatchBg","userMessageBg","customMessageBg","toolPendingBg","toolSuccessBg","toolErrorBg"];
41
41
 
42
42
  export interface Theme {
43
43
  readonly name: string;
@@ -50,64 +50,33 @@ export interface ThemeParse {
50
50
  }
51
51
 
52
52
  /**
53
- * Palette adapted from pi (earendil-works/pi, MIT), coding-agent theme/dark.json
54
- * and light.json at 3390bd93630965a12a0a1a5c36ce890ec22f7e1d. Existing custom
55
- * tokens remain accepted; missing presentation tokens inherit these defaults.
53
+ * Built-in appearances own no colours of their own: every token is one of the
54
+ * terminal's 16 slots or its default (empty), and no token paints a
55
+ * background (docs/tasks/refactor-tui-terminal-colors.md). The interactive
56
+ * view derives finer greys from the terminal's reported palette; these
57
+ * tokens are what line mode and an unreporting terminal get.
58
+ *
59
+ * `dark` and `light` differ only in which half of the palette stays legible:
60
+ * bright slots on a dark ground, normal slots on a light one. Muted text is a
61
+ * grey slot, never SGR faint (faint made muted arguments unreadable).
56
62
  */
57
- const DARK_COLORS: ThemeColors = {
58
- "text": "38;2;212;212;212",
59
- "thinkingText": "38;2;128;128;128",
60
- "toolTitle": "38;2;212;212;212",
61
- "toolOutput": "38;2;128;128;128",
62
- "error": "38;2;204;102;102",
63
- "toolDiffAdded": "38;2;181;189;104",
64
- "toolDiffRemoved": "38;2;204;102;102",
65
- "status": "38;2;128;128;128",
66
- "prompt": "38;2;212;212;212",
67
- "selectedBg": "48;2;58;58;74",
68
- "muted": "38;2;128;128;128",
69
- "warning": "38;2;255;255;0",
70
- "mdHeading": "38;2;240;198;116",
71
- "mdCode": "38;2;138;190;183",
72
- "mdLink": "38;2;129;162;190",
73
- "userMessageBg": "48;2;52;53;65",
74
- "toolPendingBg": "48;2;40;40;50",
75
- "toolSuccessBg": "48;2;40;50;40",
76
- "toolErrorBg": "48;2;60;40;40",
77
- "thinkingLow": "38;2;95;135;175",
78
- "thinkingMedium": "38;2;129;162;190",
79
- "thinkingHigh": "38;2;178;148;187",
80
- "thinkingXhigh": "38;2;209;131;232",
81
- "thinkingMax": "38;2;255;95;255"
82
- };
63
+ const DARK_COLORS: ThemeColors = terminalTokens({ muted: "37", red: "91", green: "92", yellow: "93", blue: "94", magenta: "95", cyan: "96", rule: "90" });
64
+ const LIGHT_COLORS: ThemeColors = terminalTokens({ muted: "90", red: "31", green: "32", yellow: "33", blue: "34", magenta: "35", cyan: "36", rule: "37" });
83
65
 
84
- /** Distinct from dark so `--theme light` is visible without OSC 11 probing. */
85
- const LIGHT_COLORS: ThemeColors = {
86
- "text": "38;2;31;35;40",
87
- "thinkingText": "38;2;108;108;108",
88
- "toolTitle": "38;2;31;35;40",
89
- "toolOutput": "38;2;108;108;108",
90
- "error": "38;2;170;85;85",
91
- "toolDiffAdded": "38;2;88;132;88",
92
- "toolDiffRemoved": "38;2;170;85;85",
93
- "status": "38;2;108;108;108",
94
- "prompt": "38;2;31;35;40",
95
- "selectedBg": "48;2;208;208;224",
96
- "muted": "38;2;108;108;108",
97
- "warning": "38;2;154;115;38",
98
- "mdHeading": "38;2;154;115;38",
99
- "mdCode": "38;2;90;128;128",
100
- "mdLink": "38;2;84;125;167",
101
- "userMessageBg": "48;2;232;232;232",
102
- "toolPendingBg": "48;2;232;232;240",
103
- "toolSuccessBg": "48;2;232;240;232",
104
- "toolErrorBg": "48;2;240;232;232",
105
- "thinkingLow": "38;2;84;125;167",
106
- "thinkingMedium": "38;2;90;128;128",
107
- "thinkingHigh": "38;2;135;95;135",
108
- "thinkingXhigh": "38;2;139;0;139",
109
- "thinkingMax": "38;2;175;0;95"
110
- };
66
+ function terminalTokens(slot: { muted: string; red: string; green: string; yellow: string; blue: string; magenta: string; cyan: string; rule: string }): ThemeColors {
67
+ return {
68
+ text: "", prompt: "", background: "",
69
+ thinkingText: slot.magenta, toolTitle: "", toolOutput: slot.muted,
70
+ error: slot.red, toolDiffAdded: slot.green, toolDiffRemoved: slot.red,
71
+ status: slot.muted, muted: slot.muted, dim: slot.muted, warning: slot.yellow, success: slot.green, accent: slot.blue,
72
+ mdHeading: slot.blue, mdCode: slot.cyan, mdLink: slot.blue,
73
+ selectedBg: "", userMessageBg: "", toolPendingBg: "", toolSuccessBg: "", toolErrorBg: "",
74
+ border: slot.rule, borderMuted: slot.rule,
75
+ thinkingOff: slot.muted, thinkingMinimal: slot.magenta, thinkingLow: slot.magenta, thinkingMedium: slot.magenta,
76
+ thinkingHigh: slot.magenta, thinkingXhigh: slot.magenta, thinkingMax: slot.magenta,
77
+ syntaxKeyword: slot.magenta, syntaxString: slot.green, syntaxNumber: slot.yellow, syntaxFunction: slot.blue, syntaxComment: slot.muted,
78
+ };
79
+ }
111
80
 
112
81
  export function builtinTheme(name: "dark" | "light"): Theme {
113
82
  return name === "light"
@@ -115,6 +84,15 @@ export function builtinTheme(name: "dark" | "light"): Theme {
115
84
  : { name: "dark", colors: DARK_COLORS };
116
85
  }
117
86
 
87
+ /**
88
+ * A built-in appearance (dark, light, or system/auto, which reuse them) as
89
+ * opposed to a theme file. Identity, not name: a file may call itself "dark",
90
+ * and parseTheme always copies colours into a new object.
91
+ */
92
+ export function isBuiltinTheme(theme: Theme): boolean {
93
+ return theme.colors === DARK_COLORS || theme.colors === LIGHT_COLORS;
94
+ }
95
+
118
96
  export function parseTheme(json: unknown): ThemeParse {
119
97
  const fallback = builtinTheme("dark");
120
98
  if (typeof json !== "object" || json === null || Array.isArray(json)) {
@@ -153,7 +131,7 @@ export function parseTheme(json: unknown): ThemeParse {
153
131
  diagnostics.push(`invalid colour for ${token}; using built-in`);
154
132
  continue;
155
133
  }
156
- colors[token] = token.endsWith("Bg") ? parsed.replace(/^38;/, "48;") : parsed;
134
+ colors[token] = (token.endsWith("Bg") || token === "background") ? parsed.replace(/^38;/, "48;") : parsed;
157
135
  }
158
136
 
159
137
  if (!("status" in src) && "dim" in colors) colors.status = colors["dim"]!;
@@ -1,9 +1,17 @@
1
1
  /** O.3: one declarative input contract serves model schemas and runtime admission. */
2
2
  export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };
3
3
  export type ToolDiagnostic = {
4
- code: "invalid_input" | "version_mismatch" | "not_found" | "ambiguous_edit" | "permission_denied" | "execution_failed";
4
+ code: "invalid_input" | "version_mismatch" | "not_found" | "ambiguous_edit" | "permission_denied" | "execution_failed" |
5
+ "environment_missing" | "transient" | "cancelled" | "timeout" | "unknown_effect" | "command_failed";
5
6
  message: string;
6
7
  recover?: { tool: string; input: { readonly [key: string]: JsonValue } };
8
+ /** R3: optional only for legacy records; supplied together by current failure owners. */
9
+ execution?: "not_started" | "started" | "unknown";
10
+ effects?: "none" | "possible";
11
+ retry?: "never" | "after_change" | "after_inspection" | "backoff";
12
+ next?: string;
13
+ /** A schema-valid shape with placeholders, never an automatically executable recovery. */
14
+ example?: { readonly [key: string]: JsonValue };
7
15
  };
8
16
 
9
17
  export function isJsonValue(value: unknown): value is JsonValue {
@@ -15,7 +23,17 @@ export function isJsonValue(value: unknown): value is JsonValue {
15
23
  export function isToolDiagnostic(value: unknown): value is ToolDiagnostic {
16
24
  if (value === null || typeof value !== "object") return false;
17
25
  const item = value as Record<string, unknown>;
18
- if (!["invalid_input", "version_mismatch", "not_found", "ambiguous_edit", "permission_denied", "execution_failed"].includes(String(item["code"])) || typeof item["message"] !== "string") return false;
26
+ if (!["invalid_input", "version_mismatch", "not_found", "ambiguous_edit", "permission_denied", "execution_failed",
27
+ "environment_missing", "transient", "cancelled", "timeout", "unknown_effect", "command_failed"].includes(String(item["code"])) || typeof item["message"] !== "string") return false;
28
+ if (["execution", "effects", "retry", "next"].some(key => item[key] !== undefined)) {
29
+ if (!["not_started", "started", "unknown"].includes(String(item["execution"])) ||
30
+ !["none", "possible"].includes(String(item["effects"])) ||
31
+ !["never", "after_change", "after_inspection", "backoff"].includes(String(item["retry"])) || typeof item["next"] !== "string") return false;
32
+ if (item["retry"] === "backoff" && (item["code"] !== "transient" || item["effects"] !== "none")) return false;
33
+ if (item["code"] === "permission_denied" && (item["retry"] !== "never" || item["recover"] !== undefined)) return false;
34
+ if (item["code"] === "unknown_effect" && item["retry"] !== "after_inspection") return false;
35
+ }
36
+ if (item["example"] !== undefined && (item["example"] === null || typeof item["example"] !== "object" || Array.isArray(item["example"]) || !isJsonValue(item["example"]))) return false;
19
37
  const recover = item["recover"];
20
38
  if (recover === undefined) return true;
21
39
  if (recover === null || typeof recover !== "object") return false;
@@ -24,6 +42,37 @@ export function isToolDiagnostic(value: unknown): value is ToolDiagnostic {
24
42
  typeof recovery["input"] === "object" && !Array.isArray(recovery["input"]) && isJsonValue(recovery["input"]);
25
43
  }
26
44
 
45
+ /** Required fields only; examples express a valid shape, not permission or a safe command. */
46
+ export function minimalInput(schema: unknown): JsonValue | undefined {
47
+ if (schema === null || typeof schema !== "object") return undefined;
48
+ const s = schema as Record<string, unknown>;
49
+ if (Array.isArray(s["enum"]) && s["enum"].length && isJsonValue(s["enum"][0])) return s["enum"][0];
50
+ switch (s["type"]) {
51
+ case "string": return "<value>".padEnd(typeof s["minLength"] === "number" ? Math.min(4096, s["minLength"]) : 0, "x");
52
+ case "number": case "integer": return typeof s["minimum"] === "number" ? s["minimum"] : 0;
53
+ case "boolean": return false;
54
+ case "array": return [];
55
+ case "object": {
56
+ const value: { [key: string]: JsonValue } = {};
57
+ const properties = s["properties"];
58
+ for (const key of Array.isArray(s["required"]) ? s["required"] : []) {
59
+ if (typeof key !== "string" || properties === null || typeof properties !== "object") return undefined;
60
+ const item = minimalInput((properties as Record<string, unknown>)[key]);
61
+ if (item === undefined) return undefined;
62
+ value[key] = item;
63
+ }
64
+ return value;
65
+ }
66
+ default: return null;
67
+ }
68
+ }
69
+
70
+ /** Diagnostics are projected as tool data; they never become trusted system instructions. */
71
+ export function diagnosticContent(content: string, diagnostic?: ToolDiagnostic): string {
72
+ if (diagnostic?.execution === undefined) return content;
73
+ return content + "\nTool diagnostic: " + JSON.stringify(diagnostic);
74
+ }
75
+
27
76
  import { editDistance } from "./text-normalize.ts";
28
77
 
29
78
  /**
@@ -0,0 +1,87 @@
1
+ /** R11 / D6 / D8: local discovery proposes declarations; only a paired checkpoint installs them. */
2
+ import type { LogEvent, ToolInvocationEvent } from "./events.ts";
3
+
4
+ /** Assumed response bounds, not measured model optima; prevent a directory dump from replacing a lookup. */
5
+ export const MAX_DISCOVERY_RESULTS = 8;
6
+ export const MAX_DISCOVERY_QUERY = 256;
7
+ const MAX_PARAMETER_HINTS = 8;
8
+ const MAX_PURPOSE_CHARS = 320;
9
+
10
+ export interface DiscoverableTool {
11
+ readonly name: string;
12
+ readonly description: string;
13
+ readonly input_schema: { readonly properties: Readonly<Record<string, unknown>>; readonly required: readonly string[] };
14
+ }
15
+ export interface DeclarationRequest { readonly name: string; readonly fingerprint: string }
16
+ export interface DiscoveryMatch {
17
+ name: string; purpose: string; inputs: { name: string; type: string; required: boolean }[]; omitted_inputs: number;
18
+ }
19
+ export function declaredToolNames(tools: readonly unknown[]): string[] {
20
+ return tools.flatMap(tool => tool !== null && typeof tool === "object" && "name" in tool && typeof tool.name === "string" ? [tool.name] : []);
21
+ }
22
+ const compare = (a: string, b: string) => a < b ? -1 : a > b ? 1 : 0;
23
+ function record(raw: unknown): Record<string, unknown> | undefined {
24
+ return raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : undefined;
25
+ }
26
+ function parameterType(raw: unknown): string {
27
+ if (raw === null || typeof raw !== "object") return "schema";
28
+ const type = (raw as Record<string, unknown>)["type"];
29
+ if (typeof type === "string") return type.slice(0, 64);
30
+ if (Array.isArray(type) && type.every(item => typeof item === "string")) return type.join("|").slice(0, 64);
31
+ return "schema";
32
+ }
33
+
34
+ /** Pure, deterministic matching over the caller's already-selected directory; never perform discovery I/O. */
35
+ export function discoverTools(catalog: readonly DiscoverableTool[], query: string, limit = 5): DiscoveryMatch[] {
36
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_DISCOVERY_RESULTS) throw new Error("invalid discovery limit");
37
+ const normalized = query.trim().toLowerCase();
38
+ if (!normalized || query.length > MAX_DISCOVERY_QUERY) throw new Error("query must contain 1 to 256 characters");
39
+ const terms = normalized.split(/[^\p{L}\p{N}_-]+/u).filter(Boolean);
40
+ if (terms.length === 0) return [];
41
+ return catalog.map(tool => {
42
+ const name = tool.name.toLowerCase(), text = `${name} ${tool.description.toLowerCase()}`;
43
+ const score = name === normalized ? 100 : terms.every(term => text.includes(term))
44
+ ? 1 + terms.filter(term => name.includes(term)).length : 0;
45
+ return { tool, score };
46
+ }).filter(item => item.score > 0).sort((a, b) => b.score - a.score || compare(a.tool.name, b.tool.name))
47
+ .slice(0, limit).map(({ tool }) => {
48
+ const required = new Set(tool.input_schema.required);
49
+ const parameters = Object.entries(tool.input_schema.properties).sort(([a], [b]) =>
50
+ Number(required.has(b)) - Number(required.has(a)) || compare(a, b));
51
+ return { name: tool.name, purpose: tool.description.slice(0, MAX_PURPOSE_CHARS),
52
+ inputs: parameters.slice(0, MAX_PARAMETER_HINTS).map(([name, raw]) => ({ name: name.slice(0, 128), type: parameterType(raw), required: required.has(name) })),
53
+ omitted_inputs: Math.max(0, parameters.length - MAX_PARAMETER_HINTS) };
54
+ });
55
+ }
56
+
57
+ /** A model assertion or another extension's data cannot authorize a declaration (D11). */
58
+ export function recordedDeclarations(events: readonly LogEvent[]): DeclarationRequest[] {
59
+ const invocations = new Map<string, ToolInvocationEvent>();
60
+ const requested = new Map<string, { request: DeclarationRequest; user: string | undefined }>();
61
+ let user: string | undefined;
62
+ for (const event of events) {
63
+ if (event.t === "user") { user = event.id; continue; }
64
+ if (event.t === "interrupt") {
65
+ for (const [name, value] of requested) if (value.user === event.user_id) requested.delete(name);
66
+ continue;
67
+ }
68
+ if (event.t === "tool_invocation") { invocations.set(event.id, event); continue; }
69
+ if (event.t !== "tool_result" || !event.ok || !event.invocation_ref) continue;
70
+ if (event.outcome !== undefined && event.outcome.status !== "completed") continue;
71
+ const invocation = invocations.get(event.invocation_ref.event);
72
+ if (invocation?.name !== "tool_search" || invocation.call_id !== event.call_id) continue;
73
+ const data = record(event.data);
74
+ if (!data || data["kind"] !== "tool_discovery" || data["version"] !== 1) continue;
75
+ const entries = data["requested"];
76
+ if (!Array.isArray(entries) || entries.length > MAX_DISCOVERY_RESULTS) continue;
77
+ for (const entry of entries) {
78
+ const item = record(entry);
79
+ if (!item) continue;
80
+ const name = item["name"], fingerprint = item["fingerprint"];
81
+ if (typeof name === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(name) && typeof fingerprint === "string" && /^[a-f0-9]{64}$/.test(fingerprint)) {
82
+ requested.set(name, { request: { name, fingerprint }, user });
83
+ }
84
+ }
85
+ }
86
+ return [...requested.values()].map(value => value.request);
87
+ }
@@ -0,0 +1,60 @@
1
+ /** R3 / D11: failure policy follows observed execution facts, never output keywords. */
2
+ import type { ToolDiagnostic } from "./tool-contract.ts";
3
+
4
+ export type FailureFacts = { execution: "not_started" | "started" | "unknown"; effects: "none" | "possible" };
5
+ export const NOT_STARTED: FailureFacts = { execution: "not_started", effects: "none" };
6
+ export const READ_ATTEMPTED: FailureFacts = { execution: "started", effects: "none" };
7
+ export const UNKNOWN_EFFECT: FailureFacts = { execution: "unknown", effects: "possible" };
8
+
9
+ const guidance: Record<ToolDiagnostic["code"], string> = {
10
+ invalid_input: "Correct the named argument using the tool schema; example values are placeholders.",
11
+ version_mismatch: "Read the current resource and derive a new edit from that version.",
12
+ not_found: "Verify the resource path or identifier before changing the request.",
13
+ ambiguous_edit: "Read the indicated range and choose a unique match before editing.",
14
+ permission_denied: "This operation was denied. Do not bypass it through another tool or wrapper; a user policy change is required.",
15
+ execution_failed: "Inspect the failure and change its cause before retrying.",
16
+ environment_missing: "Verify the working directory and required executable in this environment before retrying. If available and permitted, use environment with the exact program; use its python probe for actual Python API support.",
17
+ transient: "Retry with bounded backoff only if no side effect is possible.",
18
+ cancelled: "Respect cancellation; inspect any partial effects before a user-authorized continuation.",
19
+ timeout: "Inspect partial output and effects before deciding whether a new invocation is needed.",
20
+ unknown_effect: "Execution or effects are uncertain. Inspect existing state and output before any replay.",
21
+ command_failed: "Use the actual exit and output to diagnose the command; a nonzero exit alone does not identify its cause.",
22
+ };
23
+
24
+ export function failureDiagnostic(code: ToolDiagnostic["code"], message: string, facts: FailureFacts,
25
+ extra: Pick<ToolDiagnostic, "recover" | "example"> = {}): ToolDiagnostic {
26
+ const retry = code === "permission_denied" || code === "cancelled" ? "never"
27
+ : code === "unknown_effect" || facts.effects === "possible" ? "after_inspection"
28
+ : code === "transient" ? "backoff" : "after_change";
29
+ return { code, message, ...facts, retry, next: guidance[code], ...(code === "permission_denied" ? {} : extra) };
30
+ }
31
+
32
+ /** R10: only new advice is scoped; execution facts and historical records are never rewritten. */
33
+ export function scopeFailureGuidance(diagnostic: ToolDiagnostic, tools: readonly string[], recoverAllowed = true): ToolDiagnostic {
34
+ const { recover, ...facts } = diagnostic;
35
+ const next = diagnostic.execution !== undefined && diagnostic.code === "environment_missing" && !tools.includes("environment")
36
+ ? { next: "Verify the working directory and required executable in the current executor using permitted observations. If the needed capability is unavailable, report that evidence gap before retrying." } : {};
37
+ return { ...facts, ...next, ...(recover && recoverAllowed && tools.includes(recover.tool) && diagnostic.code !== "permission_denied" ? { recover } : {}) };
38
+ }
39
+
40
+ /** Runtime and store throw only facts they own; the outer boundary must retain these facts. */
41
+ export class ToolFault extends Error {
42
+ readonly diagnostic: ToolDiagnostic;
43
+ constructor(code: ToolDiagnostic["code"], message: string, facts: FailureFacts, extra: Pick<ToolDiagnostic, "recover"> = {}) {
44
+ super(message); this.name = "ToolFault";
45
+ this.diagnostic = failureDiagnostic(code, message, facts, extra);
46
+ }
47
+ }
48
+
49
+ /** OS codes are facts; words like "denied" in arbitrary output are not classification evidence. */
50
+ export function failureFromError(error: unknown, facts: FailureFacts): ToolDiagnostic {
51
+ if (error instanceof ToolFault) return error.diagnostic;
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ const os = error !== null && typeof error === "object" && "code" in error ? error.code : undefined;
54
+ const code = os === "ENOENT" || os === "ENOTDIR" ? "not_found"
55
+ : os === "EACCES" || os === "EPERM" ? "permission_denied"
56
+ : os === "EISDIR" ? "invalid_input"
57
+ : ["EAGAIN", "ETIMEDOUT", "ECONNRESET", "EMFILE"].includes(String(os)) ? "transient"
58
+ : facts.effects === "possible" ? "unknown_effect" : "execution_failed";
59
+ return failureDiagnostic(code, message, facts);
60
+ }
@@ -12,6 +12,12 @@
12
12
  */
13
13
  type Input = Record<string, unknown>;
14
14
 
15
+ /** O.3: incomplete wire JSON remains the original input so admission can refuse it without effects. */
16
+ export function completedToolInput(raw: string): unknown {
17
+ try { return JSON.parse(raw) as unknown; }
18
+ catch { return raw; }
19
+ }
20
+
15
21
  function parsedJson(value: unknown): unknown {
16
22
  if (typeof value !== "string") return value;
17
23
  const trimmed = value.trim();