@visual-z/xeno 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2066
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1713
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3001
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -200
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/screen.ts +0 -548
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/core/progress.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* D9 / research M.4: an anti-stall signal, not a judgment of task completion.
|
|
3
|
-
* Novelty belongs to observed content and executed checks, never note wording,
|
|
4
|
-
* event IDs, or timestamps. The full ledger is rebuilt from the active log path;
|
|
5
|
-
* its carried handoff view may be paged like the other structural state (D20).
|
|
6
|
-
*/
|
|
7
|
-
import type { LineRange, LogEvent } from "./events.ts";
|
|
8
|
-
import { isProcessReceipt } from "./process-receipt.ts";
|
|
9
|
-
|
|
10
|
-
export type ProgressEvidence = (
|
|
11
|
-
| { kind: "read"; sha256: string; ranges?: readonly LineRange[]; observation_sha256?: string }
|
|
12
|
-
| { kind: "observation"; sha256: string }
|
|
13
|
-
| { kind: "check"; command: string; gate?: string; workspace_sha256?: string }
|
|
14
|
-
) & { source: { node: number; event_id: string } };
|
|
15
|
-
|
|
16
|
-
/** Test coverage without expanding arbitrarily long ranges into line numbers. */
|
|
17
|
-
function hasUnseenLines(requested: readonly LineRange[], observed: readonly LineRange[]): boolean {
|
|
18
|
-
const sorted = [...observed].sort((a, b) => a[0] - b[0]);
|
|
19
|
-
return requested.some(([start, end]) => {
|
|
20
|
-
let next = start;
|
|
21
|
-
for (const [low, high] of sorted) {
|
|
22
|
-
if (high < next) continue;
|
|
23
|
-
if (low > next) return true;
|
|
24
|
-
if (high >= end) return false;
|
|
25
|
-
next = high + 1;
|
|
26
|
-
}
|
|
27
|
-
return next <= end;
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A file version's fingerprint in the observation set. Keyed by digest alone:
|
|
33
|
-
* the same bytes under another path are not new work either.
|
|
34
|
-
*/
|
|
35
|
-
const versionKey = (_path: string, sha256: string): string => `version:${sha256}`;
|
|
36
|
-
|
|
37
|
-
export function deriveProgress(
|
|
38
|
-
previous: readonly ProgressEvidence[], events: readonly LogEvent[], node: number,
|
|
39
|
-
eligibleChecks: ReadonlySet<string>,
|
|
40
|
-
): { evidence: ProgressEvidence[]; progressed: boolean } {
|
|
41
|
-
const evidence = [...previous];
|
|
42
|
-
const reads = new Map<string, Extract<ProgressEvidence, { kind: "read" }>[]>();
|
|
43
|
-
const observations = new Set<string>();
|
|
44
|
-
const checks = new Set<string>();
|
|
45
|
-
// A named gate has one status per workspace. Changing its arguments does not
|
|
46
|
-
// make another gate, and cannot keep the loop alive by spelling a pass anew.
|
|
47
|
-
const checkKey = (command: string, gate?: string, workspace?: string): string =>
|
|
48
|
-
JSON.stringify([gate === undefined ? "command" : "gate", gate ?? command, workspace ?? null]);
|
|
49
|
-
for (const item of previous) {
|
|
50
|
-
if (item.kind === "read") {
|
|
51
|
-
const seen = reads.get(item.sha256) ?? [];
|
|
52
|
-
seen.push(item);
|
|
53
|
-
reads.set(item.sha256, seen);
|
|
54
|
-
if (item.observation_sha256 !== undefined) observations.add(item.observation_sha256);
|
|
55
|
-
// A version read before a cut is as known as one read in this segment.
|
|
56
|
-
observations.add(versionKey("", item.sha256));
|
|
57
|
-
} else if (item.kind === "observation") observations.add(item.sha256);
|
|
58
|
-
else checks.add(checkKey(item.command, item.gate, item.workspace_sha256));
|
|
59
|
-
}
|
|
60
|
-
let progressed = false;
|
|
61
|
-
const calls = new Map<string, string>();
|
|
62
|
-
for (const event of events) {
|
|
63
|
-
if (event.t === "assistant") {
|
|
64
|
-
for (const block of event.blocks) if (block.type === "tool_use") calls.set(block.call_id, block.name);
|
|
65
|
-
}
|
|
66
|
-
if (event.t !== "tool_result") continue;
|
|
67
|
-
const name = calls.get(event.call_id);
|
|
68
|
-
calls.delete(event.call_id);
|
|
69
|
-
const effect = event.effect;
|
|
70
|
-
// A failed tool that never ran observed nothing. A command that ran and
|
|
71
|
-
// exited nonzero did observe something (S.6 item 2): its output counts
|
|
72
|
-
// like any other, but it is never a check.
|
|
73
|
-
if (!event.ok && effect?.kind !== "command") continue;
|
|
74
|
-
const source = { node, event_id: event.id };
|
|
75
|
-
const observation = event.observation_sha256 ?? event.content_ref?.sha256 ?? event.content_sha256;
|
|
76
|
-
if (effect?.kind === "read" && effect.sha256 !== "") observations.add(versionKey(effect.path, effect.sha256));
|
|
77
|
-
if (effect?.kind === "read" && effect.sha256 !== "") {
|
|
78
|
-
const seen = reads.get(effect.sha256) ?? [];
|
|
79
|
-
const novel = !seen.some((r) => r.ranges === undefined) &&
|
|
80
|
-
(effect.ranges === undefined || hasUnseenLines(effect.ranges, seen.flatMap((r) => r.ranges ?? [])));
|
|
81
|
-
if (novel) {
|
|
82
|
-
const item: Extract<ProgressEvidence, { kind: "read" }> = {
|
|
83
|
-
kind: "read", sha256: effect.sha256, source,
|
|
84
|
-
...(effect.ranges === undefined ? {} : { ranges: effect.ranges }),
|
|
85
|
-
...(observation === undefined ? {} : { observation_sha256: observation }),
|
|
86
|
-
};
|
|
87
|
-
evidence.push(item);
|
|
88
|
-
reads.set(effect.sha256, [...seen, item]);
|
|
89
|
-
// Whole-file hashes can change outside the returned range. Such a read
|
|
90
|
-
// extends coverage for that version but unchanged bytes are not novelty.
|
|
91
|
-
if (observation === undefined || !observations.has(observation)) progressed = true;
|
|
92
|
-
if (observation !== undefined) observations.add(observation);
|
|
93
|
-
}
|
|
94
|
-
} else if (effect?.kind === "command") {
|
|
95
|
-
// D9 revisions (2026-09-29 and S.6 item 2, user-approved): output never
|
|
96
|
-
// seen before is a new observation, exactly as for read/grep, whatever
|
|
97
|
-
// the exit code. A model exploring with `sed -n` or `grep` in the shell
|
|
98
|
-
// is gathering evidence too, and a grep with no match exits 1 after the
|
|
99
|
-
// earlier pipeline stages printed what was asked for. The digest
|
|
100
|
-
// excludes wall time, so an unchanged `git status` or the same failure
|
|
101
|
-
// repeats and stalls.
|
|
102
|
-
if (observation !== undefined && !observations.has(observation)) {
|
|
103
|
-
observations.add(observation);
|
|
104
|
-
evidence.push({ kind: "observation", sha256: observation, source });
|
|
105
|
-
progressed = true;
|
|
106
|
-
}
|
|
107
|
-
const key = checkKey(effect.cmd, effect.gate, effect.workspace_sha256);
|
|
108
|
-
if (event.ok && effect.exit === 0 && !checks.has(key)) {
|
|
109
|
-
checks.add(key);
|
|
110
|
-
evidence.push({ kind: "check", command: effect.cmd, source,
|
|
111
|
-
...(effect.gate === undefined ? {} : { gate: effect.gate }),
|
|
112
|
-
...(effect.workspace_sha256 === undefined ? {} : { workspace_sha256: effect.workspace_sha256 }) });
|
|
113
|
-
// Remember even unclaimed successes. A later model note cannot turn a
|
|
114
|
-
// rerun of old evidence into a fresh completion by giving it a new name.
|
|
115
|
-
if (eligibleChecks.has(event.id)) progressed = true;
|
|
116
|
-
}
|
|
117
|
-
} else if (effect?.kind === "edit") {
|
|
118
|
-
// D9 revision (2026-09-29, user-approved): a file version no one has
|
|
119
|
-
// seen before is new evidence -- it is how implementation work shows up.
|
|
120
|
-
// Versions already seen anywhere (read, or either side of an edit) do
|
|
121
|
-
// not count, so an A->B->A rewrite loop still stalls.
|
|
122
|
-
// Both sides become durable evidence: the ledger is all a later
|
|
123
|
-
// derivation sees, and a version seen only as "before" must still be
|
|
124
|
-
// recognised when the file is edited back to it.
|
|
125
|
-
for (const [sha, isAfter] of [[effect.sha256_before, false], [effect.sha256_after, true]] as const) {
|
|
126
|
-
if (sha === null) continue;
|
|
127
|
-
const key = versionKey(effect.path, sha);
|
|
128
|
-
if (observations.has(key)) continue;
|
|
129
|
-
observations.add(key);
|
|
130
|
-
evidence.push({ kind: "observation", sha256: key, source });
|
|
131
|
-
if (isAfter) progressed = true;
|
|
132
|
-
}
|
|
133
|
-
} else if (isProcessReceipt(event.data) && !event.data.replayed && event.data.output !== undefined && event.data.output.bytes > 0 &&
|
|
134
|
-
!observations.has(event.data.output.sha256)) {
|
|
135
|
-
// A managed command can produce useful evidence before it exits. Credit
|
|
136
|
-
// raw captured bytes, never its elapsed header, changing ID or poll count.
|
|
137
|
-
const sha256 = event.data.output.sha256;
|
|
138
|
-
observations.add(sha256);
|
|
139
|
-
evidence.push({ kind: "observation", sha256, source });
|
|
140
|
-
progressed = true;
|
|
141
|
-
} else if ((name === "grep" || name === "find" || name === "ls" || name === "query") &&
|
|
142
|
-
observation !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 &&
|
|
143
|
-
!observations.has(observation)) {
|
|
144
|
-
observations.add(observation);
|
|
145
|
-
evidence.push({ kind: "observation", sha256: observation, source });
|
|
146
|
-
progressed = true;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return { evidence, progressed };
|
|
150
|
-
}
|
package/src/core/project.ts
DELETED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* projectContext: log -> what the model sees.
|
|
3
|
-
*
|
|
4
|
-
* Pure. The whole architecture rests on this being a function rather than a
|
|
5
|
-
* mutable array: the log is the truth, the context is one recomputable projection
|
|
6
|
-
* of it, so a bad projection is a bug you fix and re-run rather than information
|
|
7
|
-
* you destroyed.
|
|
8
|
-
*
|
|
9
|
-
* Two properties worth stating because tests pin them:
|
|
10
|
-
*
|
|
11
|
-
* IDENTITY. With no handoff, the projection is the identity mapping of harness
|
|
12
|
-
* events onto messages. Segment 0 therefore behaves exactly like a harness with
|
|
13
|
-
* no context management at all, which is what makes the design adoptable in
|
|
14
|
-
* stages -- nothing is paid for until a boundary actually fires.
|
|
15
|
-
*
|
|
16
|
-
* APPEND-ONLY WITHIN A SEGMENT. Appending an event only appends to the
|
|
17
|
-
* projection; it never rewrites an earlier message. That is what keeps the
|
|
18
|
-
* prompt prefix byte-stable, so every turn reads the whole accumulated prefix
|
|
19
|
-
* from cache and writes only the delta. In-place eviction would invalidate the
|
|
20
|
-
* prefix on every request and never amortise the write cost -- the failure mode
|
|
21
|
-
* Beyond Compaction (arXiv:2606.11213) documents for incremental eviction.
|
|
22
|
-
*
|
|
23
|
-
* The message shape below is structurally the Anthropic Messages shape, which is
|
|
24
|
-
* the target wire format. It is declared locally rather than imported so the core
|
|
25
|
-
* carries no SDK dependency; mapping to other providers belongs in an adapter,
|
|
26
|
-
* not here.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import type { LogEvent, AssistantBlock } from "./events.ts";
|
|
30
|
-
import { assertNever } from "./exhaustive.ts";
|
|
31
|
-
import type { ImageContent } from "./images.ts";
|
|
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;
|
|
37
|
-
|
|
38
|
-
export type ContentBlock =
|
|
39
|
-
| ImageContent
|
|
40
|
-
| { type: "text"; text: string; wire?: unknown }
|
|
41
|
-
/**
|
|
42
|
-
* The provider's own thinking block, replayed verbatim. It is carried as
|
|
43
|
-
* opaque rather than reconstructed because the API rejects modified blocks,
|
|
44
|
-
* and a reconstructed one would lose the signature the next request needs.
|
|
45
|
-
*/
|
|
46
|
-
| { type: "thinking"; raw: unknown }
|
|
47
|
-
| { type: "tool_use"; id: string; name: string; input: unknown; wire?: unknown }
|
|
48
|
-
| { type: "tool_result"; tool_use_id: string; content: string; is_error?: boolean };
|
|
49
|
-
|
|
50
|
-
export interface Message {
|
|
51
|
-
role: "user" | "assistant";
|
|
52
|
-
content: ContentBlock[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* System prompt and tool definitions. Must be byte-identical across every segment
|
|
57
|
-
* (decision D6): they render at the front of the prefix, so any change there
|
|
58
|
-
* invalidates the cache for the entire conversation. The non-obvious consequence
|
|
59
|
-
* is that project context files must NOT be re-read into `system` at a boundary --
|
|
60
|
-
* they belong in the handoff instead.
|
|
61
|
-
*/
|
|
62
|
-
export interface FrozenPrefix {
|
|
63
|
-
system: string;
|
|
64
|
-
tools: readonly unknown[];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface ProjectInput {
|
|
68
|
-
/** Diagnostic only; trace never enters provider messages. */
|
|
69
|
-
trace?: boolean;
|
|
70
|
-
frozen: FrozenPrefix;
|
|
71
|
-
/** Previous segment's handoff, or null for segment 0. */
|
|
72
|
-
handoff: Handoff | null;
|
|
73
|
-
events: readonly LogEvent[];
|
|
74
|
-
/** Source locations and versioned recovery guidance; workspace search does not index the log. */
|
|
75
|
-
logHint?: { manifestPath: string; segmentPath: string };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface ProjectResult {
|
|
79
|
-
provenance?: { version: number; messages: string[][]; omitted: string[] };
|
|
80
|
-
system: string;
|
|
81
|
-
tools: readonly unknown[];
|
|
82
|
-
messages: Message[];
|
|
83
|
-
/**
|
|
84
|
-
* Pairing violations. A tool_use with no matching tool_result is a 400 from the
|
|
85
|
-
* API, so it is reported here -- at the pure boundary, where it is debuggable --
|
|
86
|
-
* rather than being discovered as a request failure.
|
|
87
|
-
*/
|
|
88
|
-
violations: ProjectViolation[];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface ProjectViolation {
|
|
92
|
-
code: "UNPAIRED_TOOL_USE" | "ORPHAN_TOOL_RESULT" | "EMPTY_PROJECTION" | "DUPLICATE_TOOL_USE" | "EMPTY_TOOL_USE_ID";
|
|
93
|
-
detail: string;
|
|
94
|
-
}
|
|
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
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The only event types that replay as messages.
|
|
119
|
-
*
|
|
120
|
-
* Model-annotated events (claim, decision, pitfall, ...) are deliberately absent.
|
|
121
|
-
* They are a sidecar index over tool calls the model already made, so the
|
|
122
|
-
* tool_use/tool_result pair is what replays; including the annotation too would
|
|
123
|
-
* show the model its own notes twice. Within a segment the model sees its raw tool
|
|
124
|
-
* calls; across a boundary it sees them distilled into the handoff.
|
|
125
|
-
*
|
|
126
|
-
* `cut` is absent for a different reason: it is a boundary marker about the log,
|
|
127
|
-
* not a thing that happened in the conversation.
|
|
128
|
-
*
|
|
129
|
-
* `rewind` is here conditionally, and it is the one event whose replayability
|
|
130
|
-
* is a property of the event rather than of its type. After a "both" rewind
|
|
131
|
-
* the context and the disk agree, so saying anything would carry the undone
|
|
132
|
-
* work back in another form. After a partial one they disagree, and a model
|
|
133
|
-
* planning against the wrong picture is the whole failure mode -- so it is
|
|
134
|
-
* told, with paths.
|
|
135
|
-
*
|
|
136
|
-
* `interrupt` is a one-line harness note after the cancelled user message,
|
|
137
|
-
* not a new role: the model has to tell "asked and withdrew" from "asked
|
|
138
|
-
* two things". It is a following message rather than a mutation of the user
|
|
139
|
-
* turn so appending the event only appends to the projection (P7).
|
|
140
|
-
*/
|
|
141
|
-
type ReplayEvent = Extract<LogEvent, { t: "acceptance_contract" | "acceptance_decision" | "user" | "user_bash" | "guidance" | "interrupt" | "assistant" | "tool_result" | "rewind" | "extension" }>;
|
|
142
|
-
|
|
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;
|
|
146
|
-
if (e.t === "extension") return e.inContext;
|
|
147
|
-
if (e.t === "user_bash") return !e.excludeFromContext;
|
|
148
|
-
if (e.t === "rewind") return e.mode !== "both";
|
|
149
|
-
return e.t === "user" || e.t === "guidance" || e.t === "interrupt" || e.t === "assistant" || e.t === "tool_result";
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* The one thing a divergence message must produce.
|
|
154
|
-
*
|
|
155
|
-
* Named because it is the contract, not decoration: it is the automated form
|
|
156
|
-
* of the remedy Claude Code leaves to the person to remember after a partial
|
|
157
|
-
* restore, and a test asserting the message happened has to assert something
|
|
158
|
-
* only the message can say.
|
|
159
|
-
*/
|
|
160
|
-
export const REREAD_INSTRUCTION =
|
|
161
|
-
"Re-read any file before editing it. Do not rely on what earlier messages say its contents are.";
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* What an interrupt has to tell the model, as one line.
|
|
165
|
-
*
|
|
166
|
-
* A cancelled generation was never delivered, so the projection must not
|
|
167
|
-
* invent assistant text; the note is the whole of what the harness observed.
|
|
168
|
-
* Same role as the user turn so we do not mint a role the wire format lacks.
|
|
169
|
-
*/
|
|
170
|
-
export const INTERRUPT_NOTE =
|
|
171
|
-
"The user cancelled this turn before a reply was delivered.";
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* What a partial rewind has to tell the model, as plain prose.
|
|
175
|
-
*
|
|
176
|
-
* Prose rather than a structured block because it is addressed to the model,
|
|
177
|
-
* and the one thing it must produce is a re-read before an edit. That
|
|
178
|
-
* instruction is Claude Code's documented remedy for the same desync, left
|
|
179
|
-
* there to the person to remember; the paths make it actionable.
|
|
180
|
-
*/
|
|
181
|
-
export function renderRewindMessage(e: Extract<LogEvent, { t: "rewind" }>): Message {
|
|
182
|
-
const where = e.prompt ? `the point where you were asked: ${e.prompt}` : "an earlier point";
|
|
183
|
-
let lines: string[];
|
|
184
|
-
switch (e.mode) {
|
|
185
|
-
case "files":
|
|
186
|
-
lines = [
|
|
187
|
-
`The workspace was moved back to ${where}.`,
|
|
188
|
-
"",
|
|
189
|
-
"Your messages above still describe edits that are no longer on disk.",
|
|
190
|
-
"The conversation was not moved; only the files were.",
|
|
191
|
-
];
|
|
192
|
-
break;
|
|
193
|
-
case "context":
|
|
194
|
-
lines = [
|
|
195
|
-
`This conversation was moved back to ${where}.`,
|
|
196
|
-
"",
|
|
197
|
-
"The workspace was NOT moved. Files exist that nothing above accounts",
|
|
198
|
-
"for, because the turns that made them are no longer in this context.",
|
|
199
|
-
];
|
|
200
|
-
break;
|
|
201
|
-
case "both":
|
|
202
|
-
// Unreachable through the projection, which filters "both" out before
|
|
203
|
-
// getting here. Spelled out rather than folded into the other branch:
|
|
204
|
-
// falling through would have produced a message telling the model the
|
|
205
|
-
// two had diverged when the entire point of "both" is that they did
|
|
206
|
-
// not, and nothing would have failed.
|
|
207
|
-
throw new Error("a \"both\" rewind has nothing to tell the model: context and disk agree");
|
|
208
|
-
default:
|
|
209
|
-
return assertNever(e.mode, "renderRewindMessage");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (e.divergence.length > 0) {
|
|
213
|
-
lines.push("", "Where they disagree:");
|
|
214
|
-
for (const path of e.divergence) lines.push(` ${path}`);
|
|
215
|
-
}
|
|
216
|
-
lines.push("", REREAD_INSTRUCTION);
|
|
217
|
-
|
|
218
|
-
return { role: "user", content: [{ type: "text", text: lines.join("\n") }] };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Render the handoff as the opening user message of a new segment.
|
|
223
|
-
*
|
|
224
|
-
* It is a user message rather than part of `system` for two reasons: `system` has
|
|
225
|
-
* to stay byte-stable (D6), and a handoff is untrusted-ish derived content, not an
|
|
226
|
-
* operator instruction -- it does not belong in the operator channel.
|
|
227
|
-
*/
|
|
228
|
-
export function renderHandoffMessage(
|
|
229
|
-
handoff: Handoff,
|
|
230
|
-
logHint?: { manifestPath: string; segmentPath: string },
|
|
231
|
-
): Message {
|
|
232
|
-
const yaml = emitHandoff(handoff);
|
|
233
|
-
|
|
234
|
-
const parts = [
|
|
235
|
-
"You are continuing earlier work in a new context window.",
|
|
236
|
-
"",
|
|
237
|
-
"The state below was derived mechanically from the session log -- it is not a",
|
|
238
|
-
"summary and nothing was paraphrased. Fields that are absent were absent from",
|
|
239
|
-
"the log, not judged unimportant.",
|
|
240
|
-
"",
|
|
241
|
-
"```yaml",
|
|
242
|
-
yaml.trimEnd(),
|
|
243
|
-
"```",
|
|
244
|
-
];
|
|
245
|
-
|
|
246
|
-
if (logHint) {
|
|
247
|
-
parts.push(
|
|
248
|
-
"",
|
|
249
|
-
"The full log is on disk and nothing has been deleted from it. When you need a",
|
|
250
|
-
"detail this state does not carry, read it rather than guessing:",
|
|
251
|
-
` - segment index: ${logHint.manifestPath}`,
|
|
252
|
-
` - previous segment: ${logHint.segmentPath}`,
|
|
253
|
-
"Each line is one JSON event. Recover originals using the declared tools' contracts; if evidence is inaccessible, state the gap instead of guessing.",
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const unverified = handoff.state.unverified_claims.length;
|
|
258
|
-
if (unverified > 0) {
|
|
259
|
-
parts.push(
|
|
260
|
-
"",
|
|
261
|
-
`${unverified} earlier completion ${unverified === 1 ? "claim was" : "claims were"} not matched`,
|
|
262
|
-
"to a passing command in the log. They are listed under state.unverified_claims",
|
|
263
|
-
"and must be treated as NOT done.",
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (handoff.file_anchors.length > 0) {
|
|
268
|
-
parts.push(
|
|
269
|
-
"",
|
|
270
|
-
"file_anchors records the digest each file had when it was last touched. Re-read",
|
|
271
|
-
"a file before relying on it; if the digest differs, the environment changed",
|
|
272
|
-
"underneath this state.",
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return { role: "user", content: [{ type: "text", text: parts.join("\n") }] };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function projectContext(input: ProjectInput): ProjectResult {
|
|
280
|
-
const violations: ProjectViolation[] = [];
|
|
281
|
-
const messages: Message[] = [];
|
|
282
|
-
const sources: string[][] = [];
|
|
283
|
-
const append = (message: Message, ids: string[]) => { messages.push(message); sources.push(ids); };
|
|
284
|
-
|
|
285
|
-
if (input.handoff) {
|
|
286
|
-
append(renderHandoffMessage(input.handoff, input.logHint), [`handoff:${input.handoff.predecessor?.node ?? "root"}`]);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const events = input.events.filter(replayable);
|
|
290
|
-
|
|
291
|
-
// Tool results are buffered so that every result for one assistant turn lands in
|
|
292
|
-
// a SINGLE user message. Splitting them across messages teaches the model to stop
|
|
293
|
-
// issuing parallel calls, which is a silent, permanent capability regression.
|
|
294
|
-
let pending: ContentBlock[] = [];
|
|
295
|
-
let pendingSources: string[] = [];
|
|
296
|
-
const flushPending = () => {
|
|
297
|
-
if (pending.length > 0) {
|
|
298
|
-
append({ role: "user", content: pending }, pendingSources);
|
|
299
|
-
pending = [];
|
|
300
|
-
pendingSources = [];
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
/** tool_use ids emitted by the most recent assistant turn and not yet answered. */
|
|
305
|
-
let awaiting = new Set<string>();
|
|
306
|
-
|
|
307
|
-
for (const e of events) {
|
|
308
|
-
if (e.t === "acceptance_contract" || e.t === "acceptance_decision") {
|
|
309
|
-
flushPending();
|
|
310
|
-
if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `acceptance event ${e.id} follows unanswered tools` });
|
|
311
|
-
append({ role: "user", content: [{ type: "text", text: e.t === "acceptance_contract"
|
|
312
|
-
? `Declared acceptance contract (execution success alone does not satisfy it): ${JSON.stringify(e.contract)}`
|
|
313
|
-
: `User accepted criterion ${e.criterion} of contract ${e.contract_id} for request ${e.request}.` +
|
|
314
|
-
(e.target_binding ? ` Bound to runtime instance ${e.target_binding}, observation ${e.target_observation}. Later runtime changes invalidate this acceptance.` : "") }] }, [e.id]);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (e.t === "extension") {
|
|
318
|
-
flushPending();
|
|
319
|
-
if (awaiting.size) violations.push({ code: "UNPAIRED_TOOL_USE", detail: `custom message ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}` });
|
|
320
|
-
append({ role: "user", content: [{ type: "text", text: e.content }] }, [e.id]);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (e.t === "user_bash") {
|
|
324
|
-
flushPending();
|
|
325
|
-
append({ role: "user", content: [{ type: "text", text: `User ran: ${e.command}\n${e.content}` }] }, [e.id]);
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (e.t === "rewind") {
|
|
329
|
-
// Flushed first for the same reason a user turn is: it opens a user
|
|
330
|
-
// message, and a pending tool_result batch has to close before it.
|
|
331
|
-
flushPending();
|
|
332
|
-
append(renderRewindMessage(e), [e.id]);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (e.t === "user" || e.t === "guidance") {
|
|
336
|
-
flushPending();
|
|
337
|
-
if (awaiting.size > 0) {
|
|
338
|
-
violations.push({
|
|
339
|
-
code: "UNPAIRED_TOOL_USE",
|
|
340
|
-
detail: `user turn ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}`,
|
|
341
|
-
});
|
|
342
|
-
awaiting = new Set();
|
|
343
|
-
}
|
|
344
|
-
append({ role: "user", content: [...(e.text || e.t === "guidance" || !e.images?.length ? [{ type: "text" as const, text: e.text }] : []),
|
|
345
|
-
...(e.t === "user" ? (e.images ?? []).map(image => ({ type: "image" as const, image })) : [])] }, [e.id]);
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (e.t === "interrupt") {
|
|
349
|
-
// Own message so the preceding user turn stays byte-stable (P7). A
|
|
350
|
-
// look-ahead mutation of that turn would rewrite it once a later
|
|
351
|
-
// event (tool_use, another user) was already projected after it.
|
|
352
|
-
flushPending();
|
|
353
|
-
append({ role: "user", content: [{ type: "text", text: INTERRUPT_NOTE }] }, [e.id]);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (e.t === "assistant") {
|
|
358
|
-
flushPending();
|
|
359
|
-
violations.push(...toolCallIdentityViolations(e.blocks).map(violation => ({
|
|
360
|
-
...violation, detail: `assistant turn ${e.id}: ${violation.detail}`,
|
|
361
|
-
})));
|
|
362
|
-
if (awaiting.size > 0) {
|
|
363
|
-
violations.push({
|
|
364
|
-
code: "UNPAIRED_TOOL_USE",
|
|
365
|
-
detail: `assistant turn ${e.id} follows unanswered tool_use ${[...awaiting].join(", ")}`,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
append({ role: "assistant", content: e.blocks.map(toContentBlock) }, [e.id]);
|
|
369
|
-
awaiting = new Set(
|
|
370
|
-
e.blocks.filter((b) => b.type === "tool_use").map((b) => (b as { call_id: string }).call_id),
|
|
371
|
-
);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// tool_result
|
|
376
|
-
if (!awaiting.has(e.call_id)) {
|
|
377
|
-
violations.push({
|
|
378
|
-
code: "ORPHAN_TOOL_RESULT",
|
|
379
|
-
detail: `tool_result ${e.id} answers unknown call ${e.call_id}`,
|
|
380
|
-
});
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
awaiting.delete(e.call_id);
|
|
384
|
-
pendingSources.push(e.id);
|
|
385
|
-
pending.push({
|
|
386
|
-
type: "tool_result",
|
|
387
|
-
tool_use_id: e.call_id,
|
|
388
|
-
content: diagnosticContent(e.content, e.diagnostic) + (e.warnings?.length ? "\nObserver warnings (operation result retained): " + JSON.stringify(e.warnings) : "") +
|
|
389
|
-
(e.instructions ? "\n\n" + e.instructions.text : ""),
|
|
390
|
-
...(e.ok ? {} : { is_error: true }),
|
|
391
|
-
});
|
|
392
|
-
// Images follow the paired text result, so APIs without image tool outputs receive a user image.
|
|
393
|
-
pending.push(...(e.images ?? []).map(image => ({ type: "image" as const, image })));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
flushPending();
|
|
397
|
-
|
|
398
|
-
if (awaiting.size > 0) {
|
|
399
|
-
violations.push({
|
|
400
|
-
code: "UNPAIRED_TOOL_USE",
|
|
401
|
-
detail: `segment ends with unanswered tool_use ${[...awaiting].join(", ")}`,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (messages.length === 0) {
|
|
406
|
-
violations.push({ code: "EMPTY_PROJECTION", detail: "no replayable events" });
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
...(input.trace ? { provenance: { version: PROJECTION_VERSION, messages: sources,
|
|
411
|
-
omitted: input.events.filter(e => !sources.some(ids => ids.includes(e.id))).map(e => e.id) } } : {}),
|
|
412
|
-
system: input.frozen.system,
|
|
413
|
-
tools: input.frozen.tools,
|
|
414
|
-
messages,
|
|
415
|
-
violations,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function toContentBlock(b: AssistantBlock): ContentBlock {
|
|
420
|
-
switch (b.type) {
|
|
421
|
-
case "tool_use":
|
|
422
|
-
return { type: "tool_use", id: b.call_id, name: b.name, input: b.input, ...(b.wire === undefined ? {} : { wire: b.wire }) };
|
|
423
|
-
case "thinking":
|
|
424
|
-
// Only the raw block crosses back to the API; `text` exists for display.
|
|
425
|
-
return { type: "thinking", raw: b.raw };
|
|
426
|
-
case "text":
|
|
427
|
-
return b;
|
|
428
|
-
default:
|
|
429
|
-
return assertNever(b, "toContentBlock");
|
|
430
|
-
}
|
|
431
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
/**
|
|
27
|
-
* `usersInline`: the carried `user_turns` hold every user message verbatim.
|
|
28
|
-
* Telling the model to page them anyway costs a guaranteed first-call read
|
|
29
|
-
* that returns what it already has -- or an empty page after a model switch
|
|
30
|
-
* on a fresh session (Horizyn mv3nsske-x50u, fix-recovery-inline-users).
|
|
31
|
-
*/
|
|
32
|
-
export function recoveryInstructions(tools: readonly string[], usersInline = false, images = false): string {
|
|
33
|
-
return (tools.includes("history")
|
|
34
|
-
? (usersInline ? "User messages are verbatim in user_turns. Other history(node=source.node,recovery=...) pages: failures, environment, next_actions; " : "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; ") +
|
|
35
|
-
"event=ID reads one original. history(work_id=ID) recovers work. " +
|
|
36
|
-
// Working set v3: the turn's image references are carried, the pixels are not.
|
|
37
|
-
(images ? "A user turn's images are not attached here; history(event=ID) with its event reattaches them. " : "")
|
|
38
|
-
: "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. ") +
|
|
39
|
-
"Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting.";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Preserve the source locator even when the interface for retrieving it is unavailable. */
|
|
43
|
-
export function artifactRecovery(sha256: string, offset: number, tools: readonly string[] = ["history"]): string {
|
|
44
|
-
const source = `artifact=${sha256} offset=${offset}`;
|
|
45
|
-
return tools.includes("history") ? `history ${source}` : `${source}; recovery capability unavailable`;
|
|
46
|
-
}
|