@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/project.ts
CHANGED
|
@@ -30,6 +30,10 @@ import type { LogEvent, AssistantBlock } from "./events.ts";
|
|
|
30
30
|
import { assertNever } from "./exhaustive.ts";
|
|
31
31
|
import type { ImageContent } from "./images.ts";
|
|
32
32
|
import { type Handoff, emitHandoff } from "./handoff.ts";
|
|
33
|
+
import { diagnosticContent } from "./tool-contract.ts";
|
|
34
|
+
|
|
35
|
+
/** O.8: persist the interpretation used to replay a frozen request. */
|
|
36
|
+
export const PROJECTION_VERSION = 1;
|
|
33
37
|
|
|
34
38
|
export type ContentBlock =
|
|
35
39
|
| ImageContent
|
|
@@ -61,15 +65,18 @@ export interface FrozenPrefix {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export interface ProjectInput {
|
|
68
|
+
/** Diagnostic only; trace never enters provider messages. */
|
|
69
|
+
trace?: boolean;
|
|
64
70
|
frozen: FrozenPrefix;
|
|
65
71
|
/** Previous segment's handoff, or null for segment 0. */
|
|
66
72
|
handoff: Handoff | null;
|
|
67
73
|
events: readonly LogEvent[];
|
|
68
|
-
/**
|
|
69
|
-
logHint?: { manifestPath: string; segmentPath: string };
|
|
74
|
+
/** Source locations and versioned recovery guidance; workspace search does not index the log. */
|
|
75
|
+
logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true };
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
export interface ProjectResult {
|
|
79
|
+
provenance?: { version: number; messages: string[][]; omitted: string[] };
|
|
73
80
|
system: string;
|
|
74
81
|
tools: readonly unknown[];
|
|
75
82
|
messages: Message[];
|
|
@@ -82,10 +89,31 @@ export interface ProjectResult {
|
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
export interface ProjectViolation {
|
|
85
|
-
code: "UNPAIRED_TOOL_USE" | "ORPHAN_TOOL_RESULT" | "EMPTY_PROJECTION";
|
|
92
|
+
code: "UNPAIRED_TOOL_USE" | "ORPHAN_TOOL_RESULT" | "EMPTY_PROJECTION" | "DUPLICATE_TOOL_USE" | "EMPTY_TOOL_USE_ID";
|
|
86
93
|
detail: string;
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
/**
|
|
97
|
+
* D5: a receipt identifies exactly one call in its assistant batch. Validate
|
|
98
|
+
* before either the projection's Set or the runtime's scheduling Map can erase
|
|
99
|
+
* a collision; repairing IDs would invent provider protocol history.
|
|
100
|
+
*/
|
|
101
|
+
export function toolCallIdentityViolations(blocks: readonly AssistantBlock[]): ProjectViolation[] {
|
|
102
|
+
const violations: ProjectViolation[] = [], seen = new Map<string, number>();
|
|
103
|
+
for (const [index, block] of blocks.entries()) {
|
|
104
|
+
if (block.type !== "tool_use") continue;
|
|
105
|
+
if (block.call_id.trim().length === 0) {
|
|
106
|
+
violations.push({ code: "EMPTY_TOOL_USE_ID", detail: `tool_use block ${index} has an empty call_id` });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const previous = seen.get(block.call_id);
|
|
110
|
+
if (previous !== undefined) {
|
|
111
|
+
violations.push({ code: "DUPLICATE_TOOL_USE", detail: `tool_use block ${index} repeats the call_id of block ${previous}` });
|
|
112
|
+
} else seen.set(block.call_id, index);
|
|
113
|
+
}
|
|
114
|
+
return violations;
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
/**
|
|
90
118
|
* The only event types that replay as messages.
|
|
91
119
|
*
|
|
@@ -110,9 +138,11 @@ export interface ProjectViolation {
|
|
|
110
138
|
* two things". It is a following message rather than a mutation of the user
|
|
111
139
|
* turn so appending the event only appends to the projection (P7).
|
|
112
140
|
*/
|
|
113
|
-
type ReplayEvent = Extract<LogEvent, { t: "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
|
|
141
|
+
type ReplayEvent = Extract<LogEvent, { t: "acceptance_contract" | "acceptance_decision" | "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "snapshot" | "extension" }>;
|
|
114
142
|
|
|
115
143
|
function replayable(e: LogEvent): e is ReplayEvent {
|
|
144
|
+
if (e.t === "tool_result" && (e.parent_call_id !== undefined || e.origin === "extension")) return false;
|
|
145
|
+
if (e.t === "acceptance_contract" || e.t === "acceptance_decision") return true;
|
|
116
146
|
if (e.t === "extension") return e.inContext;
|
|
117
147
|
if (e.t === "user_bash") return !e.excludeFromContext;
|
|
118
148
|
if (e.t === "rewind") return e.mode !== "both";
|
|
@@ -198,7 +228,7 @@ export function renderRewindMessage(e: Extract<LogEvent, { t: "rewind" }>): Mess
|
|
|
198
228
|
*/
|
|
199
229
|
export function renderHandoffMessage(
|
|
200
230
|
handoff: Handoff,
|
|
201
|
-
logHint?: { manifestPath: string; segmentPath: string },
|
|
231
|
+
logHint?: { manifestPath: string; segmentPath: string; scopedGuidance?: true },
|
|
202
232
|
): Message {
|
|
203
233
|
const yaml = emitHandoff(handoff);
|
|
204
234
|
|
|
@@ -221,8 +251,10 @@ export function renderHandoffMessage(
|
|
|
221
251
|
"detail this state does not carry, read it rather than guessing:",
|
|
222
252
|
` - segment index: ${logHint.manifestPath}`,
|
|
223
253
|
` - previous segment: ${logHint.segmentPath}`,
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
...(logHint.scopedGuidance !== true ? [
|
|
255
|
+
"Each line is one JSON event. Search it with history; read a line range with read,",
|
|
256
|
+
"or filter with bash and jq. The grep tool skips the log directory.",
|
|
257
|
+
] : ["Each line is one JSON event. Recover originals using the declared tools' contracts; if evidence is inaccessible, state the gap instead of guessing."]),
|
|
226
258
|
);
|
|
227
259
|
}
|
|
228
260
|
|
|
@@ -251,9 +283,11 @@ export function renderHandoffMessage(
|
|
|
251
283
|
export function projectContext(input: ProjectInput): ProjectResult {
|
|
252
284
|
const violations: ProjectViolation[] = [];
|
|
253
285
|
const messages: Message[] = [];
|
|
286
|
+
const sources: string[][] = [];
|
|
287
|
+
const append = (message: Message, ids: string[]) => { messages.push(message); sources.push(ids); };
|
|
254
288
|
|
|
255
289
|
if (input.handoff) {
|
|
256
|
-
|
|
290
|
+
append(renderHandoffMessage(input.handoff, input.logHint), [`handoff:${input.handoff.predecessor?.node ?? "root"}`]);
|
|
257
291
|
}
|
|
258
292
|
|
|
259
293
|
const events = input.events.filter(replayable);
|
|
@@ -262,10 +296,12 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
262
296
|
// a SINGLE user message. Splitting them across messages teaches the model to stop
|
|
263
297
|
// issuing parallel calls, which is a silent, permanent capability regression.
|
|
264
298
|
let pending: ContentBlock[] = [];
|
|
299
|
+
let pendingSources: string[] = [];
|
|
265
300
|
const flushPending = () => {
|
|
266
301
|
if (pending.length > 0) {
|
|
267
|
-
|
|
302
|
+
append({ role: "user", content: pending }, pendingSources);
|
|
268
303
|
pending = [];
|
|
304
|
+
pendingSources = [];
|
|
269
305
|
}
|
|
270
306
|
};
|
|
271
307
|
|
|
@@ -273,29 +309,38 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
273
309
|
let awaiting = new Set<string>();
|
|
274
310
|
|
|
275
311
|
for (const e of events) {
|
|
312
|
+
if (e.t === "acceptance_contract" || e.t === "acceptance_decision") {
|
|
313
|
+
flushPending();
|
|
314
|
+
if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `acceptance event ${e.id} follows unanswered tools` });
|
|
315
|
+
append({ role: "user", content: [{ type: "text", text: e.t === "acceptance_contract"
|
|
316
|
+
? `Declared acceptance contract (execution success alone does not satisfy it): ${JSON.stringify(e.contract)}`
|
|
317
|
+
: `User accepted criterion ${e.criterion} of contract ${e.contract_id} for request ${e.request}.` +
|
|
318
|
+
(e.target_binding ? ` Bound to runtime instance ${e.target_binding}, observation ${e.target_observation}. Later runtime changes invalidate this acceptance.` : "") }] }, [e.id]);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
276
321
|
if (e.t === "extension") {
|
|
277
322
|
flushPending();
|
|
278
323
|
if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `custom message ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}` });
|
|
279
|
-
|
|
324
|
+
append({ role: "user", content: [{ type: "text", text: e.content }] }, [e.id]);
|
|
280
325
|
continue;
|
|
281
326
|
}
|
|
282
327
|
if (e.t === "user_bash") {
|
|
283
328
|
flushPending();
|
|
284
|
-
|
|
329
|
+
append({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] }, [e.id]);
|
|
285
330
|
continue;
|
|
286
331
|
}
|
|
287
332
|
if (e.t === "snapshot") {
|
|
288
333
|
flushPending();
|
|
289
|
-
|
|
334
|
+
append({ role: "user", content: [{ type: "text",
|
|
290
335
|
text: `Workspace observation: changes outside declared file edits: ${e.undeclared.join(", ")}. ${REREAD_INSTRUCTION}`,
|
|
291
|
-
}] });
|
|
336
|
+
}] }, [e.id]);
|
|
292
337
|
continue;
|
|
293
338
|
}
|
|
294
339
|
if (e.t === "rewind") {
|
|
295
340
|
// Flushed first for the same reason a user turn is: it opens a user
|
|
296
341
|
// message, and a pending tool_result batch has to close before it.
|
|
297
342
|
flushPending();
|
|
298
|
-
|
|
343
|
+
append(renderRewindMessage(e), [e.id]);
|
|
299
344
|
continue;
|
|
300
345
|
}
|
|
301
346
|
if (e.t === "user" || e.t === "guidance") {
|
|
@@ -307,8 +352,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
307
352
|
});
|
|
308
353
|
awaiting = new Set();
|
|
309
354
|
}
|
|
310
|
-
|
|
311
|
-
...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] });
|
|
355
|
+
append({ role: "user", content: [...(e.text || e.t === "guidance" || !e.images?.length ? [{ type: "text" as const, text: e.text }] : []),
|
|
356
|
+
...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] }, [e.id]);
|
|
312
357
|
continue;
|
|
313
358
|
}
|
|
314
359
|
if (e.t === "interrupt") {
|
|
@@ -316,19 +361,22 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
316
361
|
// look-ahead mutation of that turn would rewrite it once a later
|
|
317
362
|
// event (tool_use, another user) was already projected after it.
|
|
318
363
|
flushPending();
|
|
319
|
-
|
|
364
|
+
append({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] }, [e.id]);
|
|
320
365
|
continue;
|
|
321
366
|
}
|
|
322
367
|
|
|
323
368
|
if (e.t === "assistant") {
|
|
324
369
|
flushPending();
|
|
370
|
+
violations.push(...toolCallIdentityViolations(e.blocks).map(violation => ({
|
|
371
|
+
...violation, detail: `assistant turn ${e.id}: ${violation.detail}`,
|
|
372
|
+
})));
|
|
325
373
|
if (awaiting.size > 0) {
|
|
326
374
|
violations.push({
|
|
327
375
|
code: "UNPAIRED_TOOL_USE",
|
|
328
376
|
detail: `assistant turn ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}`,
|
|
329
377
|
});
|
|
330
378
|
}
|
|
331
|
-
|
|
379
|
+
append({ role: "assistant", content: e.blocks.map(toContentBlock) }, [e.id]);
|
|
332
380
|
awaiting = new Set(
|
|
333
381
|
e.blocks.filter((b) => b.type === "tool_use").map((b) => (b as { call_id: string }).call_id),
|
|
334
382
|
);
|
|
@@ -344,10 +392,11 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
344
392
|
continue;
|
|
345
393
|
}
|
|
346
394
|
awaiting.delete(e.call_id);
|
|
395
|
+
pendingSources.push(e.id);
|
|
347
396
|
pending.push({
|
|
348
397
|
type: "tool_result",
|
|
349
398
|
tool_use_id: e.call_id,
|
|
350
|
-
content: e.content,
|
|
399
|
+
content: diagnosticContent(e.content, e.diagnostic) + (e.warnings?.length ? "\nObserver warnings (operation result retained): " + JSON.stringify(e.warnings) : ""),
|
|
351
400
|
...(e.ok ? {} : { is_error: true }),
|
|
352
401
|
});
|
|
353
402
|
// Images follow the paired text result, so APIs without image tool outputs receive a user image.
|
|
@@ -368,6 +417,8 @@ export function projectContext(input: ProjectInput): ProjectResult {
|
|
|
368
417
|
}
|
|
369
418
|
|
|
370
419
|
return {
|
|
420
|
+
...(input.trace ? { provenance: { version: PROJECTION_VERSION, messages: sources,
|
|
421
|
+
omitted: input.events.filter(e => !sources.some(ids => ids.includes(e.id))).map(e => e.id) } } : {}),
|
|
371
422
|
system: input.frozen.system,
|
|
372
423
|
tools: input.frozen.tools,
|
|
373
424
|
messages,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** R10 / D6: stable scope and completion rules; execution details belong to tool contracts. */
|
|
2
|
+
export function taskGuidance(): string {
|
|
3
|
+
return [
|
|
4
|
+
"How to work:",
|
|
5
|
+
"- Keep going until the request is fully resolved. The user's request defines scope; a tool profile only selects capabilities. State reasonable assumptions; ask only for decisions the user must make.",
|
|
6
|
+
"- Implementation: understand the relevant code, reproduce bugs, finish every requested change and verify the final state with the project's tests, build or type check. Finish with what changed and the command that verified it, or the specific blocker. A research answer does not complete an implementation request.",
|
|
7
|
+
"- Research: answer the requested questions with original sources, distinguish evidence from inference, and report gaps. Do not change files merely to establish completion.",
|
|
8
|
+
"- Data analysis: calculate over the complete declared inputs; report sources, denominators, exclusions and uncertainty. Truncated previews cannot establish totals.",
|
|
9
|
+
"- Runtime or visual work: bind checks to the actual build and running target. Automated checks do not establish human acceptance; report any required acceptance as pending.",
|
|
10
|
+
"- Put independent reads and searches in one response; they run in parallel. After a command fails, read the end of its output first. If the same approach fails twice, change the approach instead of retrying it.",
|
|
11
|
+
"- Use only declared capabilities and obey admission decisions. If required evidence is inaccessible, state the specific blocker without inventing success, bypassing permission or replaying an operation with uncertain effects.",
|
|
12
|
+
].join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ToolGuidance {
|
|
16
|
+
/** Only builtin companions with these exact contracts can satisfy the dependency. */
|
|
17
|
+
requires: readonly string[];
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Capability projection is deterministic; excluding a companion cannot add advice. */
|
|
22
|
+
export function toolDescription(description: string, guidance: readonly ToolGuidance[], tools: readonly string[]): string {
|
|
23
|
+
return [description, ...guidance.filter(item => item.requires.every(name => tools.includes(name))).map(item => item.text)].join(" ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function recoveryInstructions(tools: readonly string[]): string {
|
|
27
|
+
return (tools.includes("history")
|
|
28
|
+
? "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; event=ID reads one original. history(work_id=ID) recovers work. "
|
|
29
|
+
: "Original user messages and evidence remain at the source references. The original-source recovery capability is unavailable; state any resulting evidence gap instead of guessing. ") +
|
|
30
|
+
"Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting.";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Preserve the source locator even when the interface for retrieving it is unavailable. */
|
|
34
|
+
export function artifactRecovery(sha256: string, offset: number, tools: readonly string[] = ["history"]): string {
|
|
35
|
+
const source = `artifact=${sha256} offset=${offset}`;
|
|
36
|
+
return tools.includes("history") ? `history ${source}` : `${source}; recovery capability unavailable`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Q.21: task-specific shape advice cannot reveal the independent numeric oracle. */
|
|
2
|
+
import type { ToolModeTask } from "./tool-mode-protocol.ts";
|
|
3
|
+
|
|
4
|
+
export function queryEvalContract(task: ToolModeTask, paths: readonly string[], answerSha256: string) {
|
|
5
|
+
const columns = task === "tool-small" ? ["amount"] : task === "tool-batch" ? ["total"] : ["team", "total"];
|
|
6
|
+
const rowCount = task === "tool-aggregate" ? 4 : 1;
|
|
7
|
+
const operation = task === "tool-small" ? "select amount; return one row with only amount" : task === "tool-batch"
|
|
8
|
+
? "sum amount across all records as total; return one row with only total per source"
|
|
9
|
+
: "keep enabled=true, group by team, sum amount as total, and sort by team ascending; return four rows with columns team,total in that order";
|
|
10
|
+
const instruction = "Use query on each of " + paths.join(", ") + " to " + operation +
|
|
11
|
+
'. Write compact JSON with a trailing newline to answer.txt: [{"path":"data-0.jsonl","rows":[...]}], one item per source in the listed order. ' +
|
|
12
|
+
"Preserve all fixture inputs and run bun run gate.";
|
|
13
|
+
return { instruction, columns, rowCount, paths: [...paths], answerSha256 };
|
|
14
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** R2 / D8: source bytes, an unclipped observation, and the visible projection have distinct identities. */
|
|
2
|
+
import type { ArtifactRef } from "./events.ts";
|
|
3
|
+
import { artifactRecovery } from "./prompt-policy.ts";
|
|
4
|
+
import { headTailSelection, isObservationArtifact as ref, isBytePreview, type BytePreview } from "./observation-view.ts";
|
|
5
|
+
|
|
6
|
+
export interface ObservationReceipt {
|
|
7
|
+
version: 1;
|
|
8
|
+
source: { host: string; workspace: string; resource: string; sha256: string; raw: ArtifactRef };
|
|
9
|
+
artifact: ArtifactRef;
|
|
10
|
+
/** R4: content-addressed version premise, separate from this formatted observation. */
|
|
11
|
+
descriptor?: ArtifactRef;
|
|
12
|
+
selection: { start: number; end: number; total_lines: number };
|
|
13
|
+
preview: BytePreview;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
const size = (text: string) => encoder.encode(text).length;
|
|
18
|
+
/** Existing assumed preview cap; archival removes data loss without changing the display policy. */
|
|
19
|
+
const READ_MAX_LINE_CHARS = 2000;
|
|
20
|
+
|
|
21
|
+
export function readObservationText(header: string, lines: readonly string[], start: number): string {
|
|
22
|
+
return header + lines.map((line, i) => `${start + i}\t${line}`).join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Map two successive cuts (per line and total bytes) back to the same immutable observation. */
|
|
26
|
+
export function previewReadObservation(header: string, lines: readonly string[], start: number, artifact: string,
|
|
27
|
+
maxBytes: number, maxLineChars = READ_MAX_LINE_CHARS, tools?: readonly string[]): { text: string; preview: ObservationReceipt["preview"]; semantic: string } {
|
|
28
|
+
const spans: { view: number; source: number; bytes: number }[] = [];
|
|
29
|
+
let text = "", source = 0, view = 0;
|
|
30
|
+
const mapped = (value: string, offset: number) => {
|
|
31
|
+
const bytes = size(value);
|
|
32
|
+
if (bytes) spans.push({ view, source: offset, bytes });
|
|
33
|
+
text += value; view += bytes;
|
|
34
|
+
};
|
|
35
|
+
const synthetic = (value: string) => { text += value; view += size(value); };
|
|
36
|
+
mapped(header, 0); source += size(header);
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i]!, prefix = `${start + i}\t`;
|
|
39
|
+
let end = Math.min(line.length, maxLineChars);
|
|
40
|
+
// JavaScript string indices are UTF-16; a character can occupy a surrogate pair.
|
|
41
|
+
if (end > 0 && end < line.length && /[\uD800-\uDBFF]/.test(line[end - 1]!)) end--;
|
|
42
|
+
const kept = prefix + line.slice(0, end);
|
|
43
|
+
mapped(kept, source);
|
|
44
|
+
if (end < line.length) synthetic(` [line truncated: ${line.length} chars; ${artifactRecovery(artifact, source + size(kept), tools)}]`);
|
|
45
|
+
source += size(prefix + line);
|
|
46
|
+
if (i < lines.length - 1) { mapped("\n", source); source++; }
|
|
47
|
+
}
|
|
48
|
+
const bytes = encoder.encode(text);
|
|
49
|
+
const selected = headTailSelection(bytes, bytes, bytes.length, maxBytes,
|
|
50
|
+
omitted => `\n[truncated ${omitted} preview bytes; ${artifactRecovery(artifact, 0, tools)}]\n`);
|
|
51
|
+
const visible: [number, number][] = [];
|
|
52
|
+
for (const [lo, hi] of selected.visible) for (const span of spans) {
|
|
53
|
+
const begin = Math.max(lo, span.view), end = Math.min(hi, span.view + span.bytes);
|
|
54
|
+
if (end <= begin) continue;
|
|
55
|
+
const a = span.source + begin - span.view, b = span.source + end - span.view;
|
|
56
|
+
const last = visible.at(-1);
|
|
57
|
+
if (last?.[1] === a) last[1] = b;
|
|
58
|
+
else visible.push([a, b]);
|
|
59
|
+
}
|
|
60
|
+
let next = 0;
|
|
61
|
+
for (const [lo, hi] of visible) { if (lo !== next) break; next = hi; }
|
|
62
|
+
const full = encoder.encode(readObservationText(header, lines, start));
|
|
63
|
+
const semantic = visible.map(([lo, hi]) => new TextDecoder().decode(full.subarray(Math.max(lo, size(header)), Math.max(hi, size(header))))).join("");
|
|
64
|
+
return { text: selected.text, semantic, preview: { unit: "utf8_bytes", complete: next === source,
|
|
65
|
+
visible, next_offset: next === source ? null : next } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
69
|
+
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
70
|
+
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
71
|
+
|
|
72
|
+
/** Optional on old logs, strict when present. A cursor cannot certify bytes outside its artifact. */
|
|
73
|
+
export function isObservationReceipt(value: unknown): value is ObservationReceipt {
|
|
74
|
+
if (!record(value) || value["version"] !== 1 || !ref(value["artifact"])) return false;
|
|
75
|
+
if (value["descriptor"] !== undefined && !ref(value["descriptor"])) return false;
|
|
76
|
+
const source = value["source"], selection = value["selection"], preview = value["preview"];
|
|
77
|
+
if (!record(source) || !["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") ||
|
|
78
|
+
!digest(source["sha256"]) || !ref(source["raw"]) || source["sha256"] !== source["raw"].sha256) return false;
|
|
79
|
+
if (!record(selection) || !natural(selection["start"]) || selection["start"] < 1 || !natural(selection["end"]) ||
|
|
80
|
+
!natural(selection["total_lines"]) || selection["end"] < selection["start"] || selection["end"] > selection["total_lines"]) return false;
|
|
81
|
+
return isBytePreview(preview, value["artifact"].bytes);
|
|
82
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** R12c: curated mechanisms, never copied private transcript contents or population estimates. */
|
|
2
|
+
export const RECOVERY_SOURCE_REPORT = { path: "docs/research/local-agent-tool-audit-2026-10-06.md",
|
|
3
|
+
sha256: "a4c83ec9e5c52ea6500434673f0e90c171e55aa247d293dc3f09c893ec604832" } as const;
|
|
4
|
+
export const RECOVERY_CASES = [
|
|
5
|
+
{id:"recovery-missing-path",family:"parameters",partition:"development",source:"E2",basis:"observed",scenario:"An edit omits its required file identity."},
|
|
6
|
+
{id:"recovery-stale-version",family:"version",partition:"development",source:"section-2.2-replacement-mismatch",basis:"derived-scenario",scenario:"An obsolete file digest must not authorize an edit over newer content; the source mismatch alone did not establish a stale-version cause."},
|
|
7
|
+
{id:"recovery-long-output",family:"output",partition:"development",source:"X2",basis:"observed",scenario:"A clipped long line hides the needed value; recover immutable original bytes."},
|
|
8
|
+
{id:"recovery-api-mismatch",family:"environment",partition:"holdout",source:"E4",basis:"portable-analogue",scenario:"An older local API rejects a named option; inspect its actual contract before retrying."},
|
|
9
|
+
{id:"recovery-closed-process",family:"lifecycle",partition:"development",source:"E5",basis:"derived-scenario",scenario:"A finished process cannot accept more input; the original unknown ID did not establish why ownership was lost."},
|
|
10
|
+
{id:"recovery-unchanged-retry",family:"recovery",partition:"development",source:"E3",basis:"derived-scenario",scenario:"Changing replacement wording does not repair an unchanged missing anchor; this extends the source's identical retry mechanism."},
|
|
11
|
+
{id:"recovery-runtime-acceptance",family:"acceptance",partition:"holdout",source:"E6",basis:"scenario-from-user-question",scenario:"A build is not a running current instance or author acceptance; bind the preview to its build."},
|
|
12
|
+
{id:"recovery-eof",family:"parameters",partition:"holdout",source:"section-2.2-read-range",basis:"observed",scenario:"A line request exceeds the real file boundary; recover from the reported range."},
|
|
13
|
+
] as const;
|
|
14
|
+
export type RecoveryTask = typeof RECOVERY_CASES[number]["id"];
|
|
15
|
+
export const RECOVERY_TASKS = RECOVERY_CASES.map(item=>item.id);
|
|
16
|
+
export const recoveryCase = (task:string) => RECOVERY_CASES.find(item=>item.id===task);
|
|
17
|
+
export type RecoveryPartition = "all" | "development" | "holdout";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** R12c: metrics are projections of original calls, not generated tool-result prose. */
|
|
2
|
+
import type {LogEvent} from "./events.ts";
|
|
3
|
+
import {advanceRecovery,emptyRecoveryState} from "./failure-recovery.ts";
|
|
4
|
+
import {resolveEvidence} from "./evidence.ts";
|
|
5
|
+
|
|
6
|
+
export interface RecoveryMetrics {
|
|
7
|
+
toolCalls:number; recordedInvocations:number; outerCalls:number; nestedCalls:number; unresolvedCalls:number; toolFailures:number;
|
|
8
|
+
firstToolSuccess:boolean|null; firstPassTaskSuccess:boolean|null; finalTaskSuccess:boolean|null;
|
|
9
|
+
recoveryCalls:number; unchangedFailureRetries:number; classifiedFailures:number; unclassifiedFailures:number;
|
|
10
|
+
originalEvidenceRetrievals:number; unsupportedClaims:number;
|
|
11
|
+
}
|
|
12
|
+
export function deriveRecoveryMetrics(nodes:readonly {node:number;events:readonly LogEvent[]}[],verifiedRetrievals:ReadonlySet<string>,
|
|
13
|
+
finalTaskSuccess:boolean|null,identify:(path:string)=>string):RecoveryMetrics{
|
|
14
|
+
type Call={name:string;ordinal:number;recorded:boolean;nested:boolean;ok:boolean|null};
|
|
15
|
+
const calls=new Map<string,Call>();let ordinal=0,firstFailure=Infinity,repeated=0,classified=0,unclassified=0,retrieved=0,unsupported=0;
|
|
16
|
+
let state=emptyRecoveryState();
|
|
17
|
+
for(const part of nodes)for(const [index,event] of part.events.entries()){
|
|
18
|
+
ordinal++;
|
|
19
|
+
if(event.t==="assistant")for(const block of event.blocks)if(block.type==="tool_use"){
|
|
20
|
+
const key=`${part.node}:${block.call_id}`;
|
|
21
|
+
if(calls.has(key))throw Error("duplicate recovery call identity");
|
|
22
|
+
calls.set(key,{name:block.name,ordinal,recorded:false,nested:false,ok:null});
|
|
23
|
+
}
|
|
24
|
+
if(event.t==="tool_invocation"){
|
|
25
|
+
const key=`${part.node}:${event.call_id}`,prior=calls.get(key);
|
|
26
|
+
if(prior?.recorded)throw Error("duplicate recovery invocation");
|
|
27
|
+
calls.set(key,{name:event.name,ordinal:prior?.ordinal??ordinal,recorded:true,nested:event.parent_call_id!==undefined,ok:prior?.ok??null});
|
|
28
|
+
}
|
|
29
|
+
if(event.t==="tool_result"){
|
|
30
|
+
const key=`${event.invocation_ref?.node??part.node}:${event.call_id}`,call=calls.get(key);
|
|
31
|
+
if(!call||call.ok!==null)throw Error("unpaired recovery result");
|
|
32
|
+
call.ok=event.ok;
|
|
33
|
+
if(!event.ok){firstFailure=Math.min(firstFailure,ordinal);if(event.diagnostic)classified++;else unclassified++;}
|
|
34
|
+
if(call.name==="history"&&event.ok&&verifiedRetrievals.has(`${part.node}:${event.id}`))retrieved++;
|
|
35
|
+
}
|
|
36
|
+
if(event.t==="claim"&&!resolveEvidence(part.events.slice(0,index),event.evidence).ok)unsupported++;
|
|
37
|
+
state=advanceRecovery(state,[event],identify);
|
|
38
|
+
if(event.t==="tool_result"&&!event.ok&&[...state.failures.values()].some(run=>run.count>1&&run.sources.at(-1)===event.id))repeated++;
|
|
39
|
+
}
|
|
40
|
+
const rows=[...calls.values()],failed=rows.filter(call=>call.ok===false).length,unresolved=rows.filter(call=>call.ok===null).length;
|
|
41
|
+
// An invocation record proves the attempt was logged, not that schema or
|
|
42
|
+
// permission checks admitted its effect. Rejected requests have receipts too.
|
|
43
|
+
return {toolCalls:rows.length,recordedInvocations:rows.filter(call=>call.recorded).length,outerCalls:rows.filter(call=>!call.nested).length,
|
|
44
|
+
nestedCalls:rows.filter(call=>call.nested).length,unresolvedCalls:unresolved,toolFailures:failed,firstToolSuccess:rows[0]?.ok??null,
|
|
45
|
+
firstPassTaskSuccess:finalTaskSuccess===null?null:finalTaskSuccess&&failed===0&&unresolved===0,
|
|
46
|
+
finalTaskSuccess,recoveryCalls:rows.filter(call=>call.ordinal>firstFailure).length,unchangedFailureRetries:repeated,
|
|
47
|
+
classifiedFailures:classified,unclassifiedFailures:unclassified,originalEvidenceRetrievals:retrieved,unsupportedClaims:unsupported};
|
|
48
|
+
}
|
|
49
|
+
export function isRecoveryMetrics(value:unknown):value is RecoveryMetrics{
|
|
50
|
+
if(!value||typeof value!=="object")return false;
|
|
51
|
+
const v=value as Record<string,unknown>;
|
|
52
|
+
const numbers=["toolCalls","recordedInvocations","outerCalls","nestedCalls","unresolvedCalls","toolFailures","recoveryCalls","unchangedFailureRetries","classifiedFailures","unclassifiedFailures","originalEvidenceRetrievals","unsupportedClaims"];
|
|
53
|
+
if(numbers.some(key=>!Number.isSafeInteger(v[key])||Number(v[key])<0))return false;
|
|
54
|
+
if(["firstToolSuccess","firstPassTaskSuccess","finalTaskSuccess"].some(key=>v[key]!==null&&typeof v[key]!=="boolean"))return false;
|
|
55
|
+
const n=(key:string)=>Number(v[key]);
|
|
56
|
+
return n("outerCalls")+n("nestedCalls")===n("toolCalls")&&n("recordedInvocations")<=n("toolCalls")
|
|
57
|
+
&&n("unresolvedCalls")+n("toolFailures")<=n("toolCalls")&&n("classifiedFailures")+n("unclassifiedFailures")===n("toolFailures")
|
|
58
|
+
&&n("recoveryCalls")<=n("toolCalls")&&n("unchangedFailureRetries")<=n("toolFailures")&&n("originalEvidenceRetrievals")<=n("toolCalls")-n("toolFailures")-n("unresolvedCalls")
|
|
59
|
+
&&(v["firstPassTaskSuccess"]!==true||v["finalTaskSuccess"]===true&&n("toolFailures")===0&&n("unresolvedCalls")===0);
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** R9 / D8: navigation into original ancestry, never a semantic summary or permission grant. */
|
|
2
|
+
import type { LogEvent } from "./events.ts";
|
|
3
|
+
import { historyEntries, type HistoryEntry } from "./history-query.ts";
|
|
4
|
+
import { isEnvironmentReceipt } from "./environment.ts";
|
|
5
|
+
import { resourceKey } from "./evidence-links.ts";
|
|
6
|
+
import type { ToolDiagnostic } from "./tool-contract.ts";
|
|
7
|
+
|
|
8
|
+
export const RECOVERY_SECTIONS = ["users", "failures", "environment", "next_actions"] as const;
|
|
9
|
+
export type RecoverySection = typeof RECOVERY_SECTIONS[number];
|
|
10
|
+
export const RECOVERY_KINDS = { users: "user", failures: "failure", environment: "environment", next_actions: "next_action" } as const;
|
|
11
|
+
export type RecoveryNodes = readonly { node: number; events: readonly LogEvent[] }[];
|
|
12
|
+
export type RecoveryReference = {
|
|
13
|
+
node: number; event: string; kind: "user" | "failure" | "environment" | "next_action";
|
|
14
|
+
/** This is the recorded role, not proof of current authorization or human authorship. */
|
|
15
|
+
provenance: "user_message" | "observation" | "model_annotation";
|
|
16
|
+
abandoned?: true; tool?: string; work?: string;
|
|
17
|
+
resources?: readonly string[];
|
|
18
|
+
/** A failed request can name a resource without having observed it. */
|
|
19
|
+
requested_resource?: string;
|
|
20
|
+
diagnostic?: Pick<ToolDiagnostic, "code" | "execution" | "effects">;
|
|
21
|
+
host?: string; workspace?: string; cwd?: string; program?: string;
|
|
22
|
+
};
|
|
23
|
+
export type RecoverySources = Record<RecoverySection, RecoveryReference[]>;
|
|
24
|
+
export type RecoveryRanges = Record<RecoverySection, { total: number; omitted: number }>;
|
|
25
|
+
|
|
26
|
+
/** Newest first within each role; no keywords decide which user sentence is a constraint. */
|
|
27
|
+
export function recoverySources(nodes: RecoveryNodes, entries: readonly HistoryEntry[] = historyEntries(nodes)): RecoverySources {
|
|
28
|
+
const abandoned = new Set(nodes.flatMap(part => part.events.flatMap(event => event.t === "interrupt" ? [event.user_id] : [])));
|
|
29
|
+
const result: RecoverySources = { users: [], failures: [], environment: [], next_actions: [] };
|
|
30
|
+
for (const entry of entries.toReversed()) {
|
|
31
|
+
const source = { node: entry.node, event: entry.event };
|
|
32
|
+
if (entry.kind === "user") result.users.push({ ...source, kind: "user", provenance: "user_message",
|
|
33
|
+
...(abandoned.has(entry.event) ? { abandoned: true } : {}) });
|
|
34
|
+
if (entry.kind === "next_action") result.next_actions.push({ ...source, kind: "next_action", provenance: "model_annotation",
|
|
35
|
+
...(entry.work_ids?.[0] ? { work: entry.work_ids[0] } : {}) });
|
|
36
|
+
if (entry.kind !== "tool_result") continue;
|
|
37
|
+
if (entry.ok === false) {
|
|
38
|
+
const input = entry.input !== null && typeof entry.input === "object" ? entry.input as Record<string, unknown> : {};
|
|
39
|
+
const requested = typeof input["path"] === "string" ? input["path"] : typeof input["cwd"] === "string" ? input["cwd"] : undefined;
|
|
40
|
+
const diagnostic = entry.diagnostic;
|
|
41
|
+
result.failures.push({ ...source, kind: "failure", provenance: "observation", ...(entry.tool ? { tool: entry.tool } : {}),
|
|
42
|
+
...(entry.resources?.length ? { resources: entry.resources } : {}),
|
|
43
|
+
...(requested ? { requested_resource: resourceKey(requested) } : {}),
|
|
44
|
+
...(diagnostic ? { diagnostic: { code: diagnostic.code,
|
|
45
|
+
...(diagnostic.execution ? { execution: diagnostic.execution } : {}), ...(diagnostic.effects ? { effects: diagnostic.effects } : {}) } } : {}) });
|
|
46
|
+
}
|
|
47
|
+
if (entry.tool === "environment") {
|
|
48
|
+
const receipt = entry.data;
|
|
49
|
+
// Only a validated actual receipt contributes identity. Text that looks
|
|
50
|
+
// like a host or a successful probe does not become a machine fact (D11).
|
|
51
|
+
const identity = isEnvironmentReceipt(receipt) && entry.effect?.kind === "command" && entry.effect.exit === receipt.evidence.exit
|
|
52
|
+
? { host: receipt.source.host, workspace: receipt.source.workspace, cwd: receipt.source.cwd, program: receipt.query.program } : {};
|
|
53
|
+
result.environment.push({ ...source, kind: "environment", provenance: "observation", ...identity });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Source labels are stable even when the runtime attaches a fresh validity view. */
|
|
60
|
+
export function recoveryPage(sources: RecoverySources, entries: readonly HistoryEntry[], section: RecoverySection, offset: number, limit: number): {
|
|
61
|
+
total: number; offset: number; next: number | null; items: { source: RecoveryReference; original: HistoryEntry }[];
|
|
62
|
+
} {
|
|
63
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
64
|
+
throw new RangeError("recovery offset must be nonnegative and limit must be 1..100");
|
|
65
|
+
}
|
|
66
|
+
const indexed = new Map(entries.map(entry => [entry.event, entry]));
|
|
67
|
+
const references = sources[section], end = Math.min(references.length, offset + limit);
|
|
68
|
+
return { total: references.length, offset, next: end < references.length ? end : null,
|
|
69
|
+
items: references.slice(offset, end).map(source => {
|
|
70
|
+
const original = indexed.get(source.event);
|
|
71
|
+
if (!original || original.node !== source.node) throw new Error("recovery source is absent from the selected ancestry");
|
|
72
|
+
return { source, original };
|
|
73
|
+
}) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Q23 / D9: diagnostic observations, never a new anti-stall or success policy. */
|
|
2
|
+
import type { LogEvent, ToolResultEvent } from "./events.ts";
|
|
3
|
+
import { deriveAcceptance, type AcceptanceState } from "./acceptance.ts";
|
|
4
|
+
import { advanceRecovery, emptyRecoveryState, failureIdentity } from "./failure-recovery.ts";
|
|
5
|
+
import { canonicalValue } from "./continuation.ts";
|
|
6
|
+
import { changedPaths } from "./checkpoints.ts";
|
|
7
|
+
|
|
8
|
+
export type RequestSource = { node: number; event: string };
|
|
9
|
+
export type RequestRecord = { node: number; event: LogEvent };
|
|
10
|
+
export type RequestProgress = Record<"observations" | "artifact_changes" | "acceptance_advances" |
|
|
11
|
+
"new_diagnostics" | "unchanged_retries" | "unknown_diagnostics", RequestSource[]>;
|
|
12
|
+
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
function observationDigest(event: ToolResultEvent): string | undefined {
|
|
15
|
+
if (event.observation_sha256 !== undefined) return event.observation_sha256;
|
|
16
|
+
// Command receipts archive stdout/stderr before the timing/session header is
|
|
17
|
+
// rendered. Only that explicit matching output reference is a safe fallback.
|
|
18
|
+
const data = event.data;
|
|
19
|
+
if (event.effect?.kind !== "command" || !event.content_ref || !object(data) || data["capture"] === "partial") return undefined;
|
|
20
|
+
const output = data["output"];
|
|
21
|
+
return object(output) &&
|
|
22
|
+
output["sha256"] === event.content_ref.sha256 && output["bytes"] === event.content_ref.bytes ? event.content_ref.sha256 : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function deriveRequestProgress(records: readonly RequestRecord[]): RequestProgress {
|
|
26
|
+
const result: RequestProgress = { observations: [], artifact_changes: [], acceptance_advances: [],
|
|
27
|
+
new_diagnostics: [], unchanged_retries: [], unknown_diagnostics: [] };
|
|
28
|
+
const observations = new Set<string>(), versions = new Set<string>(), failures = new Set<string>();
|
|
29
|
+
let acceptance: AcceptanceState | undefined;
|
|
30
|
+
let recovery = emptyRecoveryState();
|
|
31
|
+
const version = (path: string, before: string | null, after: string | null, source: RequestSource) => {
|
|
32
|
+
const key = (sha: string | null) => canonicalValue([path, sha]);
|
|
33
|
+
versions.add(key(before));
|
|
34
|
+
if (!versions.has(key(after))) result.artifact_changes.push(source);
|
|
35
|
+
versions.add(key(after));
|
|
36
|
+
};
|
|
37
|
+
for (const { node, event } of records) {
|
|
38
|
+
const source = { node, event: event.id };
|
|
39
|
+
const before = acceptance;
|
|
40
|
+
acceptance = deriveAcceptance(acceptance, [event]);
|
|
41
|
+
if (acceptance && before?.contract_event === acceptance.contract_event && acceptance.criteria.some(item =>
|
|
42
|
+
(item.status === "pass" || item.status === "accepted") &&
|
|
43
|
+
before.criteria.find(old => old.id === item.id)?.status !== item.status)) result.acceptance_advances.push(source);
|
|
44
|
+
if (event.t === "tool_result") {
|
|
45
|
+
const effect = event.effect;
|
|
46
|
+
const call = recovery.calls.get(event.call_id);
|
|
47
|
+
if (effect?.kind === "edit") version(effect.path, effect.sha256_before, effect.sha256_after, source);
|
|
48
|
+
if (effect?.kind === "read") versions.add(canonicalValue([effect.path, effect.sha256]));
|
|
49
|
+
// Semantic digests exclude command timing/preview metadata. Legacy raw
|
|
50
|
+
// output hashes cannot prove a new diagnostic and stay explicitly unknown.
|
|
51
|
+
const digest = observationDigest(event);
|
|
52
|
+
if (!event.ok) {
|
|
53
|
+
const identity = call ? failureIdentity(call, event, recovery, path => path) : null;
|
|
54
|
+
if (digest === undefined || identity === null) result.unknown_diagnostics.push(source);
|
|
55
|
+
else {
|
|
56
|
+
const key = canonicalValue([identity, digest]);
|
|
57
|
+
(failures.has(key) ? result.unchanged_retries : result.new_diagnostics).push(source);
|
|
58
|
+
failures.add(key);
|
|
59
|
+
}
|
|
60
|
+
} else if ((effect?.kind === "read" || effect?.kind === "command" ||
|
|
61
|
+
["grep", "find", "ls", "query", "history", "environment"].includes(call?.name ?? "")) &&
|
|
62
|
+
digest !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 && !observations.has(digest)) {
|
|
63
|
+
result.observations.push(source); observations.add(digest);
|
|
64
|
+
}
|
|
65
|
+
if (effect?.kind === "command") for (const output of effect.artifacts?.outputs ?? []) {
|
|
66
|
+
if (output.after.state === "observed") version(output.path,
|
|
67
|
+
output.before.state === "observed" ? output.before.sha256 : null, output.after.sha256, source);
|
|
68
|
+
}
|
|
69
|
+
} else if (event.t === "checkpoint" && event.phase === "applied") {
|
|
70
|
+
for (const path of changedPaths(event.changes)) {
|
|
71
|
+
const change = event.changes[path]!;
|
|
72
|
+
version(path, change.before?.sha256 ?? null, change.after?.sha256 ?? null, source);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
recovery = advanceRecovery(recovery, [event], path => path);
|
|
76
|
+
}
|
|
77
|
+
// One receipt may change several artifacts or satisfy several criteria.
|
|
78
|
+
for (const key of Object.keys(result) as (keyof RequestProgress)[]) {
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
result[key] = result[key].filter(source => {
|
|
81
|
+
const id = canonicalValue(source);
|
|
82
|
+
if (seen.has(id)) return false;
|
|
83
|
+
seen.add(id); return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|