@visual-z/xeno 0.8.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -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 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- 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 -3108
- 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/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- 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/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- 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/prompts.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt-template expansion. Pure: the file is read in store.
|
|
3
|
-
*
|
|
4
|
-
* `{{name}}` is filled with the remainder of `/template rest`. An empty
|
|
5
|
-
* remainder keeps the placeholder and names it — emptying it would drop a
|
|
6
|
-
* sentence from the prompt with no trace (feat-skills-and-prompts design).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { parseFrontmatter } from "./frontmatter.ts";
|
|
10
|
-
|
|
11
|
-
export interface PromptTemplate {
|
|
12
|
-
readonly name: string;
|
|
13
|
-
readonly description: string;
|
|
14
|
-
readonly argumentHint?: string;
|
|
15
|
-
readonly content: string;
|
|
16
|
-
readonly filePath: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const PLACEHOLDER = /\{\{([A-Za-z_][A-Za-z0-9_-]*)\}\}/g;
|
|
20
|
-
|
|
21
|
-
export function expandTemplate(
|
|
22
|
-
content: string,
|
|
23
|
-
arg: string,
|
|
24
|
-
): { text: string; diagnostics: readonly string[] } {
|
|
25
|
-
const names: string[] = [];
|
|
26
|
-
const seen = new Set<string>();
|
|
27
|
-
for (const match of content.matchAll(PLACEHOLDER)) {
|
|
28
|
-
const name = match[1]!;
|
|
29
|
-
if (!seen.has(name)) {
|
|
30
|
-
seen.add(name);
|
|
31
|
-
names.push(name);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
text: substituteArgs(content, parseCommandArgs(arg), arg),
|
|
36
|
-
diagnostics: arg.length ? [] : names.map((name) => `unfilled placeholder: ${name}`),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Ported from pi core/prompt-templates.ts (MIT, 02eed88); quote handling is Pi's, not shell execution. */
|
|
41
|
-
export function parseCommandArgs(text: string): string[] {
|
|
42
|
-
const args: string[] = [];
|
|
43
|
-
let current = "", quote: string | null = null;
|
|
44
|
-
for (const char of text) {
|
|
45
|
-
if (quote) { if (char === quote) quote = null; else current += char; }
|
|
46
|
-
else if (char === '"' || char === "'") quote = char;
|
|
47
|
-
else if (/\s/.test(char)) { if (current) { args.push(current); current = ""; } }
|
|
48
|
-
else current += char;
|
|
49
|
-
}
|
|
50
|
-
if (current) args.push(current);
|
|
51
|
-
return args;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Pi positional/default/slice syntax and legacy named syntax share one pass: inserted text stays literal. */
|
|
55
|
-
function substituteArgs(content: string, args: readonly string[], legacy: string): string {
|
|
56
|
-
const all = args.join(" ");
|
|
57
|
-
return content.replace(/\{\{([A-Za-z_][A-Za-z0-9_-]*)\}\}|\$\{(\d+|ARGUMENTS|@):-([^}]*)\}|\$\{@:(\d+)(?::(\d+))?\}|\$(ARGUMENTS|@|\d+)/g,
|
|
58
|
-
(match: string, named: string | undefined, target: string | undefined, fallback: string | undefined,
|
|
59
|
-
start: string | undefined, length: string | undefined, simple: string | undefined) => {
|
|
60
|
-
if (named) return legacy.length ? legacy : match;
|
|
61
|
-
if (target) return (target === "@" || target === "ARGUMENTS" ? all : args[Number(target) - 1]) || fallback || "";
|
|
62
|
-
if (start !== undefined) {
|
|
63
|
-
const from = Math.max(0, Number(start) - 1);
|
|
64
|
-
return args.slice(from, length === undefined ? undefined : from + Number(length)).join(" ");
|
|
65
|
-
}
|
|
66
|
-
if (simple === "@" || simple === "ARGUMENTS") return all;
|
|
67
|
-
return args[Number(simple) - 1] ?? "";
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function parsePromptTemplate(input: {
|
|
72
|
-
readonly filePath: string;
|
|
73
|
-
readonly text: string;
|
|
74
|
-
}): { template: PromptTemplate | null; diagnostics: readonly string[] } {
|
|
75
|
-
const parsed = parseFrontmatter(input.text);
|
|
76
|
-
const diagnostics = [...parsed.diagnostics];
|
|
77
|
-
const base = fileName(input.filePath);
|
|
78
|
-
const name = base.replace(/\.md$/i, "").toLowerCase();
|
|
79
|
-
if (name.length === 0) {
|
|
80
|
-
diagnostics.push(`${input.filePath}: prompt template has no name`);
|
|
81
|
-
return { template: null, diagnostics };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const descField = parsed.fields["description"];
|
|
85
|
-
let description = typeof descField === "string" ? descField.trim() : "";
|
|
86
|
-
if (description.length === 0) {
|
|
87
|
-
const first = parsed.body.split("\n").find((line) => line.trim().length > 0);
|
|
88
|
-
description = first === undefined ? "" : first.trim();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const hintField = parsed.fields["argument-hint"];
|
|
92
|
-
const argumentHint = typeof hintField === "string" ? hintField : undefined;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
template: {
|
|
96
|
-
name,
|
|
97
|
-
description,
|
|
98
|
-
content: parsed.body,
|
|
99
|
-
filePath: input.filePath,
|
|
100
|
-
...(argumentHint !== undefined ? { argumentHint } : {}),
|
|
101
|
-
},
|
|
102
|
-
diagnostics,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Path basename without node:path, so this module stays core-pure. */
|
|
107
|
-
function fileName(path: string): string {
|
|
108
|
-
const trimmed = path.replace(/[\\/]+$/, "");
|
|
109
|
-
const slash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
110
|
-
return slash >= 0 ? trimmed.slice(slash + 1) : trimmed;
|
|
111
|
-
}
|
package/src/core/question.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/** R13 / D11: an answer is bound to its original active invocation, never guessed. */
|
|
2
|
-
|
|
3
|
-
// Product limits keep one interaction reviewable, not measured model limits.
|
|
4
|
-
export const MAX_QUESTIONS = 3;
|
|
5
|
-
export const MAX_QUESTION_CHOICES = 8;
|
|
6
|
-
export interface QuestionChoice { readonly id: string; readonly label: string; readonly description?: string }
|
|
7
|
-
export interface QuestionSpec {
|
|
8
|
-
readonly id: string;
|
|
9
|
-
readonly header: string;
|
|
10
|
-
readonly question: string;
|
|
11
|
-
readonly options: readonly QuestionChoice[];
|
|
12
|
-
readonly multiple: boolean;
|
|
13
|
-
readonly custom: boolean;
|
|
14
|
-
}
|
|
15
|
-
export interface QuestionRequest {
|
|
16
|
-
readonly request_id: string;
|
|
17
|
-
readonly turn_id: string;
|
|
18
|
-
readonly call_id: string;
|
|
19
|
-
readonly questions: readonly QuestionSpec[];
|
|
20
|
-
}
|
|
21
|
-
export interface QuestionAnswer { readonly id: string; readonly choices: readonly string[]; readonly text?: string }
|
|
22
|
-
export interface QuestionReply {
|
|
23
|
-
readonly request_id: string;
|
|
24
|
-
readonly turn_id: string;
|
|
25
|
-
readonly answers: readonly QuestionAnswer[];
|
|
26
|
-
}
|
|
27
|
-
function object(value: unknown, label: string, fields: readonly string[]): Record<string, unknown> {
|
|
28
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error(`${label} must be an object`);
|
|
29
|
-
const result = value as Record<string, unknown>;
|
|
30
|
-
if (Object.keys(result).some(key => !fields.includes(key))) throw Error(`${label} contains an unknown field`);
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
function string(value: unknown, label: string, max = 2000): string {
|
|
34
|
-
if (typeof value !== "string" || !value.trim() || value.length > max || /[\0\x1b]/.test(value)) throw Error(`${label} must be nonempty plain text (max ${max} characters)`);
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
function identifier(value: unknown, label: string): string {
|
|
38
|
-
const id = string(value, label, 64);
|
|
39
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(id)) throw Error(`${label} must be a stable lowercase identifier`);
|
|
40
|
-
return id;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function parseQuestions(value: unknown): QuestionSpec[] {
|
|
44
|
-
const input = object(value, "question input", ["questions"]);
|
|
45
|
-
const questions = input["questions"];
|
|
46
|
-
if (!Array.isArray(questions) || questions.length < 1 || questions.length > MAX_QUESTIONS) throw Error(`question requires 1..${MAX_QUESTIONS} questions`);
|
|
47
|
-
const parsed = questions.map(value => {
|
|
48
|
-
const item = object(value, "question", ["id", "header", "question", "options", "multiple", "custom"]);
|
|
49
|
-
const options = item["options"] ?? [];
|
|
50
|
-
if (!Array.isArray(options) || options.length > MAX_QUESTION_CHOICES) throw Error(`question options must contain at most ${MAX_QUESTION_CHOICES} choices`);
|
|
51
|
-
const choices = options.map(value => {
|
|
52
|
-
const choice = object(value, "question option", ["id", "label", "description"]);
|
|
53
|
-
return { id: identifier(choice["id"], "option id"), label: string(choice["label"], "option label", 120),
|
|
54
|
-
...(choice["description"] !== undefined ? { description: string(choice["description"], "option description", 600) } : {}) };
|
|
55
|
-
});
|
|
56
|
-
if (new Set(choices.map(choice => choice.id)).size !== choices.length) throw Error("question option ids must be unique");
|
|
57
|
-
if (new Set(choices.map(choice => choice.label)).size !== choices.length) throw Error("question option labels must be unique");
|
|
58
|
-
for (const key of ["multiple", "custom"]) if (item[key] !== undefined && typeof item[key] !== "boolean") throw Error(`question ${key} must be boolean`);
|
|
59
|
-
const multiple = item["multiple"] === true, custom = item["custom"] !== false;
|
|
60
|
-
if (!choices.length && !custom) throw Error("question must offer choices or custom text");
|
|
61
|
-
return { id: identifier(item["id"], "question id"), header: string(item["header"], "question header", 40),
|
|
62
|
-
question: string(item["question"], "question text"), options: choices, multiple, custom };
|
|
63
|
-
});
|
|
64
|
-
if (new Set(parsed.map(question => question.id)).size !== parsed.length) throw Error("question ids must be unique");
|
|
65
|
-
return parsed;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function questionRequest(turnId: string, callId: string, questions: readonly QuestionSpec[]): QuestionRequest {
|
|
69
|
-
if (!turnId || !callId || /[\r\n\0]/.test(turnId + callId)) throw Error("question requires a trusted active turn and call identity");
|
|
70
|
-
// Tuple encoding avoids slash/delimiter collisions in opaque runtime IDs.
|
|
71
|
-
return { request_id: JSON.stringify([turnId, callId]), turn_id: turnId, call_id: callId, questions: structuredClone(questions) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function validateQuestionReply(request: QuestionRequest, value: unknown): QuestionReply {
|
|
75
|
-
const reply = object(value, "question reply", ["request_id", "turn_id", "answers"]);
|
|
76
|
-
if (reply["request_id"] !== request.request_id || reply["turn_id"] !== request.turn_id) throw Error("question reply belongs to a different invocation or turn");
|
|
77
|
-
const answers = reply["answers"];
|
|
78
|
-
if (!Array.isArray(answers) || answers.length !== request.questions.length) throw Error("question reply must answer every question exactly once");
|
|
79
|
-
const parsed = answers.map(value => {
|
|
80
|
-
const answer = object(value, "question answer", ["id", "choices", "text"]);
|
|
81
|
-
const id = identifier(answer["id"], "answer id"), question = request.questions.find(question => question.id === id);
|
|
82
|
-
if (!question) throw Error("question reply contains an unknown question id");
|
|
83
|
-
const choices = answer["choices"];
|
|
84
|
-
if (!Array.isArray(choices) || choices.some(choice => typeof choice !== "string" || !question.options.some(option => option.id === choice)) || new Set(choices).size !== choices.length) throw Error("question reply contains unknown or duplicate choices");
|
|
85
|
-
const text = answer["text"] === undefined ? undefined : string(answer["text"], "answer text", 10000);
|
|
86
|
-
if (text !== undefined && !question.custom) throw Error("custom text is not permitted for this question");
|
|
87
|
-
const count = choices.length + (text === undefined ? 0 : 1);
|
|
88
|
-
if (count === 0 || (!question.multiple && count !== 1)) throw Error("question reply requires one answer, or multiple only when enabled");
|
|
89
|
-
return { id, choices: [...choices] as string[], ...(text !== undefined ? { text } : {}) };
|
|
90
|
-
});
|
|
91
|
-
if (new Set(parsed.map(answer => answer.id)).size !== parsed.length) throw Error("question reply contains duplicate question ids");
|
|
92
|
-
return { request_id: request.request_id, turn_id: request.turn_id,
|
|
93
|
-
answers: request.questions.map(question => parsed.find(answer => answer.id === question.id)!) };
|
|
94
|
-
}
|
package/src/core/queue.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* When a typed message may enter the log.
|
|
3
|
-
*
|
|
4
|
-
* The queue itself is UI state: a line the user may still retract. The log
|
|
5
|
-
* is what happened. nextDelivery is the only decision — it says which
|
|
6
|
-
* queued line is legal to deliver at this phase, or none. The caller
|
|
7
|
-
* records the user event; this file never does.
|
|
8
|
-
*
|
|
9
|
-
* after_tools is the first legal moment to insert a user message, for the
|
|
10
|
-
* same pairing reason switchModel already refuses a half-round: a tool_use
|
|
11
|
-
* without its tool_result is a 400 (project.ts UNPAIRED_TOOL_USE).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { assertNever } from "./exhaustive.ts";
|
|
15
|
-
import type { ImageRef } from "./images.ts";
|
|
16
|
-
|
|
17
|
-
export type QueueKind = "steering" | "followup";
|
|
18
|
-
export type DeliveryMode = "all" | "one-at-a-time";
|
|
19
|
-
|
|
20
|
-
export interface QueuedMessage {
|
|
21
|
-
readonly kind: QueueKind;
|
|
22
|
-
readonly text: string;
|
|
23
|
-
readonly images?: readonly ImageRef[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Where the turn is. after_tools means every tool_use of this assistant
|
|
28
|
-
* round already has a result. turn_end means send() has returned. idle
|
|
29
|
-
* is the editor waiting — leftover lines still deliver, steering first.
|
|
30
|
-
*/
|
|
31
|
-
export type Phase = "after_tools" | "turn_end" | "idle";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Which queued message may be written to the log right now.
|
|
35
|
-
*
|
|
36
|
-
* Same kind stays in enqueue order. Steering as a group always precedes
|
|
37
|
-
* follow-up: a follow-up is defined as "after the whole send", so it
|
|
38
|
-
* cannot win while a steering line is still waiting.
|
|
39
|
-
*/
|
|
40
|
-
export function nextDelivery(
|
|
41
|
-
queue: readonly QueuedMessage[],
|
|
42
|
-
phase: Phase,
|
|
43
|
-
): QueuedMessage | null {
|
|
44
|
-
switch (phase) {
|
|
45
|
-
case "after_tools":
|
|
46
|
-
return firstOfKind(queue, "steering");
|
|
47
|
-
case "turn_end":
|
|
48
|
-
case "idle":
|
|
49
|
-
return firstOfKind(queue, "steering") ?? firstOfKind(queue, "followup");
|
|
50
|
-
default:
|
|
51
|
-
return assertNever(phase, "nextDelivery");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function firstOfKind(
|
|
56
|
-
queue: readonly QueuedMessage[],
|
|
57
|
-
kind: QueueKind,
|
|
58
|
-
): QueuedMessage | null {
|
|
59
|
-
for (const item of queue) {
|
|
60
|
-
if (item.kind === kind) return item;
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Bash remains an atomic follow-up; batching never turns commands into model prompts. */
|
|
66
|
-
export function deliveryBatch(queue: readonly QueuedMessage[], phase: Phase,
|
|
67
|
-
modes: { steering?: DeliveryMode; followup?: DeliveryMode }): readonly QueuedMessage[] {
|
|
68
|
-
const first = nextDelivery(queue, phase);
|
|
69
|
-
if (first === null) return [];
|
|
70
|
-
if (modes[first.kind] !== "all" || first.text.startsWith("!")) return [first];
|
|
71
|
-
const batch: QueuedMessage[] = [];
|
|
72
|
-
for (const item of queue) {
|
|
73
|
-
if (item.kind !== first.kind) continue;
|
|
74
|
-
if (item.text.startsWith("!")) break;
|
|
75
|
-
batch.push(item);
|
|
76
|
-
}
|
|
77
|
-
return batch;
|
|
78
|
-
}
|
package/src/core/read-bounds.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** N.12: recoverable file bounds are observation metadata, not model guesses. */
|
|
2
|
-
export function readBounds(total: number, first: unknown, last: unknown):
|
|
3
|
-
{ ok: true; start: number; end: number; notice: string } | { ok: false; message: string } {
|
|
4
|
-
if (!Number.isSafeInteger(total) || total < 1) throw new RangeError("split text must contain at least one line");
|
|
5
|
-
const start = first === undefined ? 1 : first, end = last === undefined ? total : last;
|
|
6
|
-
const bounds = `total lines: ${total}; valid range: 1-${total}`;
|
|
7
|
-
if (typeof start !== "number" || typeof end !== "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < 1) {
|
|
8
|
-
return { ok: false, message: `read start and end must be positive safe integers (${bounds})` };
|
|
9
|
-
}
|
|
10
|
-
if (start > total) return { ok: false, message: `read start ${start} is beyond end of file (${bounds}); choose a start in that range` };
|
|
11
|
-
if (end < start) return { ok: false, message: `read end ${end} precedes start ${start} (${bounds})` };
|
|
12
|
-
const actualEnd = Math.min(end, total);
|
|
13
|
-
return { ok: true, start, end: actualEnd, notice: readNotice(start, actualEnd, total) };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The selection header. `budget` names the byte limit when it, rather than the
|
|
18
|
-
* requested range, ended the selection, so "next start" is never mistaken for
|
|
19
|
-
* the end of what was asked.
|
|
20
|
-
*/
|
|
21
|
-
export function readNotice(start: number, end: number, total: number, budget?: number): string {
|
|
22
|
-
const rest = end < total ? `next start: ${end + 1}${budget === undefined ? "" : `; stopped at the ${budget}-byte output budget`}` : "end of file";
|
|
23
|
-
return `[Read selection: lines ${start}-${end}; total lines: ${total}; ${rest}.]`;
|
|
24
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/** R2 / D8: source bytes, an unclipped observation, and the visible projection have distinct identities. */
|
|
2
|
-
import type { ArtifactRef } from "./events.ts";
|
|
3
|
-
import { artifactRecovery } from "./prompt-policy.ts";
|
|
4
|
-
import { headTailSelection, isObservationArtifact as ref, isBytePreview, type BytePreview } from "./observation-view.ts";
|
|
5
|
-
|
|
6
|
-
export interface ObservationReceipt {
|
|
7
|
-
version: 1;
|
|
8
|
-
source: { host: string; workspace: string; resource: string; sha256: string; raw: ArtifactRef };
|
|
9
|
-
artifact: ArtifactRef;
|
|
10
|
-
/** R4: content-addressed version premise, separate from this formatted observation. */
|
|
11
|
-
descriptor?: ArtifactRef;
|
|
12
|
-
selection: { start: number; end: number; total_lines: number };
|
|
13
|
-
preview: BytePreview;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const encoder = new TextEncoder();
|
|
17
|
-
const size = (text: string) => encoder.encode(text).length;
|
|
18
|
-
/** Existing assumed preview cap; archival removes data loss without changing the display policy. */
|
|
19
|
-
const READ_MAX_LINE_CHARS = 2000;
|
|
20
|
-
|
|
21
|
-
export function readObservationText(header: string, lines: readonly string[], start: number): string {
|
|
22
|
-
return header + lines.map((line, i) => `${start + i}\t${line}`).join("\n");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Map two successive cuts (per line and total bytes) back to the same immutable observation. */
|
|
26
|
-
export function previewReadObservation(header: string, lines: readonly string[], start: number, artifact: string,
|
|
27
|
-
maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): { text: string; preview: ObservationReceipt["preview"]; semantic: string } {
|
|
28
|
-
const spans: { view: number; source: number; bytes: number }[] = [];
|
|
29
|
-
let text = "", source = 0, view = 0;
|
|
30
|
-
const mapped = (value: string, offset: number) => {
|
|
31
|
-
const bytes = size(value);
|
|
32
|
-
if (bytes) spans.push({ view, source: offset, bytes });
|
|
33
|
-
text += value; view += bytes;
|
|
34
|
-
};
|
|
35
|
-
const synthetic = (value: string) => { text += value; view += size(value); };
|
|
36
|
-
mapped(header, 0); source += size(header);
|
|
37
|
-
for (let i = 0; i < lines.length; i++) {
|
|
38
|
-
const line = lines[i]!, prefix = `${start + i}\t`;
|
|
39
|
-
let end = Math.min(line.length, maxLineChars);
|
|
40
|
-
// JavaScript string indices are UTF-16; a character can occupy a surrogate pair.
|
|
41
|
-
if (end > 0 && end < line.length && /[\uD800-\uDBFF]/.test(line[end - 1]!)) end--;
|
|
42
|
-
const kept = prefix + line.slice(0, end);
|
|
43
|
-
mapped(kept, source);
|
|
44
|
-
if (end < line.length) synthetic(` [line truncated: ${line.length} chars; ${artifactRecovery(artifact, source + size(kept), tools)}]`);
|
|
45
|
-
source += size(prefix + line);
|
|
46
|
-
if (i < lines.length - 1) { mapped("\n", source); source++; }
|
|
47
|
-
}
|
|
48
|
-
const bytes = encoder.encode(text);
|
|
49
|
-
const selected = headTailSelection(bytes, bytes, bytes.length, maxBytes,
|
|
50
|
-
omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
|
|
51
|
-
const visible: [number, number][] = [];
|
|
52
|
-
for (const [lo, hi] of selected.visible) for (const span of spans) {
|
|
53
|
-
const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
|
|
54
|
-
if (end <= begin) continue;
|
|
55
|
-
const a = span.source + begin - span.view, b = span.source + end - span.view;
|
|
56
|
-
const last = visible.at(-1);
|
|
57
|
-
if (last?.[1] === a) last[1] = b;
|
|
58
|
-
else visible.push([a, b]);
|
|
59
|
-
}
|
|
60
|
-
let next = 0;
|
|
61
|
-
for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
|
|
62
|
-
const full = encoder.encode(readObservationText(header, lines, start));
|
|
63
|
-
const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(Math.max(lo, size(header)), Math.max(hi, size(header))))).join("");
|
|
64
|
-
return { text: selected.text, semantic, preview: { unit: "utf8_bytes", complete: next === source,
|
|
65
|
-
visible, next_offset: next === source ? null : next } };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* How many of `lines` a read shows whole within `maxBytes`. OpenCode and Pi cut
|
|
70
|
-
* a file read at a line boundary and name the next offset; eliding the middle
|
|
71
|
-
* lets a receipt claim "end of file" while hiding the lines between, so the
|
|
72
|
-
* model re-reads them and the stall ledger, trusting the claimed range, counts
|
|
73
|
-
* that as no progress. At least one line is kept: a single line over budget
|
|
74
|
-
* is still cut inside itself, with its recovery reference.
|
|
75
|
-
*/
|
|
76
|
-
export function linesWithin(header: (count: number) => string, lines: readonly string[], start: number,
|
|
77
|
-
maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): number {
|
|
78
|
-
// Rendered without a byte cut, so only the budget decides; a line cut at
|
|
79
|
-
// maxLineChars is already part of the rendering. Any 64-hex artifact name
|
|
80
|
-
// renders recovery references at their final length.
|
|
81
|
-
const fits = (count: number) => size(previewReadObservation(header(count), lines.slice(0, count), start, "0".repeat(64),
|
|
82
|
-
Number.MAX_SAFE_INTEGER, maxLineChars, tools).text) <= maxBytes;
|
|
83
|
-
if (lines.length === 0 || fits(lines.length)) return lines.length;
|
|
84
|
-
let low = 1, high = lines.length - 1;
|
|
85
|
-
while (low < high) {
|
|
86
|
-
const mid = Math.ceil((low + high) / 2);
|
|
87
|
-
if (fits(mid)) low = mid; else high = mid - 1;
|
|
88
|
-
}
|
|
89
|
-
return low;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
93
|
-
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
94
|
-
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
95
|
-
|
|
96
|
-
/** Optional on old logs, strict when present. A cursor cannot certify bytes outside its artifact. */
|
|
97
|
-
export function isObservationReceipt(value: unknown): value is ObservationReceipt {
|
|
98
|
-
if (!record(value) || value["version"] !== 1 || !ref(value["artifact"])) return false;
|
|
99
|
-
if (value["descriptor"] !== undefined && !ref(value["descriptor"])) return false;
|
|
100
|
-
const source = value["source"], selection = value["selection"], preview = value["preview"];
|
|
101
|
-
if (!record(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") ||
|
|
102
|
-
!digest(source["sha256"]) || !ref(source["raw"]) || source["sha256"] !== source["raw"].sha256) return false;
|
|
103
|
-
if (!record(selection) || !natural(selection["start"]) || selection["start"] < 1 || !natural(selection["end"]) ||
|
|
104
|
-
!natural(selection["total_lines"]) || selection["end"] < selection["start"] || selection["end"] > selection["total_lines"]) return false;
|
|
105
|
-
return isBytePreview(preview, value["artifact"].bytes);
|
|
106
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/** R9 / D8: navigation into original ancestry, never a semantic summary or permission grant. */
|
|
2
|
-
import type { LogEvent } from "./events.ts";
|
|
3
|
-
import { historyEntries, type HistoryEntry } from "./history-query.ts";
|
|
4
|
-
import { isEnvironmentReceipt } from "./environment.ts";
|
|
5
|
-
import { resourceKey } from "./evidence-links.ts";
|
|
6
|
-
import type { ToolDiagnostic } from "./tool-contract.ts";
|
|
7
|
-
|
|
8
|
-
export const RECOVERY_SECTIONS = ["users", "failures", "environment", "next_actions"] as const;
|
|
9
|
-
export type RecoverySection = typeof RECOVERY_SECTIONS[number];
|
|
10
|
-
export const RECOVERY_KINDS = { users: "user", failures: "failure", environment: "environment", next_actions: "next_action" } as const;
|
|
11
|
-
export type RecoveryNodes = readonly { node: number; events: readonly LogEvent[] }[];
|
|
12
|
-
export type RecoveryReference = {
|
|
13
|
-
node: number; event: string; kind: "user" | "failure" | "environment" | "next_action";
|
|
14
|
-
/** This is the recorded role, not proof of current authorization or human authorship. */
|
|
15
|
-
provenance: "user_message" | "observation" | "model_annotation";
|
|
16
|
-
abandoned?: true; tool?: string; work?: string;
|
|
17
|
-
resources?: readonly string[];
|
|
18
|
-
/** A failed request can name a resource without having observed it. */
|
|
19
|
-
requested_resource?: string;
|
|
20
|
-
diagnostic?: Pick<ToolDiagnostic, "code" | "execution" | "effects">;
|
|
21
|
-
host?: string; workspace?: string; cwd?: string; program?: string;
|
|
22
|
-
};
|
|
23
|
-
export type RecoverySources = Record<RecoverySection, RecoveryReference[]>;
|
|
24
|
-
export type RecoveryRanges = Record<RecoverySection, { total: number; omitted: number }>;
|
|
25
|
-
|
|
26
|
-
/** Newest first within each role; no keywords decide which user sentence is a constraint. */
|
|
27
|
-
export function recoverySources(nodes: RecoveryNodes, entries: readonly HistoryEntry[] = historyEntries(nodes)): RecoverySources {
|
|
28
|
-
const abandoned = new Set(nodes.flatMap(part => part.events.flatMap(event => event.t === "interrupt" ? [event.user_id] : [])));
|
|
29
|
-
const result: RecoverySources = { users: [], failures: [], environment: [], next_actions: [] };
|
|
30
|
-
for (const entry of entries.toReversed()) {
|
|
31
|
-
const source = { node: entry.node, event: entry.event };
|
|
32
|
-
if (entry.kind === "user") result.users.push({ ...source, kind: "user", provenance: "user_message",
|
|
33
|
-
...(abandoned.has(entry.event) ? { abandoned: true } : {}) });
|
|
34
|
-
if (entry.kind === "next_action") result.next_actions.push({ ...source, kind: "next_action", provenance: "model_annotation",
|
|
35
|
-
...(entry.work_ids?.[0] ? { work: entry.work_ids[0] } : {}) });
|
|
36
|
-
if (entry.kind !== "tool_result") continue;
|
|
37
|
-
if (entry.ok === false) {
|
|
38
|
-
const input = entry.input !== null && typeof entry.input === "object" ? entry.input as Record<string, unknown> : {};
|
|
39
|
-
const requested = typeof input["path"] === "string" ? input["path"] : typeof input["cwd"] === "string" ? input["cwd"] : undefined;
|
|
40
|
-
const diagnostic = entry.diagnostic;
|
|
41
|
-
result.failures.push({ ...source, kind: "failure", provenance: "observation", ...(entry.tool ? { tool: entry.tool } : {}),
|
|
42
|
-
...(entry.resources?.length ? { resources: entry.resources } : {}),
|
|
43
|
-
...(requested ? { requested_resource: resourceKey(requested) } : {}),
|
|
44
|
-
...(diagnostic ? { diagnostic: { code: diagnostic.code,
|
|
45
|
-
...(diagnostic.execution ? { execution: diagnostic.execution } : {}), ...(diagnostic.effects ? { effects: diagnostic.effects } : {}) } } : {}) });
|
|
46
|
-
}
|
|
47
|
-
if (entry.tool === "environment") {
|
|
48
|
-
const receipt = entry.data;
|
|
49
|
-
// Only a validated actual receipt contributes identity. Text that looks
|
|
50
|
-
// like a host or a successful probe does not become a machine fact (D11).
|
|
51
|
-
const identity = isEnvironmentReceipt(receipt) && entry.effect?.kind === "command" && entry.effect.exit === receipt.evidence.exit
|
|
52
|
-
? { host: receipt.source.host, workspace: receipt.source.workspace, cwd: receipt.source.cwd, program: receipt.query.program } : {};
|
|
53
|
-
result.environment.push({ ...source, kind: "environment", provenance: "observation", ...identity });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Source labels are stable even when the runtime attaches a fresh validity view. */
|
|
60
|
-
export function recoveryPage(sources: RecoverySources, entries: readonly HistoryEntry[], section: RecoverySection, offset: number, limit: number): {
|
|
61
|
-
total: number; offset: number; next: number | null; items: { source: RecoveryReference; original: HistoryEntry }[];
|
|
62
|
-
} {
|
|
63
|
-
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
64
|
-
throw new RangeError("recovery offset must be nonnegative and limit must be 1..100");
|
|
65
|
-
}
|
|
66
|
-
const indexed = new Map(entries.map(entry => [entry.event, entry]));
|
|
67
|
-
const references = sources[section], end = Math.min(references.length, offset + limit);
|
|
68
|
-
return { total: references.length, offset, next: end < references.length ? end : null,
|
|
69
|
-
items: references.slice(offset, end).map(source => {
|
|
70
|
-
const original = indexed.get(source.event);
|
|
71
|
-
if (!original || original.node !== source.node) throw new Error("recovery source is absent from the selected ancestry");
|
|
72
|
-
return { source, original };
|
|
73
|
-
}) };
|
|
74
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brings the ripgrep regex dialect to JS RegExp where the two differ in
|
|
3
|
-
* spelling, not meaning.
|
|
4
|
-
*
|
|
5
|
-
* grep runs ripgrep when it is on PATH and a TypeScript walk otherwise, and
|
|
6
|
-
* the model writes for ripgrep (Rust regex), where a leading `(?i)` is the
|
|
7
|
-
* usual way to ask for case-insensitivity. JS rejects that spelling but
|
|
8
|
-
* accepts the scoped form `(?i:...)`, so a leading global group is rewritten
|
|
9
|
-
* into a scoped group around the rest of the pattern. Without this the same
|
|
10
|
-
* call works or fails depending on whether rg happens to be installed.
|
|
11
|
-
*
|
|
12
|
-
* Only the leading form and only flags JS modifiers support (i, m, s) are
|
|
13
|
-
* rewritten; anything else is passed through so RegExp reports it as it is.
|
|
14
|
-
*/
|
|
15
|
-
const LEADING_FLAGS = /^\(\?([ims]+)\)/;
|
|
16
|
-
|
|
17
|
-
export function jsPatternFromRipgrep(pattern: string): string {
|
|
18
|
-
const match = LEADING_FLAGS.exec(pattern);
|
|
19
|
-
if (match === null) return pattern;
|
|
20
|
-
const flags = match[1]!;
|
|
21
|
-
// A repeated flag is a SyntaxError inside a modifier group; ripgrep
|
|
22
|
-
// tolerates it, so duplicates are dropped rather than surfaced.
|
|
23
|
-
const unique = [...new Set(flags)].join("");
|
|
24
|
-
return `(?${unique}:${pattern.slice(match[0].length)})`;
|
|
25
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* D6 / Q.26: finalization spends the existing budget and cannot admit new work.
|
|
3
|
-
* `Infinity` is no cap (S.6 item 1): there is no final request to reserve, and
|
|
4
|
-
* the stall watchdog is what ends a run that stops producing evidence.
|
|
5
|
-
*/
|
|
6
|
-
export function requestPhase(index: number, limit: number): "work" | "finalize" | "exhausted" {
|
|
7
|
-
if (!(Number.isSafeInteger(limit) || limit === Infinity) || limit < 1 || !Number.isSafeInteger(index) || index < 0)
|
|
8
|
-
throw new RangeError("request budget requires a positive integer or unbounded limit and a nonnegative index");
|
|
9
|
-
if (index >= limit) return "exhausted";
|
|
10
|
-
// One request cannot both investigate and close. Preserve that work opportunity;
|
|
11
|
-
// its deterministic outcome is the exit, without a second paid request.
|
|
12
|
-
return limit > 1 && index === limit - 1 ? "finalize" : "work";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function finalizationNotice(): string {
|
|
16
|
-
return "Harness: this is the final request in the existing budget. Tools are disabled by the host. Give the user a concise answer from the observations already recorded: what is established, what remains unresolved, and the next useful action. Do not claim unobserved execution or verified completion.";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Without a request cap the stall watchdog is the only backstop, so it cannot be off (S.6 item 1). */
|
|
20
|
-
export function validateRequestBudget(limit: number, stallLimit: number): void {
|
|
21
|
-
requestPhase(0, limit);
|
|
22
|
-
if (limit === Infinity && stallLimit === 0) throw new RangeError("an unbounded request budget requires the stall watchdog");
|
|
23
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/** Q23 / D9: diagnostic observations, never a new anti-stall or success policy. */
|
|
2
|
-
import type { LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
-
import { deriveAcceptance, type AcceptanceState } from "./acceptance.ts";
|
|
4
|
-
import { advanceRecovery, emptyRecoveryState, failureIdentity } from "./failure-recovery.ts";
|
|
5
|
-
import { canonicalValue } from "./continuation.ts";
|
|
6
|
-
import { changedPaths } from "./checkpoints.ts";
|
|
7
|
-
|
|
8
|
-
export type RequestSource = { node: number; event: string };
|
|
9
|
-
export type RequestRecord = { node: number; event: LogEvent };
|
|
10
|
-
export type RequestProgress = Record<"observations" | "artifact_changes" | "acceptance_advances" |
|
|
11
|
-
"new_diagnostics" | "unchanged_retries" | "unknown_diagnostics", RequestSource[]>;
|
|
12
|
-
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
-
|
|
14
|
-
function observationDigest(event: ToolResultEvent): string | undefined {
|
|
15
|
-
if (event.observation_sha256 !== undefined) return event.observation_sha256;
|
|
16
|
-
// Command receipts archive stdout/stderr before the timing/session header is
|
|
17
|
-
// rendered. Only that explicit matching output reference is a safe fallback.
|
|
18
|
-
const data = event.data;
|
|
19
|
-
if (event.effect?.kind !== "command" || !event.content_ref || !object(data) || data["capture"] === "partial") return undefined;
|
|
20
|
-
const output = data["output"];
|
|
21
|
-
return object(output) &&
|
|
22
|
-
output["sha256"] === event.content_ref.sha256 && output["bytes"] === event.content_ref.bytes ? event.content_ref.sha256 : undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function deriveRequestProgress(records: readonly RequestRecord[]): RequestProgress {
|
|
26
|
-
const result: RequestProgress = { observations: [], artifact_changes: [], acceptance_advances: [],
|
|
27
|
-
new_diagnostics: [], unchanged_retries: [], unknown_diagnostics: [] };
|
|
28
|
-
const observations = new Set<string>(), versions = new Set<string>(), failures = new Set<string>();
|
|
29
|
-
let acceptance: AcceptanceState | undefined;
|
|
30
|
-
let recovery = emptyRecoveryState();
|
|
31
|
-
const version = (path: string, before: string | null, after: string | null, source: RequestSource) => {
|
|
32
|
-
const key = (sha: string | null) => canonicalValue([path, sha]);
|
|
33
|
-
versions.add(key(before));
|
|
34
|
-
if (!versions.has(key(after))) result.artifact_changes.push(source);
|
|
35
|
-
versions.add(key(after));
|
|
36
|
-
};
|
|
37
|
-
for (const { node, event } of records) {
|
|
38
|
-
const source = { node, event: event.id };
|
|
39
|
-
const before = acceptance;
|
|
40
|
-
acceptance = deriveAcceptance(acceptance, [event]);
|
|
41
|
-
if (acceptance && before?.contract_event === acceptance.contract_event && acceptance.criteria.some(item =>
|
|
42
|
-
(item.status === "pass" || item.status === "accepted") &&
|
|
43
|
-
before.criteria.find(old => old.id === item.id)?.status !== item.status)) result.acceptance_advances.push(source);
|
|
44
|
-
if (event.t === "tool_result") {
|
|
45
|
-
const effect = event.effect;
|
|
46
|
-
const call = recovery.calls.get(event.call_id);
|
|
47
|
-
if (effect?.kind === "edit") version(effect.path, effect.sha256_before, effect.sha256_after, source);
|
|
48
|
-
if (effect?.kind === "read") versions.add(canonicalValue([effect.path, effect.sha256]));
|
|
49
|
-
// Semantic digests exclude command timing/preview metadata. Legacy raw
|
|
50
|
-
// output hashes cannot prove a new diagnostic and stay explicitly unknown.
|
|
51
|
-
const digest = observationDigest(event);
|
|
52
|
-
if (!event.ok) {
|
|
53
|
-
const identity = call ? failureIdentity(call, event, recovery, path => path) : null;
|
|
54
|
-
if (digest === undefined || identity === null) result.unknown_diagnostics.push(source);
|
|
55
|
-
else {
|
|
56
|
-
const key = canonicalValue([identity, digest]);
|
|
57
|
-
(failures.has(key) ? result.unchanged_retries : result.new_diagnostics).push(source);
|
|
58
|
-
failures.add(key);
|
|
59
|
-
}
|
|
60
|
-
} else if ((effect?.kind === "read" || effect?.kind === "command" ||
|
|
61
|
-
["grep", "find", "ls", "query", "history", "environment"].includes(call?.name ?? "")) &&
|
|
62
|
-
digest !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 && !observations.has(digest)) {
|
|
63
|
-
result.observations.push(source); observations.add(digest);
|
|
64
|
-
}
|
|
65
|
-
if (effect?.kind === "command") for (const output of effect.artifacts?.outputs ?? []) {
|
|
66
|
-
if (output.after.state === "observed") version(output.path,
|
|
67
|
-
output.before.state === "observed" ? output.before.sha256 : null, output.after.sha256, source);
|
|
68
|
-
}
|
|
69
|
-
} else if (event.t === "checkpoint" && event.phase === "applied") {
|
|
70
|
-
for (const path of changedPaths(event.changes)) {
|
|
71
|
-
const change = event.changes[path]!;
|
|
72
|
-
version(path, change.before?.sha256 ?? null, change.after?.sha256 ?? null, source);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
recovery = advanceRecovery(recovery, [event], path => path);
|
|
76
|
-
}
|
|
77
|
-
// One receipt may change several artifacts or satisfy several criteria.
|
|
78
|
-
for (const key of Object.keys(result) as (keyof RequestProgress)[]) {
|
|
79
|
-
const seen = new Set<string>();
|
|
80
|
-
result[key] = result[key].filter(source => {
|
|
81
|
-
const id = canonicalValue(source);
|
|
82
|
-
if (seen.has(id)) return false;
|
|
83
|
-
seen.add(id); return true;
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
return result;
|
|
87
|
-
}
|