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