@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.
- package/CHANGELOG.md +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
package/src/core/skills.ts
CHANGED
|
@@ -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
|
-
*
|
|
54
|
-
* and
|
|
55
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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"
|
|
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
|
+
}
|
package/src/core/tool-input.ts
CHANGED
|
@@ -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();
|