@visual-z/xeno 0.1.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/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- package/src/vendor/pi-tui/word-navigation.ts +117 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a provider error means "the prompt does not fit the window".
|
|
3
|
+
*
|
|
4
|
+
* That failure needs a different remedy from every other one: retrying the
|
|
5
|
+
* same request fails the same way (so it must not be retried even when a
|
|
6
|
+
* relay reports it as a 5xx), and the only thing that makes it succeed is a
|
|
7
|
+
* smaller projection. Providers word it differently and none shares a code,
|
|
8
|
+
* so recognition is by message text.
|
|
9
|
+
*
|
|
10
|
+
* Patterns ported from pi-mono `packages/ai/src/utils/overflow.ts` (MIT,
|
|
11
|
+
* commit 6f75515); the provider each one came from is kept beside it. A
|
|
12
|
+
* provider not listed here surfaces as an ordinary error, which is the safe
|
|
13
|
+
* direction: a missed overflow ends the turn visibly, a false one would cut
|
|
14
|
+
* a healthy context.
|
|
15
|
+
*/
|
|
16
|
+
const OVERFLOW_PATTERNS: readonly RegExp[] = [
|
|
17
|
+
/prompt (?:is )?too long/i, // Anthropic, z.ai
|
|
18
|
+
/request_too_large/i, // Anthropic byte-size overflow (413)
|
|
19
|
+
/input is too long for requested model/i, // Amazon Bedrock
|
|
20
|
+
/exceeds the context window/i, // OpenAI (Completions & Responses)
|
|
21
|
+
/exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, // OpenAI-compatible proxies
|
|
22
|
+
/input token count.*exceeds the maximum/i, // Google Gemini
|
|
23
|
+
/maximum prompt length is \d+/i, // xAI
|
|
24
|
+
/reduce the length of the messages/i, // Groq
|
|
25
|
+
/maximum context length is \d+ tokens/i, // OpenRouter
|
|
26
|
+
/exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i, // OpenRouter / Poolside
|
|
27
|
+
/input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, // Together AI
|
|
28
|
+
/exceeds the limit of \d+/i, // GitHub Copilot
|
|
29
|
+
/exceeds the available context size/i, // llama.cpp
|
|
30
|
+
/greater than the context length/i, // LM Studio
|
|
31
|
+
/context window exceeds limit/i, // MiniMax
|
|
32
|
+
/exceeded model token limit/i, // Kimi
|
|
33
|
+
/too large for model with \d+ maximum context length/i, // Mistral
|
|
34
|
+
/prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, // DS4
|
|
35
|
+
/model_context_window_exceeded/i, // z.ai finish reason surfaced as error text
|
|
36
|
+
/prompt too long; exceeded (?:max )?context length/i, // Ollama
|
|
37
|
+
/range of input length should be/i, // DashScope / Qwen
|
|
38
|
+
/context[_ ]length[_ ]exceeded/i, // generic
|
|
39
|
+
/too many tokens/i, // generic
|
|
40
|
+
/token limit exceeded/i, // generic
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Throttling that happens to mention tokens; excluded even when a pattern above matches. */
|
|
44
|
+
const NOT_OVERFLOW: readonly RegExp[] = [
|
|
45
|
+
/^(Throttling error|Service unavailable):/i,
|
|
46
|
+
/throttl/i,
|
|
47
|
+
/rate limit/i,
|
|
48
|
+
/too many requests/i,
|
|
49
|
+
/please wait/i,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Cerebras answers an oversized request with a bodiless 400 / 413. */
|
|
53
|
+
const BODYLESS = /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i;
|
|
54
|
+
|
|
55
|
+
export function isContextOverflow(message: string, status?: number): boolean {
|
|
56
|
+
if (status === 429) return false;
|
|
57
|
+
if (NOT_OVERFLOW.some((pattern) => pattern.test(message))) return false;
|
|
58
|
+
return OVERFLOW_PATTERNS.some((pattern) => pattern.test(message)) || BODYLESS.test(message.trim());
|
|
59
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure permission rules: which tools the model may see, and whether a
|
|
3
|
+
* call is allow / ask / deny.
|
|
4
|
+
*
|
|
5
|
+
* Approval is the last look at what is about to run, not a boundary.
|
|
6
|
+
* Path tools are already confined to the workspace root; bash is not,
|
|
7
|
+
* which is why bash defaults to ask and everything else to allow.
|
|
8
|
+
*
|
|
9
|
+
* This module must stay free of I/O, clocks, and randomness. The CLI
|
|
10
|
+
* compiles flags and session memory into a rule list; this file only
|
|
11
|
+
* matches them. There is no command-danger heuristic: a screen that
|
|
12
|
+
* misses `find . -delete` is worse than none, because it implies
|
|
13
|
+
* someone is checking.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type PermissionAction = "allow" | "ask" | "deny";
|
|
17
|
+
|
|
18
|
+
export interface PermissionRequest {
|
|
19
|
+
readonly tool: string;
|
|
20
|
+
/** Exact command string for bash; omitted for tools that have none. */
|
|
21
|
+
readonly command?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PermissionRule {
|
|
25
|
+
readonly action: PermissionAction;
|
|
26
|
+
/** Absent matches every tool. */
|
|
27
|
+
readonly tool?: string;
|
|
28
|
+
/** Absent matches every command. When set, matches only this exact string. */
|
|
29
|
+
readonly command?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Who contributed the tool. `builtin` is the only producer today.
|
|
34
|
+
* `extension` is in the union so `--no-builtin-tools` can keep a
|
|
35
|
+
* non-builtin tool without a later schema change across layers.
|
|
36
|
+
*/
|
|
37
|
+
export type ToolSource = "builtin" | "extension";
|
|
38
|
+
|
|
39
|
+
export interface ToolSelection {
|
|
40
|
+
/** `--tools`: a strict allowlist. */
|
|
41
|
+
readonly only?: readonly string[];
|
|
42
|
+
/** `--exclude-tools`: removed after the allowlist (or the full set) is built. */
|
|
43
|
+
readonly exclude?: readonly string[];
|
|
44
|
+
/** `--no-tools`: start from nothing. Ignored when `only` is set. */
|
|
45
|
+
readonly none?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* `--no-builtin-tools`. Drops `source === "builtin"` (and untagged tools,
|
|
48
|
+
* which are builtin). Ignored when `only` is set: an explicit `--tools`
|
|
49
|
+
* names a specific tool and is a stronger statement than "not that category".
|
|
50
|
+
*/
|
|
51
|
+
readonly noBuiltin?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The flag a no-tty refusal names so a pipeline can choose explicitly.
|
|
56
|
+
*
|
|
57
|
+
* Session memory is not this flag: session memory dies with the process.
|
|
58
|
+
* A persisted "always allow" would fire months later when nobody is watching.
|
|
59
|
+
*/
|
|
60
|
+
export const ALLOW_TOOLS_FLAG = "--allow-tools";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Product default. bash asks because it is not confined to the workspace
|
|
64
|
+
* root; every other built-in is allow because the path tools are.
|
|
65
|
+
*/
|
|
66
|
+
export function defaultRules(): readonly PermissionRule[] {
|
|
67
|
+
return [
|
|
68
|
+
{ action: "ask", tool: "bash" },
|
|
69
|
+
// Same reach as bash. write_stdin needs no rule of its own: it can only
|
|
70
|
+
// address a session exec_command already started, under this approval.
|
|
71
|
+
{ action: "ask", tool: "exec_command" },
|
|
72
|
+
{ action: "allow" },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `--allow-tools` compiled to rules, in front of the defaults so they win.
|
|
78
|
+
*/
|
|
79
|
+
export function rulesWithAllowTools(allowTools: readonly string[]): PermissionRule[] {
|
|
80
|
+
return [
|
|
81
|
+
...allowTools.map((tool) => ({ action: "allow" as const, tool })),
|
|
82
|
+
...defaultRules(),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function ruleMatches(rule: PermissionRule, request: PermissionRequest): boolean {
|
|
87
|
+
if (rule.tool !== undefined && rule.tool !== request.tool) return false;
|
|
88
|
+
if (rule.command !== undefined && rule.command !== request.command) return false;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* First matching rule wins. An empty list allows: the tool tests pass no
|
|
94
|
+
* rules because they are testing the tool, not the gate. The CLI always
|
|
95
|
+
* supplies defaultRules().
|
|
96
|
+
*/
|
|
97
|
+
export function decide(
|
|
98
|
+
rules: readonly PermissionRule[],
|
|
99
|
+
request: PermissionRequest,
|
|
100
|
+
): PermissionAction {
|
|
101
|
+
for (const rule of rules) {
|
|
102
|
+
if (ruleMatches(rule, request)) return rule.action;
|
|
103
|
+
}
|
|
104
|
+
return "allow";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Comma-separated flag values. Empty tokens are dropped so `--tools a,,b`
|
|
109
|
+
* does not invent a blank name.
|
|
110
|
+
*/
|
|
111
|
+
export function parseToolList(raw: string): string[] {
|
|
112
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Names the model will see. An excluded tool has to vanish here -- leaving
|
|
117
|
+
* it in the schema and failing at call time teaches the model to retry
|
|
118
|
+
* the same call and burn tokens.
|
|
119
|
+
*
|
|
120
|
+
* `--tools` is a strict allowlist. `--no-tools` starts from nothing, then
|
|
121
|
+
* `--tools` can add names back (pi's combination). `--exclude-tools` is
|
|
122
|
+
* applied last.
|
|
123
|
+
*/
|
|
124
|
+
export function selectToolNames(
|
|
125
|
+
available: readonly string[],
|
|
126
|
+
selection: ToolSelection,
|
|
127
|
+
): string[] {
|
|
128
|
+
let names: string[];
|
|
129
|
+
if (selection.only !== undefined) {
|
|
130
|
+
const allow = new Set(selection.only);
|
|
131
|
+
names = available.filter((n) => allow.has(n));
|
|
132
|
+
} else if (selection.none === true) {
|
|
133
|
+
names = [];
|
|
134
|
+
} else {
|
|
135
|
+
names = [...available];
|
|
136
|
+
}
|
|
137
|
+
if (selection.exclude !== undefined) {
|
|
138
|
+
const ex = new Set(selection.exclude);
|
|
139
|
+
names = names.filter((n) => !ex.has(n));
|
|
140
|
+
}
|
|
141
|
+
return names;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function selectTools<T extends { name: string; source?: ToolSource }>(
|
|
145
|
+
tools: readonly T[],
|
|
146
|
+
selection: ToolSelection,
|
|
147
|
+
): T[] {
|
|
148
|
+
const names = new Set(selectToolNames(tools.map((t) => t.name), selection));
|
|
149
|
+
let selected = tools.filter((t) => names.has(t.name));
|
|
150
|
+
// `--tools` already named specific tools, so the category filter does
|
|
151
|
+
// not get a second veto. Untagged tools are builtin: that is today's
|
|
152
|
+
// only producer, and treating absence as "keep" would make the flag a
|
|
153
|
+
// no-op until every schema is recut.
|
|
154
|
+
if (selection.noBuiltin === true && selection.only === undefined) {
|
|
155
|
+
selected = selected.filter((t) => (t.source ?? "builtin") !== "builtin");
|
|
156
|
+
}
|
|
157
|
+
return selected;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function permissionRequest(name: string, input: unknown): PermissionRequest {
|
|
161
|
+
if ((name !== "bash" && name !== "exec_command") || typeof input !== "object" || input === null) {
|
|
162
|
+
return { tool: name };
|
|
163
|
+
}
|
|
164
|
+
const command = (input as { readonly [k: string]: unknown })[name === "bash" ? "command" : "cmd"];
|
|
165
|
+
if (typeof command !== "string") return { tool: name };
|
|
166
|
+
return { tool: name, command };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Why this ask became a deny: nobody could be asked. Names the flag that
|
|
171
|
+
* would have allowed the call without asking, so a CI author can choose.
|
|
172
|
+
*/
|
|
173
|
+
export function askWithoutTtyMessage(request: PermissionRequest): string {
|
|
174
|
+
return (
|
|
175
|
+
`refused: ${request.tool} requires approval and there is no tty to ask. ` +
|
|
176
|
+
`Pass ${ALLOW_TOOLS_FLAG} ${request.tool} to allow it without asking.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function deniedMessage(request: PermissionRequest): string {
|
|
181
|
+
return `refused: ${request.tool} was denied`;
|
|
182
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** N.7: repetitions stay attached to their task; they are not independent questions. */
|
|
2
|
+
import { type ContextPolicy } from "./context-policy.ts";
|
|
3
|
+
import { shuffled } from "./experiment.ts";
|
|
4
|
+
|
|
5
|
+
// N.7 is a frozen three-arm protocol; new production policies cannot change it.
|
|
6
|
+
const EVALUATED_POLICIES: readonly ContextPolicy[] = ["segmented", "continuous", "adaptive"];
|
|
7
|
+
|
|
8
|
+
export interface PolicyJob { id: string; policy: ContextPolicy; repeat: number }
|
|
9
|
+
export function policyJobKey(job: PolicyJob): string { return JSON.stringify([job.id, job.policy, job.repeat]); }
|
|
10
|
+
export function policySchedule(ids: readonly string[], repetitions: number, seed: number): PolicyJob[] {
|
|
11
|
+
if (ids.length === 0 || new Set(ids).size !== ids.length || !Number.isSafeInteger(repetitions) || repetitions < 1) throw new Error("invalid policy trial design");
|
|
12
|
+
const policies = shuffled(EVALUATED_POLICIES, seed);
|
|
13
|
+
const jobs: PolicyJob[] = [];
|
|
14
|
+
let block = 0;
|
|
15
|
+
for (let repeat = 0; repeat < repetitions; repeat++) for (const id of shuffled([...ids].sort(), seed + repeat + 1)) {
|
|
16
|
+
for (let position = 0; position < policies.length; position++) jobs.push({ id, repeat, policy: policies[(block + position) % policies.length]! });
|
|
17
|
+
block++;
|
|
18
|
+
}
|
|
19
|
+
return jobs;
|
|
20
|
+
}
|
|
21
|
+
export interface PolicyObservation extends PolicyJob {
|
|
22
|
+
status: string; correct: boolean; peakRateUsd: number; reservedUnknownUsd: number;
|
|
23
|
+
requests: number; cuts: number; incompleteHandoffs: number; wallMs: number;
|
|
24
|
+
usage: { prompt_tokens: number; cache_read_input_tokens: number; output_tokens: number };
|
|
25
|
+
}
|
|
26
|
+
export function summarizePolicies(plan: readonly PolicyJob[], results: readonly PolicyObservation[]) {
|
|
27
|
+
const planned = new Set(plan.map(policyJobKey)), observed = new Set<string>();
|
|
28
|
+
if (planned.size !== plan.length) throw new Error("duplicate planned policy job");
|
|
29
|
+
for (const result of results) {
|
|
30
|
+
const key = policyJobKey(result);
|
|
31
|
+
if (!planned.has(key) || observed.has(key) || (result.correct && result.status !== "answered")) throw new Error("invalid policy result");
|
|
32
|
+
observed.add(key);
|
|
33
|
+
}
|
|
34
|
+
return [...new Set(plan.map((job) => job.policy))].sort().map((policy) => {
|
|
35
|
+
const schedule = plan.filter((job) => job.policy === policy), group = results.filter((row) => row.policy === policy);
|
|
36
|
+
const sum = (read: (row: PolicyObservation) => number) => group.reduce((n, row) => n + read(row), 0);
|
|
37
|
+
const correct = group.filter((row) => row.correct).length;
|
|
38
|
+
const peakRateUsd = sum((row) => row.peakRateUsd), reservedUnknownUsd = sum((row) => row.reservedUnknownUsd);
|
|
39
|
+
const promptTokens = sum((row) => row.usage.prompt_tokens), cachedTokens = sum((row) => row.usage.cache_read_input_tokens);
|
|
40
|
+
return { policy, planned: schedule.length, attempted: group.length, missing: schedule.length - group.length,
|
|
41
|
+
independentTasks: new Set(schedule.map((job) => job.id)).size, correct,
|
|
42
|
+
accuracy: group.length ? correct / group.length : null, complete: group.length === schedule.length,
|
|
43
|
+
plannedSolvedFraction: schedule.length ? correct / schedule.length : null,
|
|
44
|
+
peakRateUsd, reservedUnknownUsd, allAttemptCostPerSuccessUsd: correct ? (peakRateUsd + reservedUnknownUsd) / correct : null,
|
|
45
|
+
promptTokens, cachedTokens, outputTokens: sum((row) => row.usage.output_tokens),
|
|
46
|
+
cacheHitRate: promptTokens ? cachedTokens / promptTokens : null,
|
|
47
|
+
requests: sum((row) => row.requests), cuts: sum((row) => row.cuts), incompleteHandoffs: sum((row) => row.incompleteHandoffs),
|
|
48
|
+
wallMs: sum((row) => row.wallMs),
|
|
49
|
+
tasks: [...new Set(schedule.map((job) => job.id))].map((id) => ({ id,
|
|
50
|
+
planned: schedule.filter((job) => job.id === id).length,
|
|
51
|
+
attempted: group.filter((row) => row.id === id).length,
|
|
52
|
+
correct: group.filter((row) => row.id === id && row.correct).length })),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D9 / research M.4: an anti-stall signal, not a judgment of task completion.
|
|
3
|
+
* Novelty belongs to observed content and executed checks, never note wording,
|
|
4
|
+
* event IDs, or timestamps. The full ledger is rebuilt from the active log path;
|
|
5
|
+
* its carried handoff view may be paged like the other structural state (D20).
|
|
6
|
+
*/
|
|
7
|
+
import type { LineRange, LogEvent } from "./events.ts";
|
|
8
|
+
|
|
9
|
+
export type ProgressEvidence = (
|
|
10
|
+
| { kind: "read"; sha256: string; ranges?: readonly LineRange[]; observation_sha256?: string }
|
|
11
|
+
| { kind: "observation"; sha256: string }
|
|
12
|
+
| { kind: "check"; command: string; gate?: string; workspace_sha256?: string }
|
|
13
|
+
) & { source: { node: number; event_id: string } };
|
|
14
|
+
|
|
15
|
+
/** Test coverage without expanding arbitrarily long ranges into line numbers. */
|
|
16
|
+
function hasUnseenLines(requested: readonly LineRange[], observed: readonly LineRange[]): boolean {
|
|
17
|
+
const sorted = [...observed].sort((a, b) => a[0] - b[0]);
|
|
18
|
+
return requested.some(([start, end]) => {
|
|
19
|
+
let next = start;
|
|
20
|
+
for (const [low, high] of sorted) {
|
|
21
|
+
if (high < next) continue;
|
|
22
|
+
if (low > next) return true;
|
|
23
|
+
if (high >= end) return false;
|
|
24
|
+
next = high + 1;
|
|
25
|
+
}
|
|
26
|
+
return next <= end;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deriveProgress(
|
|
31
|
+
previous: readonly ProgressEvidence[], events: readonly LogEvent[], node: number,
|
|
32
|
+
eligibleChecks: ReadonlySet<string>,
|
|
33
|
+
): { evidence: ProgressEvidence[]; progressed: boolean } {
|
|
34
|
+
const evidence = [...previous];
|
|
35
|
+
const reads = new Map<string, Extract<ProgressEvidence, { kind: "read" }>[]>();
|
|
36
|
+
const observations = new Set<string>();
|
|
37
|
+
const checks = new Set<string>();
|
|
38
|
+
// A named gate has one status per workspace. Changing its arguments does not
|
|
39
|
+
// make another gate, and cannot keep the loop alive by spelling a pass anew.
|
|
40
|
+
const checkKey = (command: string, gate?: string, workspace?: string): string =>
|
|
41
|
+
JSON.stringify([gate === undefined ? "command" : "gate", gate ?? command, workspace ?? null]);
|
|
42
|
+
for (const item of previous) {
|
|
43
|
+
if (item.kind === "read") {
|
|
44
|
+
const seen = reads.get(item.sha256) ?? [];
|
|
45
|
+
seen.push(item);
|
|
46
|
+
reads.set(item.sha256, seen);
|
|
47
|
+
if (item.observation_sha256 !== undefined) observations.add(item.observation_sha256);
|
|
48
|
+
} else if (item.kind === "observation") observations.add(item.sha256);
|
|
49
|
+
else checks.add(checkKey(item.command, item.gate, item.workspace_sha256));
|
|
50
|
+
}
|
|
51
|
+
let progressed = false;
|
|
52
|
+
const calls = new Map<string, string>();
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (event.t === "assistant") {
|
|
55
|
+
for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, block.name);
|
|
56
|
+
}
|
|
57
|
+
if (event.t !== "tool_result") continue;
|
|
58
|
+
const name = calls.get(event.call_id);
|
|
59
|
+
calls.delete(event.call_id);
|
|
60
|
+
if (!event.ok) continue;
|
|
61
|
+
const source = { node, event_id: event.id };
|
|
62
|
+
const effect = event.effect;
|
|
63
|
+
const observation = event.observation_sha256 ?? event.content_ref?.sha256 ?? event.content_sha256;
|
|
64
|
+
if (effect?.kind === "read" && effect.sha256 !== "") {
|
|
65
|
+
const seen = reads.get(effect.sha256) ?? [];
|
|
66
|
+
const novel = !seen.some((r) => r.ranges === undefined) &&
|
|
67
|
+
(effect.ranges === undefined || hasUnseenLines(effect.ranges, seen.flatMap((r) => r.ranges ?? [])));
|
|
68
|
+
if (novel) {
|
|
69
|
+
const item: Extract<ProgressEvidence, { kind: "read" }> = {
|
|
70
|
+
kind: "read", sha256: effect.sha256, source,
|
|
71
|
+
...(effect.ranges === undefined ? {} : { ranges: effect.ranges }),
|
|
72
|
+
...(observation === undefined ? {} : { observation_sha256: observation }),
|
|
73
|
+
};
|
|
74
|
+
evidence.push(item);
|
|
75
|
+
reads.set(effect.sha256, [...seen, item]);
|
|
76
|
+
// Whole-file hashes can change outside the returned range. Such a read
|
|
77
|
+
// extends coverage for that version but unchanged bytes are not novelty.
|
|
78
|
+
if (observation === undefined || !observations.has(observation)) progressed = true;
|
|
79
|
+
if (observation !== undefined) observations.add(observation);
|
|
80
|
+
}
|
|
81
|
+
} else if (effect?.kind === "command" && effect.exit === 0) {
|
|
82
|
+
const key = checkKey(effect.cmd, effect.gate, effect.workspace_sha256);
|
|
83
|
+
if (!checks.has(key)) {
|
|
84
|
+
checks.add(key);
|
|
85
|
+
evidence.push({ kind: "check", command: effect.cmd, source,
|
|
86
|
+
...(effect.gate === undefined ? {} : { gate: effect.gate }),
|
|
87
|
+
...(effect.workspace_sha256 === undefined ? {} : { workspace_sha256: effect.workspace_sha256 }) });
|
|
88
|
+
// Remember even unclaimed successes. A later model note cannot turn a
|
|
89
|
+
// rerun of old evidence into a fresh completion by giving it a new name.
|
|
90
|
+
if (eligibleChecks.has(event.id)) progressed = true;
|
|
91
|
+
}
|
|
92
|
+
} else if ((name === "grep" || name === "find" || name === "ls" || name === "query") &&
|
|
93
|
+
observation !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 &&
|
|
94
|
+
!observations.has(observation)) {
|
|
95
|
+
observations.add(observation);
|
|
96
|
+
evidence.push({ kind: "observation", sha256: observation, source });
|
|
97
|
+
progressed = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { evidence, progressed };
|
|
101
|
+
}
|