@visual-z/xeno 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +126 -34
- package/docs/app-server.md +84 -0
- package/docs/install.md +51 -13
- package/docs/providers.md +24 -3
- package/package.json +7 -5
- package/src/apps/app-server.ts +28 -0
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +45 -8
- package/src/apps/completions.ts +78 -22
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +333 -1542
- package/src/apps/startup-input.ts +45 -9
- package/src/apps/startup.ts +149 -94
- package/src/apps/ui/app.ts +1553 -0
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +86 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +37 -21
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +81 -6
- package/src/core/events.ts +99 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +70 -19
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +33 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-input.ts +6 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +11 -4
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +462 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1088 -282
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +146 -0
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/session-control.ts +198 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +200 -0
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +279 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
package/src/core/environment.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ArtifactRef, LogEvent } from "./events.ts";
|
|
2
|
+
import { isObservationArtifact } from "./observation-view.ts";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Facts about the machine, fixed at session start.
|
|
3
6
|
*
|
|
@@ -27,3 +30,145 @@ export function environmentSection(env: SessionEnvironment): string {
|
|
|
27
30
|
`- Git repository: ${env.git ? "yes" : "no"}`,
|
|
28
31
|
].join("\n");
|
|
29
32
|
}
|
|
33
|
+
|
|
34
|
+
/** R6: these are operational assumptions, not measured freshness guarantees. */
|
|
35
|
+
export const ENVIRONMENT_TTL_MS = 5 * 60_000;
|
|
36
|
+
export const ENVIRONMENT_TIMEOUT_MS = 5_000;
|
|
37
|
+
export const ENVIRONMENT_OUTPUT_BYTES = 256 * 1024;
|
|
38
|
+
export const ENVIRONMENT_PARSE_BYTES = 64 * 1024;
|
|
39
|
+
/** Version reports are metadata; verbose command output remains in the original artifact. */
|
|
40
|
+
export const ENVIRONMENT_VERSION_BYTES = 4096;
|
|
41
|
+
export type EnvironmentProbe = "version" | "python";
|
|
42
|
+
export type EnvironmentSource = { host: string; workspace: string; cwd: string; runtime: string; inherited_sha256: string };
|
|
43
|
+
export type PythonCapability = { executable: string; prefix: string; base_prefix: string; version: string;
|
|
44
|
+
implementation: string; isolated: true; features: { tarfile_extractall_filter: boolean } };
|
|
45
|
+
export type EnvironmentReceipt = {
|
|
46
|
+
kind: "environment"; version: 1;
|
|
47
|
+
query: { program: string; probe: EnvironmentProbe };
|
|
48
|
+
source: EnvironmentSource;
|
|
49
|
+
observed_at: number; expires_at: number;
|
|
50
|
+
state: "observed" | "missing" | "opaque" | "failed" | "unknown";
|
|
51
|
+
shell: { executable: string; version: string; cwd: string; path_sha256: string } | null;
|
|
52
|
+
resolution: { kind: string; executable: string | null } | null;
|
|
53
|
+
reported_version: string | null;
|
|
54
|
+
python?: PythonCapability;
|
|
55
|
+
evidence: { output: ArtifactRef; exit: number; complete: boolean };
|
|
56
|
+
reason?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const quote = (text: string) => "'" + text.replaceAll("'", "'\\''") + "'";
|
|
60
|
+
const marker = "\0XENO_ENVIRONMENT_V1\0";
|
|
61
|
+
const PYTHON_CAPABILITY_PROBE = [
|
|
62
|
+
"import inspect,json,platform,sys,tarfile",
|
|
63
|
+
"print(json.dumps({'executable':sys.executable,'prefix':sys.prefix,'base_prefix':sys.base_prefix,'version':platform.python_version(),'implementation':platform.python_implementation(),'isolated':bool(sys.flags.isolated),'features':{'tarfile_extractall_filter':'filter' in inspect.signature(tarfile.TarFile.extractall).parameters}},sort_keys=True))",
|
|
64
|
+
].join(";");
|
|
65
|
+
|
|
66
|
+
/** Program names are data, including paths with quotes/newlines; no caller code is evaluated. */
|
|
67
|
+
export function environmentProbeCommand(program: unknown, probe: unknown = "version"): string {
|
|
68
|
+
if (typeof program !== "string" || !program || program.includes("\0") || new TextEncoder().encode(program).length > 4096) throw new Error("program must be a nonempty name or path up to 4096 bytes without NUL");
|
|
69
|
+
if (probe !== "version" && probe !== "python") throw new Error("probe must be version or python");
|
|
70
|
+
return [
|
|
71
|
+
"builtin set +e", `xeno_probe_program=${quote(program)}`,
|
|
72
|
+
'xeno_probe_kind=$(builtin type -t -- "$xeno_probe_program" 2>/dev/null)',
|
|
73
|
+
'xeno_probe_path=""',
|
|
74
|
+
'if [ "$xeno_probe_kind" = file ]; then xeno_probe_path=$(builtin type -P -- "$xeno_probe_program" 2>/dev/null); fi',
|
|
75
|
+
// A single frame precedes the program output, which remains untrusted data.
|
|
76
|
+
`builtin printf '\\0XENO_ENVIRONMENT_V1\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0' "$xeno_probe_program" ${quote(probe)} "$xeno_probe_kind" "$xeno_probe_path" "$BASH" "$BASH_VERSION" "$PWD" "$PATH"`,
|
|
77
|
+
'if [ -z "$xeno_probe_kind" ]; then builtin exit 127; fi',
|
|
78
|
+
'if [ "$xeno_probe_kind" != file ] || [ -z "$xeno_probe_path" ]; then builtin exit 125; fi',
|
|
79
|
+
// Replace the discovery shell: the managed group leader and exit belong
|
|
80
|
+
// to the probed program, not an extra shell waiting for its child (R1).
|
|
81
|
+
probe === "python" ? `builtin exec -- "$xeno_probe_path" -I -c ${quote(PYTHON_CAPABILITY_PROBE)}` : 'builtin exec -- "$xeno_probe_path" --version',
|
|
82
|
+
].join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface EnvironmentFrame { program: string; probe: EnvironmentProbe; kind: string; executable: string | null;
|
|
86
|
+
shell: string; shell_version: string; cwd: string; path: string; output: string }
|
|
87
|
+
export function parseEnvironmentFrame(text: string, program: string, probe: EnvironmentProbe): EnvironmentFrame | null {
|
|
88
|
+
const start = text.indexOf(marker);
|
|
89
|
+
if (start < 0) return null;
|
|
90
|
+
const fields: string[] = [];
|
|
91
|
+
let at = start + marker.length;
|
|
92
|
+
for (let i = 0; i < 8; i++) {
|
|
93
|
+
const end = text.indexOf("\0", at);
|
|
94
|
+
if (end < 0) return null;
|
|
95
|
+
fields.push(text.slice(at, end)); at = end + 1;
|
|
96
|
+
}
|
|
97
|
+
if (fields[0] !== program || fields[1] !== probe || !["", "file", "function", "alias", "builtin", "keyword"].includes(fields[2]!) ||
|
|
98
|
+
!fields[4] || !fields[5] || !fields[6] || (fields[2] === "file" && !fields[3])) return null;
|
|
99
|
+
return { program, probe, kind: fields[2]!, executable: fields[3] || null, shell: fields[4]!, shell_version: fields[5]!, cwd: fields[6]!, path: fields[7]!, output: text.slice(at) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
103
|
+
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
104
|
+
const strings = (value: Record<string, unknown>, keys: readonly string[]) => keys.every(key => typeof value[key] === "string" && value[key] !== "");
|
|
105
|
+
export function isPythonCapability(value: unknown): value is PythonCapability {
|
|
106
|
+
return object(value) && strings(value, ["executable", "prefix", "base_prefix", "version", "implementation"]) && value["isolated"] === true &&
|
|
107
|
+
object(value["features"]) && typeof value["features"]["tarfile_extractall_filter"] === "boolean";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** D11: neither a version-looking line nor an interrupted exit zero proves a capability. */
|
|
111
|
+
export function environmentCapability(frame: EnvironmentFrame | null, exit: number, capture: {
|
|
112
|
+
complete: boolean; stopped?: "cancelled" | "timeout"; limited?: "output" | "parse";
|
|
113
|
+
}): Pick<EnvironmentReceipt, "state" | "reported_version" | "python" | "reason"> {
|
|
114
|
+
const unknown = (reason: string) => ({ state: "unknown" as const, reported_version: null, reason });
|
|
115
|
+
if (capture.limited) return unknown(`probe exceeded its ${capture.limited} budget; inspect the original output`);
|
|
116
|
+
if (capture.stopped) return unknown(`probe ${capture.stopped}; inspect partial effects before continuing`);
|
|
117
|
+
if (!capture.complete || !frame) return unknown("probe output is incomplete or its environment frame is invalid");
|
|
118
|
+
if (frame.kind === "" && exit === 127) return { state: "missing", reported_version: null, reason: "program was not found in the command environment" };
|
|
119
|
+
if (frame.kind !== "file") return { state: "opaque", reported_version: null, reason: "shell resolution is not an executable file; specify its exact path" };
|
|
120
|
+
if (exit !== 0) return { state: "failed", reported_version: null, reason: "program exited unsuccessfully; a version-like line is not capability evidence" };
|
|
121
|
+
const output = frame.output.trim();
|
|
122
|
+
if (!output || new TextEncoder().encode(output).length > ENVIRONMENT_VERSION_BYTES) return unknown("version report is empty or exceeds the metadata budget");
|
|
123
|
+
if (frame.probe === "version") return { state: "observed", reported_version: output };
|
|
124
|
+
try {
|
|
125
|
+
const value: unknown = JSON.parse(output);
|
|
126
|
+
if (isPythonCapability(value)) return { state: "observed", reported_version: value.version, python: value };
|
|
127
|
+
} catch { /* The complete untrusted report remains available in the artifact. */ }
|
|
128
|
+
return unknown("Python did not return a valid isolated capability report");
|
|
129
|
+
}
|
|
130
|
+
export function isEnvironmentReceipt(value: unknown): value is EnvironmentReceipt {
|
|
131
|
+
if (!object(value) || value["kind"] !== "environment" || value["version"] !== 1 || !object(value["query"]) ||
|
|
132
|
+
!strings(value["query"], ["program"]) || !["version", "python"].includes(String(value["query"]["probe"]))) return false;
|
|
133
|
+
const source = value["source"], evidence = value["evidence"], shell = value["shell"], resolution = value["resolution"];
|
|
134
|
+
if (!object(source) || !strings(source, ["host", "workspace", "cwd", "runtime"]) || !digest(source["inherited_sha256"]) ||
|
|
135
|
+
!object(evidence) || !isObservationArtifact(evidence["output"]) || !Number.isInteger(evidence["exit"]) || typeof evidence["complete"] !== "boolean") return false;
|
|
136
|
+
const observed = value["observed_at"], expires = value["expires_at"];
|
|
137
|
+
if (typeof observed !== "number" || !Number.isSafeInteger(observed) || observed < 0 || expires !== observed + ENVIRONMENT_TTL_MS ||
|
|
138
|
+
!["observed", "missing", "opaque", "failed", "unknown"].includes(String(value["state"])) ||
|
|
139
|
+
!(value["reported_version"] === null || typeof value["reported_version"] === "string") ||
|
|
140
|
+
!(value["reason"] === undefined || typeof value["reason"] === "string")) return false;
|
|
141
|
+
if (shell !== null && (!object(shell) || !strings(shell, ["executable", "version", "cwd"]) || !digest(shell["path_sha256"]))) return false;
|
|
142
|
+
if (resolution !== null && (!object(resolution) || typeof resolution["kind"] !== "string" ||
|
|
143
|
+
!(resolution["executable"] === null || typeof resolution["executable"] === "string"))) return false;
|
|
144
|
+
if (value["python"] !== undefined && !isPythonCapability(value["python"])) return false;
|
|
145
|
+
if (value["state"] === "observed" && (evidence["exit"] !== 0 || !evidence["complete"] || !object(shell) || !object(resolution) ||
|
|
146
|
+
resolution["kind"] !== "file" || !resolution["executable"] || !value["reported_version"] ||
|
|
147
|
+
value["query"]["probe"] === "python" && value["python"] === undefined)) return false;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type EnvironmentValidity = { state: "recorded" | "expired" | "scope_changed" | "invalidated" | "unconfirmed";
|
|
152
|
+
reason: string; event?: string };
|
|
153
|
+
/** Recomputed on consumption: recent observations are never guarantees about a future invocation. */
|
|
154
|
+
export function environmentValidity(receipt: EnvironmentReceipt, source: EnvironmentSource, now: number, after: readonly LogEvent[]): EnvironmentValidity {
|
|
155
|
+
for (const event of after) {
|
|
156
|
+
if (event.t === "user_bash" || event.t === "snapshot" &&
|
|
157
|
+
(Object.keys(event.added).length > 0 || Object.keys(event.changed).length > 0 || event.removed.length > 0)) {
|
|
158
|
+
return { state: "invalidated", reason: "later command or workspace change requires environment revalidation", event: event.id };
|
|
159
|
+
}
|
|
160
|
+
if (event.t !== "tool_result") continue;
|
|
161
|
+
if (isEnvironmentReceipt(event.data) && event.data.query.program === receipt.query.program && event.data.query.probe === receipt.query.probe) {
|
|
162
|
+
return { state: "invalidated", reason: "a later probe supersedes this observation, including a failed probe", event: event.id };
|
|
163
|
+
}
|
|
164
|
+
if (event.effect?.kind === "edit" || event.effect?.kind === "command" || event.diagnostic?.code === "environment_missing" ||
|
|
165
|
+
event.diagnostic?.effects === "possible" && event.diagnostic.execution !== "not_started") {
|
|
166
|
+
return { state: "invalidated", reason: "later execution or mutation requires environment revalidation", event: event.id };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const keys: readonly (keyof EnvironmentSource)[] = ["host", "workspace", "cwd", "runtime", "inherited_sha256"];
|
|
170
|
+
if (keys.some(key => source[key] !== receipt.source[key])) return { state: "scope_changed", reason: "host, workspace, directory, runtime, or inherited environment changed" };
|
|
171
|
+
if (!Number.isFinite(now) || now < receipt.observed_at || now >= receipt.expires_at) return { state: "expired", reason: "observation time is outside its validity interval" };
|
|
172
|
+
if (receipt.state !== "observed") return { state: "unconfirmed", reason: "the probe did not establish this capability" };
|
|
173
|
+
return { state: "recorded", reason: "recent observation only; login startup files or external changes may require another probe" };
|
|
174
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** R12c: missing final rows cannot erase attempted work or measured charges. */
|
|
2
|
+
import type { EvalJob, EvalResult } from "./experiment.ts";
|
|
3
|
+
import { recoveryCase } from "./recovery-corpus.ts";
|
|
4
|
+
import { TOOL_MODE_FACTORS, isToolModeTask, isToolModeMetrics } from "./tool-mode-protocol.ts";
|
|
5
|
+
|
|
6
|
+
export interface BillingRecord {
|
|
7
|
+
kind: "reserved" | "settled"; request: number; inputSha256: string;
|
|
8
|
+
reservation: number; charge: number; measured: number | null; ledgerCharged: number;
|
|
9
|
+
}
|
|
10
|
+
export function requestAccounting(records: readonly BillingRecord[], before: number) {
|
|
11
|
+
const nonnegative = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
12
|
+
if (!nonnegative(before)) throw Error("invalid trial ledger start");
|
|
13
|
+
let charged = before, pending: BillingRecord | undefined, requests = 0, known = 0, unknown = 0;
|
|
14
|
+
for (const row of records) {
|
|
15
|
+
if (![row.reservation, row.charge, row.ledgerCharged].every(nonnegative) || row.measured !== null && !nonnegative(row.measured) ||
|
|
16
|
+
!Number.isSafeInteger(row.request) || row.request < 1 || !/^[a-f0-9]{64}$/.test(row.inputSha256)) throw Error("invalid billing record");
|
|
17
|
+
if (row.kind === "reserved") {
|
|
18
|
+
if (pending || row.request !== ++requests || row.measured !== null || row.charge !== row.reservation) throw Error("invalid reservation order");
|
|
19
|
+
pending = row; charged += row.reservation;
|
|
20
|
+
} else if (row.kind === "settled") {
|
|
21
|
+
if (!pending || row.request !== pending.request || row.inputSha256 !== pending.inputSha256 || row.reservation !== pending.reservation ||
|
|
22
|
+
row.charge !== (row.measured ?? row.reservation)) throw Error("invalid settlement identity");
|
|
23
|
+
charged += row.charge - pending.reservation;
|
|
24
|
+
if (row.measured === null) unknown += row.reservation; else known += row.measured;
|
|
25
|
+
pending = undefined;
|
|
26
|
+
} else throw Error("invalid billing record kind");
|
|
27
|
+
if (row.ledgerCharged !== charged) throw Error("billing ledger sequence changed");
|
|
28
|
+
}
|
|
29
|
+
return { requests, charged, knownCostSubtotal: known, unsettledRequests: pending ? 1 : 0,
|
|
30
|
+
unsettledReservation: pending?.reservation ?? 0, unknownSettlementReserve: unknown,
|
|
31
|
+
fullyMeasured: !pending && records.filter(row => row.kind === "settled").every(row => row.measured !== null) };
|
|
32
|
+
}
|
|
33
|
+
export type RequestAccounting = ReturnType<typeof requestAccounting>;
|
|
34
|
+
export type TrialCoverage = { id: string; accounting: RequestAccounting; tail: "complete" | "truncated";
|
|
35
|
+
result?: EvalResult; interruption: "deadline" | "caller" | null; partialMetrics?: EvalResult["recovery"] };
|
|
36
|
+
|
|
37
|
+
export function summarizeRecoveryCoverage(jobs: readonly EvalJob[], trials: readonly TrialCoverage[], mechanical: boolean) {
|
|
38
|
+
const ids = new Set(jobs.map(job => job.id)), byId = new Map(trials.map(trial => [trial.id, trial]));
|
|
39
|
+
if (ids.size !== jobs.length || byId.size !== trials.length || trials.some(trial => !ids.has(trial.id) || trial.result && trial.result.id !== trial.id)) throw Error("invalid recovery coverage identities");
|
|
40
|
+
const group = (selected: readonly EvalJob[]) => {
|
|
41
|
+
const rows = selected.flatMap(job => byId.has(job.id) ? [byId.get(job.id)!] : []), finalized = rows.filter(row => row.result);
|
|
42
|
+
const passed = finalized.filter(row => row.result!.scenarioPassed).length;
|
|
43
|
+
const unknown = rows.filter(row => !row.result || row.tail !== "complete" || !row.accounting.fullyMeasured).length;
|
|
44
|
+
const known = rows.reduce((n, row) => n + row.accounting.knownCostSubtotal, 0);
|
|
45
|
+
const cost = !mechanical && rows.length === selected.length && unknown === 0 ? known : null;
|
|
46
|
+
const metrics = rows.flatMap(row => row.result?.recovery ?? row.partialMetrics ?? []);
|
|
47
|
+
const modes = finalized.flatMap(row => row.result?.toolMode ?? []);
|
|
48
|
+
if (modes.some(mode => !isToolModeMetrics(mode))) throw Error("invalid tool mode measurements");
|
|
49
|
+
const toolMode = selected.some(job => isToolModeTask(job.task)) ? { observed: modes.length, missing: selected.length - modes.length,
|
|
50
|
+
queryCalls: modes.reduce((n, mode) => n + mode.queryCalls, 0),
|
|
51
|
+
primitiveCalls: modes.reduce((n, mode) => n + mode.primitiveCalls, 0), sourcesComplete: modes.filter(mode => mode.sourceComplete).length,
|
|
52
|
+
meanElapsedMs: modes.length === selected.length && modes.length > 0 ? modes.reduce((n, mode) => n + mode.elapsedMs, 0) / modes.length : null } : undefined;
|
|
53
|
+
return { ...(toolMode ? { toolMode } : {}), planned: selected.length, attempted: rows.length, finalized: finalized.length, incomplete: rows.length - finalized.length,
|
|
54
|
+
unattempted: selected.length - rows.length, failed: finalized.length - passed, passed, plannedPassFraction: selected.length ? passed / selected.length : null,
|
|
55
|
+
cancelled: rows.filter(row => row.interruption === "caller").length, timedOut: rows.filter(row => row.interruption === "deadline").length,
|
|
56
|
+
requests: rows.reduce((n, row) => n + row.accounting.requests, 0), truncatedRequestLogs: rows.filter(row => row.tail === "truncated").length,
|
|
57
|
+
knownCostSubtotal: mechanical ? null : known, costUsd: cost,
|
|
58
|
+
unsettledRequests: rows.reduce((n, row) => n + row.accounting.unsettledRequests, 0),
|
|
59
|
+
unsettledReservationUsd: mechanical ? null : rows.reduce((n, row) => n + row.accounting.unsettledReservation, 0),
|
|
60
|
+
unknownSettlementReserveUsd: mechanical ? null : rows.reduce((n, row) => n + row.accounting.unknownSettlementReserve, 0),
|
|
61
|
+
unknownCostTrials: mechanical ? rows.length : unknown, costPerScenarioSuccess: cost !== null && passed > 0 ? cost / passed : null,
|
|
62
|
+
unverifiedEndings: finalized.filter(row => row.result!.unverifiedEnding).length,
|
|
63
|
+
recovery: { observed: metrics.length, missing: selected.length - metrics.length,
|
|
64
|
+
firstToolSuccess: metrics.filter(row => row.firstToolSuccess === true).length,
|
|
65
|
+
firstToolUnknown: metrics.filter(row => row.firstToolSuccess === null).length,
|
|
66
|
+
firstPassTaskSuccess: metrics.filter(row => row.firstPassTaskSuccess === true).length,
|
|
67
|
+
...Object.fromEntries((["toolCalls", "outerCalls", "nestedCalls", "unresolvedCalls", "toolFailures", "classifiedFailures", "unclassifiedFailures", "recoveryCalls", "unchangedFailureRetries", "originalEvidenceRetrievals", "unsupportedClaims"] as const)
|
|
68
|
+
.map(key => [key, metrics.reduce((n, row) => n + row[key], 0)])) } };
|
|
69
|
+
};
|
|
70
|
+
const toolModes = jobs.some(job => isToolModeTask(job.task));
|
|
71
|
+
return { ...group(jobs), arms: (toolModes ? TOOL_MODE_FACTORS : ["profile", "workingSet", "strictVerification"]).flatMap(factor => ["control", "treatment"].map(arm =>
|
|
72
|
+
({ factor, arm, ...group(jobs.filter(job => job.factor === factor && job.arm === arm)) }))),
|
|
73
|
+
partitions: toolModes ? [] : ["development", "holdout"].map(partition => ({ partition, ...group(jobs.filter(job => recoveryCase(job.task)?.partition === partition)) })) };
|
|
74
|
+
}
|
|
@@ -3,6 +3,14 @@ import type { LogEvent, NodeRef } from "./events.ts";
|
|
|
3
3
|
import { isExecutionOutcome } from "./outcome.ts";
|
|
4
4
|
import { isJsonValue, isToolDiagnostic } from "./tool-contract.ts";
|
|
5
5
|
import { validImageRef } from "./images.ts";
|
|
6
|
+
import { parseAcceptanceContract } from "./acceptance.ts";
|
|
7
|
+
import { validFileChanges } from "./checkpoints.ts";
|
|
8
|
+
import { isObservationReceipt } from "./read-observation.ts";
|
|
9
|
+
import { isSearchReceipt } from "./search-observation.ts";
|
|
10
|
+
import { isProcessReceipt } from "./process-receipt.ts";
|
|
11
|
+
import { isObservationArtifact } from "./observation-view.ts";
|
|
12
|
+
import { isCommandArtifactReceipt } from "./command-artifacts.ts";
|
|
13
|
+
import { isRuntimeTargetReceipt } from "./runtime-target.ts";
|
|
6
14
|
|
|
7
15
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
16
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -11,6 +19,7 @@ const strings = (value: unknown): boolean => Array.isArray(value) && value.every
|
|
|
11
19
|
const stringMap = (value: unknown): boolean => isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
12
20
|
const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
|
|
13
21
|
const natural = (value: unknown): boolean => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
22
|
+
const eventRef = (value: unknown): boolean => isRecord(value) && natural(value["node"]) && typeof value["event"] === "string" && value["event"] !== "";
|
|
14
23
|
export function validNodeRef(value: unknown): value is NodeRef {
|
|
15
24
|
return isRecord(value) && Number.isSafeInteger(value["node"]) && Number(value["node"]) >= 0 &&
|
|
16
25
|
typeof value["log_ref"] === "string" && typeof value["sha256"] === "string" &&
|
|
@@ -27,8 +36,39 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
27
36
|
const images = value["images"];
|
|
28
37
|
if (images !== undefined && (!Array.isArray(images) || !images.every(validImageRef))) return false;
|
|
29
38
|
switch (value["t"]) {
|
|
39
|
+
case "tool_invocation": return has("call_id", "name") && value["call_id"] !== "" && value["name"] !== "" &&
|
|
40
|
+
(value["origin"] === undefined || value["origin"] === "extension") &&
|
|
41
|
+
isObservationArtifact(value["input_ref"]) && (value["schema_ref"] === null || isObservationArtifact(value["schema_ref"])) &&
|
|
42
|
+
(value["input"] === undefined || isJsonValue(value["input"])) && optionalString(value["parent_call_id"]) &&
|
|
43
|
+
(value["parent_ref"] === undefined ? value["parent_call_id"] === undefined : eventRef(value["parent_ref"]) && value["parent_call_id"] !== undefined) &&
|
|
44
|
+
value["parent_call_id"] !== "" && value["parent_call_id"] !== value["call_id"];
|
|
45
|
+
case "checkpoint": return has("operation") && ["edit", "git", "restore"].includes(String(value["source"])) &&
|
|
46
|
+
["prepared", "applied", "unavailable"].includes(String(value["phase"])) && validFileChanges(value["changes"]) &&
|
|
47
|
+
optionalString(value["tree"]) && optionalString(value["reason"]);
|
|
48
|
+
case "workspace_observation": return value["scope"] === "source-v1" && typeof value["changed"] === "boolean" && optionalString(value["reason"]) &&
|
|
49
|
+
(value["fingerprint"] === undefined || typeof value["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(value["fingerprint"]));
|
|
50
|
+
case "process_exit": {
|
|
51
|
+
const output = value["output"];
|
|
52
|
+
const receipt = value["receipt"];
|
|
53
|
+
if (receipt !== undefined && (!isProcessReceipt(receipt) || receipt.replayed || receipt.running !== false ||
|
|
54
|
+
receipt.session_id !== value["session"] || receipt.exit !== value["exit"] || !isRecord(output) ||
|
|
55
|
+
receipt.artifacts !== undefined && receipt.artifacts.command !== value["command"] ||
|
|
56
|
+
receipt.output === undefined || receipt.output.sha256 !== output["sha256"] || receipt.output.bytes !== output["bytes"])) return false;
|
|
57
|
+
return has("command") && natural(value["session"]) && Number(value["session"]) > 0 && Number.isInteger(value["exit"]) &&
|
|
58
|
+
value["reason"] === "disposed" && isRecord(output) && typeof output["path"] === "string" &&
|
|
59
|
+
typeof output["sha256"] === "string" && /^[a-f0-9]{64}$/.test(output["sha256"]) && natural(output["bytes"]);
|
|
60
|
+
}
|
|
61
|
+
case "skill_loaded": return has("name", "path", "sha256", "artifact") &&
|
|
62
|
+
["sha256", "artifact"].every(key => /^[a-f0-9]{64}$/.test(String(value[key]))) &&
|
|
63
|
+
["inline", "fork", "read"].includes(String(value["mode"])) && ["user", "model"].includes(String(value["origin"]));
|
|
64
|
+
case "acceptance_contract": {
|
|
65
|
+
try { parseAcceptanceContract(value["contract"]); return has("request"); } catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
case "acceptance_decision": return has("request", "contract_id", "criterion") && value["actor"] === "user" &&
|
|
68
|
+
(value["target_binding"] === undefined ? value["target_observation"] === undefined : has("target_binding", "target_observation"));
|
|
69
|
+
case "target_observation": return isRuntimeTargetReceipt(value["observation"]);
|
|
30
70
|
case "extension": return has("customType", "content") && typeof value["display"] === "boolean" && typeof value["inContext"] === "boolean" && (value["data"] === undefined || isJsonValue(value["data"]));
|
|
31
|
-
case "user": return has("text");
|
|
71
|
+
case "user": return has("text") && (value["request"] === undefined || typeof value["request"] === "string" && value["request"] !== "");
|
|
32
72
|
case "outcome": return has("reason") && isExecutionOutcome(value["outcome"]);
|
|
33
73
|
case "user_bash": {
|
|
34
74
|
const ref = value["content_ref"];
|
|
@@ -40,7 +80,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
40
80
|
isRecord(ref) && typeof ref["sha256"] === "string" &&
|
|
41
81
|
typeof ref["path"] === "string" && natural(ref["bytes"]);
|
|
42
82
|
}
|
|
43
|
-
case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline"].includes(String(value["reason"])) && strings(value["sources"]);
|
|
83
|
+
case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline", "runtime_acceptance"].includes(String(value["reason"])) && strings(value["sources"]);
|
|
44
84
|
case "interrupt": return has("user_id");
|
|
45
85
|
case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) &&
|
|
46
86
|
(value["replay_key"] === undefined || typeof value["replay_key"] === "string" && /^[a-f0-9]{64}$/.test(value["replay_key"])) &&
|
|
@@ -51,8 +91,34 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
51
91
|
return b["type"] === "tool_use" && typeof b["call_id"] === "string" && typeof b["name"] === "string" && "input" in b;
|
|
52
92
|
});
|
|
53
93
|
case "tool_result": {
|
|
94
|
+
if (!optionalString(value["parent_call_id"]) || value["parent_call_id"] === "" || value["parent_call_id"] === value["call_id"]) return false;
|
|
95
|
+
if (value["origin"] !== undefined && value["origin"] !== "extension") return false;
|
|
96
|
+
if (value["invocation_ref"] !== undefined && !eventRef(value["invocation_ref"])) return false;
|
|
97
|
+
const observation = value["observation"];
|
|
98
|
+
if (observation !== undefined && (!isObservationReceipt(observation) ||
|
|
99
|
+
!isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== observation.artifact.sha256 ||
|
|
100
|
+
value["content_ref"]["bytes"] !== observation.artifact.bytes || value["content_sha256"] !== observation.artifact.sha256)) return false;
|
|
54
101
|
if (value["data"] !== undefined && !isJsonValue(value["data"])) return false;
|
|
102
|
+
if (value["data_ref"] !== undefined && (!isObservationArtifact(value["data_ref"]) || value["data"] !== undefined)) return false;
|
|
103
|
+
if (isRecord(value["data"]) && value["data"]["kind"] === "process") {
|
|
104
|
+
const process = value["data"];
|
|
105
|
+
if (!isProcessReceipt(process)) return false;
|
|
106
|
+
if (process.output && (!isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== process.output.sha256 ||
|
|
107
|
+
value["content_ref"]["bytes"] !== process.output.bytes || value["content_sha256"] !== process.output.sha256)) return false;
|
|
108
|
+
if (process.replayed && value["effect"] !== undefined) return false;
|
|
109
|
+
if (value["effect"] !== undefined && (!isRecord(value["effect"]) || value["effect"]["kind"] !== "command" ||
|
|
110
|
+
process.running !== false || value["effect"]["exit"] !== process.exit ||
|
|
111
|
+
JSON.stringify(value["effect"]["artifacts"]) !== JSON.stringify(process.artifacts))) return false;
|
|
112
|
+
}
|
|
113
|
+
if (isObservationReceipt(observation) && observation.descriptor !== undefined &&
|
|
114
|
+
(!isRecord(value["data"]) || value["data"]["read_handle"] !== observation.descriptor.sha256)) return false;
|
|
115
|
+
if (isRecord(value["data"]) && value["data"]["kind"] === "search") {
|
|
116
|
+
const search = value["data"];
|
|
117
|
+
if (!isSearchReceipt(search) || !isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== search.artifact.sha256 ||
|
|
118
|
+
value["content_ref"]["bytes"] !== search.artifact.bytes || value["content_sha256"] !== search.artifact.sha256) return false;
|
|
119
|
+
}
|
|
55
120
|
if (value["diagnostic"] !== undefined && !isToolDiagnostic(value["diagnostic"])) return false;
|
|
121
|
+
if (value["warnings"] !== undefined && (!Array.isArray(value["warnings"]) || !value["warnings"].every(isToolDiagnostic))) return false;
|
|
56
122
|
if (!has("call_id", "content") || typeof value["ok"] !== "boolean") return false;
|
|
57
123
|
if (!optionalString(value["content_sha256"])) return false;
|
|
58
124
|
if (!optionalString(value["observation_sha256"])) return false;
|
|
@@ -63,14 +129,19 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
63
129
|
if (effect === undefined) return true;
|
|
64
130
|
if (!isRecord(effect)) return false;
|
|
65
131
|
if (effect["kind"] === "command") return typeof effect["cmd"] === "string" && Number.isInteger(effect["exit"]) &&
|
|
66
|
-
optionalString(effect["gate"]) && optionalString(effect["workspace_sha256"])
|
|
132
|
+
optionalString(effect["gate"]) && optionalString(effect["workspace_sha256"]) &&
|
|
133
|
+
(effect["artifacts"] === undefined || isCommandArtifactReceipt(effect["artifacts"]) &&
|
|
134
|
+
effect["artifacts"].exit === effect["exit"] && effect["artifacts"].command === effect["cmd"]) &&
|
|
135
|
+
(effect["workspace_scope"] === undefined || effect["workspace_scope"] === "source-v1") &&
|
|
136
|
+
(effect["workspace_observed"] === undefined || typeof effect["workspace_observed"] === "boolean");
|
|
67
137
|
if (effect["kind"] === "read") return typeof effect["path"] === "string" && typeof effect["sha256"] === "string" &&
|
|
68
138
|
(effect["ranges"] === undefined || (Array.isArray(effect["ranges"]) && effect["ranges"].every((r: unknown) =>
|
|
69
139
|
Array.isArray(r) && r.length === 2 && r.every((n) => Number.isSafeInteger(n)) && r[0] >= 1 && r[1] >= r[0])));
|
|
70
140
|
return effect["kind"] === "edit" && typeof effect["path"] === "string" && typeof effect["sha256_after"] === "string" &&
|
|
71
141
|
(effect["sha256_before"] === null || typeof effect["sha256_before"] === "string");
|
|
72
142
|
}
|
|
73
|
-
case "cut": return has("handoff_ref") && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
|
|
143
|
+
case "cut": return has("handoff_ref") && (value["working_set_version"] === undefined || value["working_set_version"] === 1 || value["working_set_version"] === 2) && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
|
|
144
|
+
(value["guidance_tools"] === undefined || Array.isArray(value["guidance_tools"]) && value["guidance_tools"].every(item => typeof item === "string")) &&
|
|
74
145
|
optionalString(value["retained_from"]) && optionalString(value["next_context_ref"]) &&
|
|
75
146
|
(value["next_node"] === undefined || natural(value["next_node"])) && optionalString(value["next_model"]) &&
|
|
76
147
|
(value["handoff_tokens"] === undefined || (typeof value["handoff_tokens"] === "number" && Number.isFinite(value["handoff_tokens"]) && value["handoff_tokens"] > 0)) &&
|
|
@@ -83,7 +154,9 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
83
154
|
case "pitfall": return has("tried", "failed_because");
|
|
84
155
|
case "blocked": return has("what", "reason");
|
|
85
156
|
case "question": return has("text");
|
|
86
|
-
case "next_action": return has("what", "next_action");
|
|
157
|
+
case "next_action": return has("what", "next_action") && (value["depends_on"] === undefined || strings(value["depends_on"]));
|
|
158
|
+
case "work_dependencies": return has("work_id", "what") && strings(value["depends_on"]);
|
|
159
|
+
case "work_owner": return has("work_id", "what", "owner") && ["claim", "release"].includes(String(value["operation"]));
|
|
87
160
|
case "evidence_link": return has("work", "what", "source") && optionalString(value["resource"]) && optionalString(value["version"]) &&
|
|
88
161
|
optionalString(value["supersedes"]) && (value["observed_version"] === null || optionalString(value["observed_version"]));
|
|
89
162
|
case "work_status": return has("work_id", "what") && ["unblock", "reopen", "cancel"].includes(String(value["operation"])) &&
|
|
@@ -93,6 +166,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
93
166
|
strings(value["undeclared"]) && strings(value["declared_but_unchanged"]) && optionalString(value["workspace_sha256"]) &&
|
|
94
167
|
(value["phase"] === undefined || value["phase"] === "turn" || value["phase"] === "idle");
|
|
95
168
|
case "rewind": return ["both", "files", "context"].includes(String(value["mode"])) && validNodeRef(value["from"]) &&
|
|
169
|
+
(value["files_from"] === undefined || validNodeRef(value["files_from"])) &&
|
|
96
170
|
has("prompt") && strings(value["restored"]) && strings(value["removed"]) && strings(value["unrecoverable"]) && strings(value["divergence"]);
|
|
97
171
|
case "spawn": {
|
|
98
172
|
const input = value["input"];
|
|
@@ -103,10 +177,11 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
103
177
|
if (!(workspace["kind"] === "shared_readonly" || (workspace["kind"] === "git_tree" &&
|
|
104
178
|
typeof workspace["head"] === "string" && typeof workspace["tree"] === "string"))) return false;
|
|
105
179
|
}
|
|
106
|
-
return validNodeRef(value["branch"]) && has("purpose") && (value["workspace"] === undefined ||
|
|
180
|
+
return (value["background"] === undefined || value["background"] === true) && validNodeRef(value["branch"]) && has("purpose") && (value["workspace"] === undefined ||
|
|
107
181
|
(isRecord(value["workspace"]) && typeof value["workspace"]["path"] === "string" && typeof value["workspace"]["branch"] === "string"));
|
|
108
182
|
}
|
|
109
183
|
case "joined": return validNodeRef(value["branch"]) && has("handoff_ref", "summary") && optionalString(value["model"]) &&
|
|
184
|
+
(value["receipt_ref"] === undefined || typeof value["receipt_ref"] === "string" && /^[a-f0-9]{64}$/.test(value["receipt_ref"])) &&
|
|
110
185
|
(value["tip"] === undefined || validNodeRef(value["tip"]));
|
|
111
186
|
case "adopt": return validNodeRef(value["branch"]) && (value["next_node"] === undefined || natural(value["next_node"])) &&
|
|
112
187
|
(value["applied"] === undefined || value["applied"] === true);
|
package/src/core/events.ts
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
import type { ExecutionOutcome } from "./outcome.ts";
|
|
22
22
|
import type { ImageRef } from "./images.ts";
|
|
23
23
|
import type { JsonValue, ToolDiagnostic } from "./tool-contract.ts";
|
|
24
|
+
import type { AcceptanceContract } from "./acceptance.ts";
|
|
25
|
+
import type { CheckpointEvent, WorkspaceObservationEvent } from "./checkpoints.ts";
|
|
26
|
+
import type { ObservationReceipt } from "./read-observation.ts";
|
|
27
|
+
import type { CommandArtifactReceipt } from "./command-artifacts.ts";
|
|
28
|
+
import type { RuntimeTargetReceipt } from "./runtime-target.ts";
|
|
24
29
|
|
|
25
30
|
export type EventId = string;
|
|
26
31
|
export type Sha256 = string;
|
|
@@ -68,7 +73,7 @@ export type NodeRef = {
|
|
|
68
73
|
export type Effect =
|
|
69
74
|
| { kind: "read"; path: string; sha256: Sha256; ranges?: readonly LineRange[] }
|
|
70
75
|
| { kind: "edit"; path: string; sha256_before: Sha256 | null; sha256_after: Sha256 }
|
|
71
|
-
| { kind: "command"; cmd: string; exit: number; gate?: string; workspace_sha256?: Sha256 };
|
|
76
|
+
| { kind: "command"; cmd: string; exit: number; gate?: string; workspace_sha256?: Sha256; workspace_observed?: boolean; workspace_scope?: "source-v1"; artifacts?: CommandArtifactReceipt };
|
|
72
77
|
|
|
73
78
|
// ---------------------------------------------------------------------------
|
|
74
79
|
// Harness-derived events
|
|
@@ -79,6 +84,8 @@ export interface UserEvent {
|
|
|
79
84
|
id: EventId;
|
|
80
85
|
at: IsoTime;
|
|
81
86
|
text: string;
|
|
87
|
+
/** Q23: in-flight steering belongs to this original user event; legacy events open a request. */
|
|
88
|
+
request?: EventId;
|
|
82
89
|
images?: readonly ImageRef[];
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -109,7 +116,7 @@ export interface GuidanceEvent {
|
|
|
109
116
|
t: "guidance";
|
|
110
117
|
id: EventId;
|
|
111
118
|
at: IsoTime;
|
|
112
|
-
reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline";
|
|
119
|
+
reason: "continuation" | "repetition" | "budget" | "boundary" | "stall" | "output_recovery" | "completion_review" | "churn" | "deadline" | "runtime_acceptance";
|
|
113
120
|
text: string;
|
|
114
121
|
sources: readonly EventId[];
|
|
115
122
|
}
|
|
@@ -167,6 +174,11 @@ export interface ToolResultEvent {
|
|
|
167
174
|
id: EventId;
|
|
168
175
|
at: IsoTime;
|
|
169
176
|
call_id: string;
|
|
177
|
+
/** Trusted host execution has no provider tool_use and is excluded from provider replay. */
|
|
178
|
+
origin?: "extension";
|
|
179
|
+
/** Pi A: nested observations remain evidence without creating provider-visible tool pairs. */
|
|
180
|
+
parent_call_id?: string;
|
|
181
|
+
invocation_ref?: { node: number; event: EventId };
|
|
170
182
|
ok: boolean;
|
|
171
183
|
/** What the model sees. Large payloads belong on disk, referenced by effect. */
|
|
172
184
|
content: string;
|
|
@@ -175,17 +187,36 @@ export interface ToolResultEvent {
|
|
|
175
187
|
content_sha256?: Sha256;
|
|
176
188
|
/** O.3: semantic observation digest excludes presentation metadata. */
|
|
177
189
|
observation_sha256?: Sha256;
|
|
190
|
+
/** R2: exact preview coverage and immutable source identity; old logs omit this receipt. */
|
|
191
|
+
observation?: ObservationReceipt;
|
|
178
192
|
content_ref?: ArtifactRef;
|
|
179
193
|
effect?: Effect;
|
|
180
194
|
/** O.1: actual termination, independent of effect/verification; legacy logs omit it. */
|
|
181
195
|
outcome?: ExecutionOutcome;
|
|
182
196
|
/** O.3: machine-readable observations and recovery hints, not additional proof. */
|
|
183
197
|
data?: JsonValue;
|
|
198
|
+
/** Pi A: large structured results stay recoverable without bloating each JSONL event. */
|
|
199
|
+
data_ref?: ArtifactRef;
|
|
184
200
|
diagnostic?: ToolDiagnostic;
|
|
201
|
+
/** Post-operation observer failures cannot erase the operation's factual result. */
|
|
202
|
+
warnings?: readonly ToolDiagnostic[];
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
export type ArtifactRef = { sha256: Sha256; path: string; bytes: number };
|
|
188
206
|
|
|
207
|
+
/** Pi A: an invocation is durable before its admitted action may start. */
|
|
208
|
+
export interface ToolInvocationEvent {
|
|
209
|
+
t: "tool_invocation"; id: EventId; at: IsoTime;
|
|
210
|
+
call_id: string; name: string; parent_call_id?: string;
|
|
211
|
+
origin?: "extension";
|
|
212
|
+
parent_ref?: { node: number; event: EventId };
|
|
213
|
+
/** Original request and the exact declarative contract are independently recoverable. */
|
|
214
|
+
input_ref: ArtifactRef;
|
|
215
|
+
schema_ref: ArtifactRef | null;
|
|
216
|
+
/** Bounded actual arguments; absent for a direct unchanged request or an oversized refusal. */
|
|
217
|
+
input?: JsonValue;
|
|
218
|
+
}
|
|
219
|
+
|
|
189
220
|
/** Terminates the segment it appears in. Carries refs only -- see rule 1 above. */
|
|
190
221
|
export interface CutEvent {
|
|
191
222
|
t: "cut";
|
|
@@ -205,6 +236,10 @@ export interface CutEvent {
|
|
|
205
236
|
next_context_ref?: Sha256;
|
|
206
237
|
/** V2 carried-state budget, fixed in the log for identical replay. */
|
|
207
238
|
handoff_tokens?: number;
|
|
239
|
+
/** O.10: absent preserves the original D20 carried projection. */
|
|
240
|
+
working_set_version?: 1 | 2;
|
|
241
|
+
/** R10: declared builtin advice capabilities at this cut, not an authorization grant. Absent replays old text. */
|
|
242
|
+
guidance_tools?: readonly string[];
|
|
208
243
|
/** N.8: first retained event on the effective ancestry; no copied transcript. */
|
|
209
244
|
retained_from?: EventId;
|
|
210
245
|
}
|
|
@@ -242,6 +277,7 @@ export interface DecisionEvent {
|
|
|
242
277
|
chose: string;
|
|
243
278
|
because: string;
|
|
244
279
|
rejected?: readonly string[];
|
|
280
|
+
work_id?: string;
|
|
245
281
|
/** Set when an adopt moved this event from another node. */
|
|
246
282
|
carried_from?: CarriedFrom;
|
|
247
283
|
}
|
|
@@ -252,6 +288,7 @@ export interface PitfallEvent {
|
|
|
252
288
|
at: IsoTime;
|
|
253
289
|
tried: string;
|
|
254
290
|
failed_because: string;
|
|
291
|
+
work_id?: string;
|
|
255
292
|
/** Set when an adopt moved this event from another node. */
|
|
256
293
|
carried_from?: CarriedFrom;
|
|
257
294
|
}
|
|
@@ -340,6 +377,8 @@ export interface RewindEvent {
|
|
|
340
377
|
id: EventId;
|
|
341
378
|
at: IsoTime;
|
|
342
379
|
mode: RewindMode;
|
|
380
|
+
/** P.3: context navigation keeps the file history at this separate cursor. */
|
|
381
|
+
files_from?: NodeRef;
|
|
343
382
|
/** Where things were moved back to. */
|
|
344
383
|
from: NodeRef;
|
|
345
384
|
/** The prompt that opened the exchange moved back to, for a human reader. */
|
|
@@ -368,6 +407,8 @@ export interface SpawnEvent {
|
|
|
368
407
|
branch: NodeRef;
|
|
369
408
|
/** What the branch is for, in one line. */
|
|
370
409
|
purpose: string;
|
|
410
|
+
/** O.14: execution mode is durable; it never proves a live handle exists. */
|
|
411
|
+
background?: true;
|
|
371
412
|
/** The isolated checkout belongs to this branch, including after restart. */
|
|
372
413
|
workspace?: { path: string; branch: string };
|
|
373
414
|
/** O.4: a task input envelope references the original request, never copies history. */
|
|
@@ -393,6 +434,8 @@ export interface JoinedEvent {
|
|
|
393
434
|
branch: NodeRef;
|
|
394
435
|
handoff_ref: string;
|
|
395
436
|
summary: string;
|
|
437
|
+
/** O.9: artifact contains a deterministic evidence index, never raw history. */
|
|
438
|
+
receipt_ref?: Sha256;
|
|
396
439
|
/** A branch can cut before joining; its last node need not be its root. */
|
|
397
440
|
tip?: NodeRef;
|
|
398
441
|
model?: string;
|
|
@@ -445,6 +488,7 @@ export interface NextActionEvent {
|
|
|
445
488
|
what: string;
|
|
446
489
|
work_id?: string;
|
|
447
490
|
next_action: string;
|
|
491
|
+
depends_on?: readonly string[];
|
|
448
492
|
/** Set when an adopt moved this event from another node. */
|
|
449
493
|
carried_from?: CarriedFrom;
|
|
450
494
|
}
|
|
@@ -462,6 +506,15 @@ export interface WorkStatusEvent {
|
|
|
462
506
|
carried_from?: CarriedFrom;
|
|
463
507
|
}
|
|
464
508
|
|
|
509
|
+
export interface WorkDependenciesEvent {
|
|
510
|
+
t: "work_dependencies"; id: EventId; at: IsoTime; work_id: string; what: string;
|
|
511
|
+
depends_on: readonly string[]; carried_from?: CarriedFrom;
|
|
512
|
+
}
|
|
513
|
+
export interface WorkOwnerEvent {
|
|
514
|
+
t: "work_owner"; id: EventId; at: IsoTime; work_id: string; what: string;
|
|
515
|
+
operation: "claim" | "release"; owner: string; carried_from?: CarriedFrom;
|
|
516
|
+
}
|
|
517
|
+
|
|
465
518
|
/** O.6: a model associates an original observation with work; the runtime resolves its source. */
|
|
466
519
|
export interface EvidenceLinkEvent {
|
|
467
520
|
t: "evidence_link"; id: EventId; at: IsoTime;
|
|
@@ -479,7 +532,38 @@ export interface ExtensionEvent {
|
|
|
479
532
|
data?: import("./tool-contract.ts").JsonValue;
|
|
480
533
|
}
|
|
481
534
|
|
|
535
|
+
export interface AcceptanceContractEvent {
|
|
536
|
+
t: "acceptance_contract"; id: EventId; at: IsoTime; request: EventId;
|
|
537
|
+
contract: AcceptanceContract;
|
|
538
|
+
}
|
|
539
|
+
export interface AcceptanceDecisionEvent {
|
|
540
|
+
t: "acceptance_decision"; id: EventId; at: IsoTime; request: EventId;
|
|
541
|
+
contract_id: string; criterion: string; actor: "user";
|
|
542
|
+
target_binding?: string; target_observation?: EventId;
|
|
543
|
+
}
|
|
544
|
+
export interface TargetObservationEvent {
|
|
545
|
+
t: "target_observation"; id: EventId; at: IsoTime; observation: RuntimeTargetReceipt;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export interface SkillLoadedEvent {
|
|
549
|
+
t: "skill_loaded"; id: EventId; at: IsoTime; name: string; path: string;
|
|
550
|
+
sha256: Sha256; artifact: Sha256; mode: "inline" | "fork" | "read"; origin: "user" | "model";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** O.14: owner cleanup is an actual process observation, not a model claim. */
|
|
554
|
+
export interface ProcessExitEvent {
|
|
555
|
+
t: "process_exit"; id: EventId; at: IsoTime; session: number;
|
|
556
|
+
command: string; exit: number; output: ArtifactRef; reason: "disposed";
|
|
557
|
+
receipt?: import("./process-receipt.ts").ProcessReceipt;
|
|
558
|
+
}
|
|
559
|
+
|
|
482
560
|
export type LogEvent =
|
|
561
|
+
| ToolInvocationEvent
|
|
562
|
+
| ProcessExitEvent
|
|
563
|
+
| SkillLoadedEvent
|
|
564
|
+
| AcceptanceContractEvent
|
|
565
|
+
| AcceptanceDecisionEvent
|
|
566
|
+
| TargetObservationEvent
|
|
483
567
|
| ExtensionEvent
|
|
484
568
|
| UserEvent
|
|
485
569
|
| UserBashEvent
|
|
@@ -491,6 +575,8 @@ export type LogEvent =
|
|
|
491
575
|
| EvidenceLinkEvent
|
|
492
576
|
| CutEvent
|
|
493
577
|
| SnapshotEvent
|
|
578
|
+
| CheckpointEvent
|
|
579
|
+
| WorkspaceObservationEvent
|
|
494
580
|
| RewindEvent
|
|
495
581
|
| SpawnEvent
|
|
496
582
|
| JoinedEvent
|
|
@@ -502,12 +588,21 @@ export type LogEvent =
|
|
|
502
588
|
| BlockedEvent
|
|
503
589
|
| QuestionEvent
|
|
504
590
|
| NextActionEvent
|
|
505
|
-
| WorkStatusEvent
|
|
591
|
+
| WorkStatusEvent
|
|
592
|
+
| WorkDependenciesEvent
|
|
593
|
+
| WorkOwnerEvent;
|
|
594
|
+
// Work coordination is intent, never a substitute for command proof.
|
|
506
595
|
|
|
507
596
|
export const HARNESS_EVENTS = [
|
|
597
|
+
"checkpoint", "workspace_observation",
|
|
598
|
+
"target_observation",
|
|
599
|
+
"process_exit",
|
|
600
|
+
"skill_loaded",
|
|
601
|
+
"acceptance_contract", "acceptance_decision",
|
|
508
602
|
"user", "user_bash", "outcome", "guidance", "interrupt", "assistant", "tool_result", "cut", "snapshot", "rewind", "extension",
|
|
509
603
|
] as const;
|
|
510
604
|
export const MODEL_EVENTS = [
|
|
605
|
+
"work_dependencies", "work_owner",
|
|
511
606
|
"claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
|
|
512
607
|
"spawn", "joined", "adopt", "discard",
|
|
513
608
|
] as const;
|
|
@@ -519,6 +614,7 @@ export const MODEL_EVENTS = [
|
|
|
519
614
|
* ancestry or are parent actions the branch should not inherit.
|
|
520
615
|
*/
|
|
521
616
|
export const CARRYABLE_EVENTS = [
|
|
617
|
+
"work_dependencies", "work_owner",
|
|
522
618
|
"claim", "decision", "pitfall", "blocked", "question", "next_action", "work_status", "evidence_link",
|
|
523
619
|
] as const;
|
|
524
620
|
|