@visual-z/xeno 0.7.2 → 0.8.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 +43 -0
- package/README.md +36 -9
- package/docs/foundations.md +1 -1
- package/package.json +3 -1
- package/src/apps/cli.ts +4 -3
- package/src/apps/commands.ts +8 -0
- package/src/apps/goal-flow.ts +70 -0
- package/src/apps/render.ts +0 -2
- package/src/apps/session-hub.ts +6 -5
- package/src/apps/session-lifecycle.ts +64 -0
- package/src/apps/shell.ts +89 -34
- package/src/apps/startup.ts +32 -31
- package/src/apps/ui/app.ts +38 -6
- package/src/apps/ui/control-model.ts +18 -1
- package/src/apps/ui/surface.ts +2 -0
- package/src/apps/update-check.ts +1 -1
- package/src/core/acceptance.ts +41 -9
- package/src/core/blind-checks.ts +72 -0
- package/src/core/branch-receipt.ts +3 -1
- package/src/core/checkpoints.ts +21 -0
- package/src/core/churn.ts +2 -2
- package/src/core/completion-review.ts +18 -11
- package/src/core/continuation.ts +11 -10
- package/src/core/control-view.ts +5 -8
- package/src/core/deadline.ts +2 -1
- package/src/core/derive.ts +0 -7
- package/src/core/environment.ts +2 -2
- package/src/core/event-validation.ts +19 -6
- package/src/core/events.ts +41 -32
- package/src/core/evidence-ladder.ts +62 -0
- package/src/core/evidence-links.ts +0 -10
- package/src/core/evidence.ts +1 -2
- package/src/core/export.ts +6 -11
- package/src/core/goal.ts +200 -0
- package/src/core/history.ts +1 -11
- package/src/core/json-event.ts +0 -6
- package/src/core/outcome.ts +0 -1
- package/src/core/program-status.ts +11 -2
- package/src/core/progress.ts +14 -8
- package/src/core/project.ts +6 -16
- package/src/core/read-bounds.ts +11 -2
- package/src/core/read-observation.ts +24 -0
- package/src/core/request-budget.ts +13 -3
- package/src/core/request-state.ts +178 -19
- package/src/core/response-recovery.ts +2 -1
- package/src/core/session-entry.ts +8 -3
- package/src/core/session-exit.ts +19 -3
- package/src/core/session-format.ts +169 -0
- package/src/core/settings.ts +7 -27
- package/src/core/stall.ts +30 -7
- package/src/core/stop-report.ts +4 -3
- package/src/core/transcript.ts +22 -2
- package/src/runtime/blind-check.ts +36 -0
- package/src/runtime/counter-check.ts +74 -0
- package/src/runtime/exec-sessions.ts +11 -0
- package/src/runtime/foundation.ts +46 -8
- package/src/runtime/loop.ts +239 -430
- package/src/runtime/oauth.ts +1 -3
- package/src/runtime/provider-request.ts +2 -1
- package/src/runtime/runtime-target.ts +2 -2
- package/src/runtime/session-control.ts +4 -4
- package/src/runtime/tools.ts +44 -42
- package/src/runtime/version.ts +12 -0
- package/src/runtime/workspace-lifecycle.ts +31 -0
- package/src/store/blind-checks.ts +32 -0
- package/src/store/checkpoint-history.ts +1 -12
- package/src/store/file-index.ts +88 -7
- package/src/store/log.ts +25 -27
- package/src/store/recovery.ts +1 -3
- package/src/store/replay.ts +2 -2
- package/src/store/restore.ts +10 -202
- package/src/store/search.ts +7 -5
- package/src/store/session-archive.ts +0 -1
- package/src/store/sessions.ts +31 -33
- package/src/store/workspace-clone.ts +42 -0
- package/src/store/workspace.ts +10 -157
- package/src/tui/ui/status.ts +6 -2
- package/src/core/benchmark.ts +0 -195
- package/src/core/evaluation-coverage.ts +0 -74
- package/src/core/experiment.ts +0 -173
- package/src/core/harness-comparison.ts +0 -90
- package/src/core/harness-diagnostics.ts +0 -206
- package/src/core/native-eval.ts +0 -25
- package/src/core/policy-evaluation.ts +0 -55
- package/src/core/query-eval-contract.ts +0 -14
- package/src/core/recall-analysis.ts +0 -90
- package/src/core/recall-trial.ts +0 -153
- package/src/core/recovery-corpus.ts +0 -17
- package/src/core/recovery-evaluation.ts +0 -60
- package/src/core/tool-mode-protocol.ts +0 -25
- package/src/core/trial-lifecycle.ts +0 -14
- package/src/core/window-trial.ts +0 -65
- package/src/index.ts +0 -10
- package/src/runtime/benchmark.ts +0 -187
- package/src/runtime/harness-comparison.ts +0 -376
- package/src/runtime/harness-eval.ts +0 -462
- package/src/runtime/native-eval-fixtures.ts +0 -33
- package/src/runtime/native-eval.ts +0 -146
- package/src/runtime/query-eval-verifier.ts +0 -28
- package/src/runtime/recall-trial.ts +0 -226
- package/src/runtime/recovery-coverage.ts +0 -71
- package/src/runtime/recovery-evidence.ts +0 -76
- package/src/runtime/recovery-fixtures.ts +0 -116
- package/src/runtime/tool-mode-eval.ts +0 -146
- package/src/runtime/trial-control.ts +0 -45
- package/src/runtime/window-trial.ts +0 -141
- package/src/store/git-branch.ts +0 -35
- package/src/tui/ui/testing.ts +0 -5
package/src/core/acceptance.ts
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
import type { LogEvent } from "./events.ts";
|
|
3
3
|
import type { Evidence } from "./handoff.ts";
|
|
4
4
|
import { evidenceStale } from "./evidence.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { workspaceChanged } from "./checkpoints.ts";
|
|
6
6
|
import { parseRuntimeTarget, type RuntimeTarget, type TargetAcceptance } from "./runtime-target.ts";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A command criterion either tests the change -- it fails before the work and
|
|
10
|
+
* passes after it (SWE-bench's FAIL_TO_PASS) -- or, with `guard`, protects
|
|
11
|
+
* what the work must not break: it passes before and after (PASS_TO_PASS),
|
|
12
|
+
* such as "the test file is unchanged" (S.7).
|
|
13
|
+
*/
|
|
8
14
|
export type AcceptanceCriterion = { id: string; description: string } &
|
|
9
|
-
({ kind: "command"; command: string } | { kind: "human"; target?: string });
|
|
15
|
+
({ kind: "command"; command: string; guard?: true } | { kind: "human"; target?: string });
|
|
10
16
|
export type AcceptanceContract = { version: 1; id: string; scope?: "request" | "changes"; criteria: AcceptanceCriterion[];
|
|
11
17
|
source?: { path: string; sha256: string }; targets?: RuntimeTarget[] };
|
|
12
18
|
export type AcceptanceState = {
|
|
@@ -15,6 +21,13 @@ export type AcceptanceState = {
|
|
|
15
21
|
targets?: TargetAcceptance[];
|
|
16
22
|
status: "needs_work" | "awaiting_human" | "verified" | "not_applicable";
|
|
17
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* The longest check command a contract accepts. Drafted checks are often one
|
|
26
|
+
* inline script (`python3 -c '...'`); a real 599-character structure check
|
|
27
|
+
* was refused at the earlier 500 (fix-goal-read-only-draft). Bounded so a
|
|
28
|
+
* frozen contract stays small.
|
|
29
|
+
*/
|
|
30
|
+
export const ACCEPTANCE_COMMAND_LIMIT = 2000;
|
|
18
31
|
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
32
|
const label = (value: unknown, max: number) => typeof value === "string" && value.trim().length > 0 && value.length <= max;
|
|
20
33
|
|
|
@@ -27,10 +40,12 @@ export function parseAcceptanceContract(value: unknown): AcceptanceContract {
|
|
|
27
40
|
if (source !== undefined && (!record(source) || !label(source["path"], 4096) || typeof source["sha256"] !== "string" || !/^[a-f0-9]{64}$/.test(source["sha256"]))) throw new Error("invalid acceptance source");
|
|
28
41
|
const criteria: AcceptanceCriterion[] = value["criteria"].map((item: unknown) => {
|
|
29
42
|
if (!record(item) || !label(item["id"], 80) || !label(item["description"], 300)) throw new Error("invalid acceptance criterion");
|
|
30
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(String(item["id"])) || Object.keys(item).some(key => !["id", "description", "kind", "command", "target"].includes(key))) throw new Error("unsupported acceptance criterion field or ID");
|
|
43
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(String(item["id"])) || Object.keys(item).some(key => !["id", "description", "kind", "command", "target", "guard"].includes(key))) throw new Error("unsupported acceptance criterion field or ID");
|
|
31
44
|
const fields = { id: String(item["id"]), description: String(item["description"]) };
|
|
32
|
-
if (item["kind"] === "human" && item["command"] === undefined && (item["target"] === undefined || label(item["target"], 80))) return { ...fields, kind: "human", ...(item["target"] ? { target: String(item["target"]) } : {}) };
|
|
33
|
-
if (item["kind"] === "command" && item["target"] === undefined && label(item["command"],
|
|
45
|
+
if (item["kind"] === "human" && item["command"] === undefined && item["guard"] === undefined && (item["target"] === undefined || label(item["target"], 80))) return { ...fields, kind: "human", ...(item["target"] ? { target: String(item["target"]) } : {}) };
|
|
46
|
+
if (item["kind"] === "command" && item["target"] === undefined && label(item["command"], ACCEPTANCE_COMMAND_LIMIT) && (item["guard"] === undefined || item["guard"] === true)) {
|
|
47
|
+
return { ...fields, kind: "command", command: String(item["command"]), ...(item["guard"] === true ? { guard: true as const } : {}) };
|
|
48
|
+
}
|
|
34
49
|
throw new Error("acceptance criterion must declare a command or human decision");
|
|
35
50
|
});
|
|
36
51
|
if (new Set(criteria.map(item => item.id)).size !== criteria.length) throw new Error("duplicate acceptance criterion IDs");
|
|
@@ -46,6 +61,26 @@ export function parseAcceptanceContract(value: unknown): AcceptanceContract {
|
|
|
46
61
|
...(value["scope"] ? { scope: value["scope"] as "request" | "changes" } : {}) };
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
/**
|
|
65
|
+
* What one counter-check observation means (S.1, S.7): the check run once more
|
|
66
|
+
* in a clone with the request's changes undone. A check that tests the change
|
|
67
|
+
* must fail there; a guard must still pass there. The log records only the
|
|
68
|
+
* exit code and the role it ran under, so every view derives the verdict here
|
|
69
|
+
* and none can disagree with another.
|
|
70
|
+
*
|
|
71
|
+
* tests the change fails there: discriminating passes there: vacuous
|
|
72
|
+
* guard passes there: held fails there: not_held
|
|
73
|
+
*
|
|
74
|
+
* A guard that did not hold is not a defect in the evidence: the check does
|
|
75
|
+
* test the change, only its declaration was wrong.
|
|
76
|
+
*/
|
|
77
|
+
export type CounterVerdict = "discriminating" | "vacuous" | "held" | "not_held" | "unavailable";
|
|
78
|
+
export function counterVerdict(observation: { guard?: true } & ({ exit: number } | { reason: string })): CounterVerdict {
|
|
79
|
+
if (!("exit" in observation)) return "unavailable";
|
|
80
|
+
if (observation.guard) return observation.exit === 0 ? "held" : "not_held";
|
|
81
|
+
return observation.exit === 0 ? "vacuous" : "discriminating";
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
export function deriveAcceptance(previous: AcceptanceState | undefined, events: readonly LogEvent[]): AcceptanceState | undefined {
|
|
50
85
|
let state = previous ? { ...previous, criteria: previous.criteria.map(item => ({ ...item })), ...(previous.targets ? { targets: previous.targets.map(target => ({ ...target })) } : {}) } : undefined;
|
|
51
86
|
for (const event of events) {
|
|
@@ -56,10 +91,7 @@ export function deriveAcceptance(previous: AcceptanceState | undefined, events:
|
|
|
56
91
|
continue;
|
|
57
92
|
}
|
|
58
93
|
if (!state) continue;
|
|
59
|
-
const mutation = event
|
|
60
|
-
event.t === "snapshot" && Object.keys(event.added).length + Object.keys(event.changed).length + event.removed.length > 0 ||
|
|
61
|
-
event.t === "checkpoint" && event.phase === "applied" && changedPaths(event.changes).length > 0 ||
|
|
62
|
-
event.t === "workspace_observation" && event.changed;
|
|
94
|
+
const mutation = workspaceChanged(event);
|
|
63
95
|
if (mutation) { state.changed = true; for (const target of state.targets ?? []) target.status = "stale"; }
|
|
64
96
|
if (event.t === "target_observation" && event.observation.request === state.request && event.observation.contract_event === state.contract_event) {
|
|
65
97
|
const target = state.targets?.find(item => item.id === event.observation.target), receipt = event.observation;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind checks: the independent rung of the evidence ladder (S.1).
|
|
3
|
+
*
|
|
4
|
+
* A check the model can see is a check the model can work toward. Blind
|
|
5
|
+
* checks live outside the workspace, never enter a projection, and run only
|
|
6
|
+
* after the goal ends, against a clone of the final result. Because the log
|
|
7
|
+
* is complete, whether the model ever touched them is a mechanical question
|
|
8
|
+
* over its own tool inputs and read receipts -- not a promise.
|
|
9
|
+
*/
|
|
10
|
+
import type { LogEvent } from "./events.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Assumed, not measured: a hidden test suite of a handful of cases plus their
|
|
14
|
+
* fixtures. Bounded so freezing a blind directory into the session context
|
|
15
|
+
* cannot grow without limit.
|
|
16
|
+
*/
|
|
17
|
+
export const BLIND_CHECK_LIMIT = 8;
|
|
18
|
+
export const BLIND_FILE_LIMIT = 64;
|
|
19
|
+
export const BLIND_BYTES_LIMIT = 8 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
export type BlindCheck = { id: string; description: string; command: string };
|
|
22
|
+
export type BlindManifest = { version: 1; checks: BlindCheck[] };
|
|
23
|
+
/**
|
|
24
|
+
* What a session freezes: where the checks live (the real path, plus every
|
|
25
|
+
* spelling of it the user gave, since a symlinked temp directory is a
|
|
26
|
+
* different string), the manifest, and every file's digest.
|
|
27
|
+
*/
|
|
28
|
+
export type BlindChecks = { dir: string; aliases: readonly string[]; manifest: BlindManifest; files: Record<string, string> };
|
|
29
|
+
|
|
30
|
+
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
const label = (value: unknown, max: number): value is string => typeof value === "string" && value.trim().length > 0 && value.length <= max;
|
|
32
|
+
|
|
33
|
+
export function parseBlindManifest(value: unknown): BlindManifest {
|
|
34
|
+
if (!record(value) || value["version"] !== 1 || Object.keys(value).some(key => key !== "version" && key !== "checks")) throw Error("blind.json must be {version: 1, checks: [...]}");
|
|
35
|
+
const checks = value["checks"];
|
|
36
|
+
if (!Array.isArray(checks) || checks.length < 1 || checks.length > BLIND_CHECK_LIMIT) throw Error(`blind.json needs 1..${BLIND_CHECK_LIMIT} checks`);
|
|
37
|
+
const parsed = checks.map((check: unknown, index): BlindCheck => {
|
|
38
|
+
if (!record(check) || Object.keys(check).some(key => !["id", "description", "command"].includes(key))) throw Error(`blind check ${index + 1}: only id, description and command are allowed`);
|
|
39
|
+
const { id, description, command } = check;
|
|
40
|
+
if (!label(id, 80) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/.test(id)) throw Error(`blind check ${index + 1}: invalid id`);
|
|
41
|
+
if (!label(description, 300) || !label(command, 500)) throw Error(`blind check ${index + 1}: description and command are required`);
|
|
42
|
+
return { id, description, command };
|
|
43
|
+
});
|
|
44
|
+
if (new Set(parsed.map(check => check.id)).size !== parsed.length) throw Error("blind.json has duplicate check ids");
|
|
45
|
+
return { version: 1, checks: parsed };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isBlindChecks(value: unknown): value is BlindChecks {
|
|
49
|
+
if (!record(value) || typeof value["dir"] !== "string" || !record(value["files"]) ||
|
|
50
|
+
!Array.isArray(value["aliases"]) || !value["aliases"].every(alias => typeof alias === "string" && alias.length > 0)) return false;
|
|
51
|
+
try { parseBlindManifest(value["manifest"]); } catch { return false; }
|
|
52
|
+
return Object.values(value["files"]).every(sha => typeof sha === "string" && /^[a-f0-9]{64}$/.test(sha));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the model could have seen the blind checks: a tool input naming the
|
|
57
|
+
* blind directory, or a read whose content digest matches a blind file. This
|
|
58
|
+
* covers what the harness records for every tool; output a command produced
|
|
59
|
+
* without naming the path (a recursive search, say) is not matched, and the
|
|
60
|
+
* report says only what was found.
|
|
61
|
+
*/
|
|
62
|
+
export function blindExposure(events: readonly LogEvent[], blind: BlindChecks): string | null {
|
|
63
|
+
const digests = new Set(Object.values(blind.files)), paths = [blind.dir, ...blind.aliases];
|
|
64
|
+
for (const event of events) {
|
|
65
|
+
if (event.t === "assistant") for (const block of event.blocks) {
|
|
66
|
+
const input = block.type === "tool_use" ? JSON.stringify(block.input) : "";
|
|
67
|
+
if (block.type === "tool_use" && paths.some(path => input.includes(path))) return `tool call ${block.call_id} names the blind check directory`;
|
|
68
|
+
}
|
|
69
|
+
if (event.t === "tool_result" && event.effect?.kind === "read" && digests.has(event.effect.sha256)) return `tool result ${event.id} read a blind check file`;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { LogEvent, NodeRef } from "./events.ts";
|
|
3
3
|
import type { Handoff } from "./handoff.ts";
|
|
4
4
|
import type { ExecutionOutcome } from "./outcome.ts";
|
|
5
|
+
import { deriveRequestState } from "./request-state.ts";
|
|
5
6
|
|
|
6
7
|
export function deriveBranchReceipt(input: {
|
|
7
8
|
source: NodeRef; outcome: ExecutionOutcome; handoff: Handoff;
|
|
@@ -16,7 +17,8 @@ export function deriveBranchReceipt(input: {
|
|
|
16
17
|
const unfinished = (input.handoff.state.work_items ?? []).filter(item => item.status === "active" || item.status === "blocked")
|
|
17
18
|
.map(item => ({ work_id: item.id, status: item.status, next_action: item.next_action ?? null }));
|
|
18
19
|
return { version: 1, source: input.source, execution: input.outcome,
|
|
19
|
-
|
|
20
|
+
// S.1: the child's request ledger, the same verdict any request view shows.
|
|
21
|
+
verification: deriveRequestState(input.nodes)?.status ?? "not_applicable",
|
|
20
22
|
task_acceptance: "not_assessed", commands, changes, observations, unfinished,
|
|
21
23
|
...(input.handoff.processes?.length ? { processes: input.handoff.processes } : {}),
|
|
22
24
|
unresolved_branches: input.handoff.branches.filter(b => b.resolution === null).map(b => b.node),
|
package/src/core/checkpoints.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface WorkspaceObservationEvent {
|
|
|
22
22
|
scope: "source-v1";
|
|
23
23
|
changed: boolean;
|
|
24
24
|
reason?: string;
|
|
25
|
+
/** The workspace as a goal request began: the state a counter-check must revert to exactly. */
|
|
26
|
+
baseline?: true;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function sameVersion(a: FileVersion, b: FileVersion): boolean {
|
|
@@ -62,3 +64,22 @@ export function validFileChanges(value: unknown): value is FileChanges {
|
|
|
62
64
|
return Object.entries(value).every(([path, change]: [string, unknown]) => path.length > 0 && typeof change === "object" && change !== null &&
|
|
63
65
|
version((change as Record<string, unknown>)["before"]) && version((change as Record<string, unknown>)["after"]));
|
|
64
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The one test for "this event changed the workspace": a real edit, an
|
|
70
|
+
* applied checkpoint that moved a file, or an observation that saw a change.
|
|
71
|
+
* Acceptance, request evidence and environment validity all ask exactly this
|
|
72
|
+
* question and must not answer it three different ways (S.1).
|
|
73
|
+
*/
|
|
74
|
+
export function workspaceChanged(event: import("./events.ts").LogEvent): boolean {
|
|
75
|
+
return event.t === "tool_result" && event.effect?.kind === "edit" && event.effect.sha256_before !== event.effect.sha256_after ||
|
|
76
|
+
event.t === "checkpoint" && event.phase === "applied" && changedPaths(event.changes).length > 0 ||
|
|
77
|
+
event.t === "workspace_observation" && event.changed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The transition back: every path's before and after swapped. Applying it undoes `changes`. */
|
|
81
|
+
export function invertFileChanges(changes: FileChanges): FileChanges {
|
|
82
|
+
const inverted: FileChanges = Object.create(null) as FileChanges;
|
|
83
|
+
for (const [path, change] of Object.entries(changes)) inverted[path] = { before: change.after, after: change.before };
|
|
84
|
+
return inverted;
|
|
85
|
+
}
|
package/src/core/churn.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* unchanged.
|
|
15
15
|
*/
|
|
16
16
|
import type { GuidanceEvent, LogEvent } from "./events.ts";
|
|
17
|
+
import { currentRequestEvents } from "./request-state.ts";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Assumed, not measured. Three failures leaves room for "change, retry,
|
|
@@ -25,8 +26,7 @@ export const CHURN_FAILURES = 3;
|
|
|
25
26
|
export const CHURN_EDITS = 8;
|
|
26
27
|
|
|
27
28
|
export function churnReview(events: readonly LogEvent[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
|
|
28
|
-
const
|
|
29
|
-
const current = events.slice(Math.max(0, start));
|
|
29
|
+
const current = currentRequestEvents(events);
|
|
30
30
|
if (current.some((event) => event.t === "guidance" && event.reason === "churn")) return null;
|
|
31
31
|
const failures = new Map<string, { count: number; last: string }>();
|
|
32
32
|
const edits = new Map<string, { count: number; last: string }>();
|
|
@@ -37,9 +37,9 @@ export function looksLikeHandBack(text: string): boolean {
|
|
|
37
37
|
export function completionReview(
|
|
38
38
|
events: readonly LogEvent[],
|
|
39
39
|
remainingRequests: number,
|
|
40
|
-
options?: { strictVerification?: boolean; acceptance?: AcceptanceState
|
|
40
|
+
options?: { strictVerification?: boolean; acceptance?: AcceptanceState },
|
|
41
41
|
): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
|
|
42
|
-
const state = deriveRequestState([{ node: 0, events }]);
|
|
42
|
+
const state = deriveRequestState([{ node: 0, events }], { strict: options?.strictVerification === true });
|
|
43
43
|
const decision = state ? requestCompletion(state, remainingRequests, options) : { action: "finish" as const };
|
|
44
44
|
return decision.action === "continue" ? decision.guidance : null;
|
|
45
45
|
}
|
|
@@ -47,29 +47,36 @@ export function completionReview(
|
|
|
47
47
|
export type RequestCompletion = { action: "finish" } |
|
|
48
48
|
{ action: "continue"; guidance: Pick<GuidanceEvent, "reason" | "text" | "sources"> };
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Q23 / S.1: loop termination reads the same ledger as every request view.
|
|
52
|
+
* A reported blocker does not end the check early: the model's statement is
|
|
53
|
+
* not evidence (D11), and the check still runs only once, so an honest
|
|
54
|
+
* blocker restated at the second finish is respected.
|
|
55
|
+
*/
|
|
51
56
|
export function requestCompletion(state: RequestState, remainingRequests: number,
|
|
52
|
-
options?: {
|
|
57
|
+
options?: { acceptance?: AcceptanceState }): RequestCompletion {
|
|
53
58
|
const finish = { action: "finish" as const };
|
|
54
|
-
if (remainingRequests < 1 || state.reviewed
|
|
59
|
+
if (remainingRequests < 1 || state.reviewed) return finish;
|
|
55
60
|
const final = state.final;
|
|
56
61
|
if (!final || final.has_tools || state.execution?.reason !== "model_end" && state.execution?.reason !== "unknown") return finish;
|
|
57
62
|
const text = final.text;
|
|
58
|
-
const verifiedAfter = state.commands.some(command => command.status === "pass" && (!options?.strictVerification || command.gate !== undefined));
|
|
59
63
|
|
|
60
64
|
const gaps: string[] = [];
|
|
61
65
|
const acceptance = options?.acceptance ?? state.acceptance;
|
|
62
66
|
const missingCommands = acceptance?.status === "needs_work" ? acceptance.criteria.flatMap(item =>
|
|
63
67
|
item.kind === "command" && item.status !== "pass" ? [{ id: item.id, command: item.command, status: item.status }] : []) : [];
|
|
68
|
+
const freshCheck = acceptance ? undefined : state.criteria.find(item => item.kind === "fresh_check");
|
|
64
69
|
if (state.used_tools && text.length === 0) gaps.push("the final reply is empty");
|
|
65
|
-
|
|
70
|
+
// Strict verification counts only configured gates (O.11); say so, since a successful `cat` would otherwise look like a check.
|
|
71
|
+
if (freshCheck && freshCheck.status !== "pass") gaps.push(freshCheck.gate_only
|
|
72
|
+
? "files were changed but no configured verification gate (a test, typecheck, lint, build or check command) has passed since the last edit; reading or printing files does not count"
|
|
73
|
+
: "files were changed but no command has succeeded since the last edit");
|
|
66
74
|
if (acceptance?.status === "needs_work") gaps.push(`declared acceptance criteria need evidence: ${[
|
|
67
75
|
...missingCommands.map(item => `${item.id}=${item.status}`),
|
|
68
76
|
...(acceptance.targets ?? []).filter(target => target.status !== "ready").map(target => `target ${target.id}=${target.status}`),
|
|
69
77
|
].join(", ")}`);
|
|
70
|
-
if (text.length > 0 && looksLikeHandBack(text) && acceptance?.status !== "awaiting_human")
|
|
71
|
-
|
|
72
|
-
: "the reply handed the task back instead of finishing it; no one will answer");
|
|
78
|
+
if (text.length > 0 && looksLikeHandBack(text) && acceptance?.status !== "awaiting_human")
|
|
79
|
+
gaps.push("the reply deferred the request; check whether authorized work remains or a specific blocker requires the user");
|
|
73
80
|
if (gaps.length === 0) return finish;
|
|
74
81
|
// D11 / O.11: a declared command needs its exact receipt even when the
|
|
75
82
|
// request has no recorded edit. Compound shell success is not that receipt.
|
|
@@ -77,7 +84,7 @@ export function requestCompletion(state: RequestState, remainingRequests: number
|
|
|
77
84
|
? "Obtain a fresh successful receipt for each missing command. Run each command exactly as a separate invocation without adding other commands:\n" +
|
|
78
85
|
missingCommands.map(item => `- ${item.id}: ${JSON.stringify(item.command)}`).join("\n") +
|
|
79
86
|
"\nIf verification cannot run, report the specific blocker. "
|
|
80
|
-
:
|
|
87
|
+
: !state.changed
|
|
81
88
|
? "Use original sources for research and full inputs for calculations; do not invent command evidence. Finish with the result or the specific blocker. "
|
|
82
89
|
: "Run the command that verifies the result, or finish with the specific blocker that prevents verification. ";
|
|
83
90
|
return { action: "continue", guidance: {
|
package/src/core/continuation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** N.1: working-state guidance is an append-only observation, never a summary. */
|
|
2
|
+
import { currentRequestEvents } from "./request-state.ts";
|
|
2
3
|
import type { GuidanceEvent, LogEvent } from "./events.ts";
|
|
3
4
|
import type { Handoff } from "./handoff.ts";
|
|
4
5
|
import { DEFAULT_BOUNDARY, type BoundaryConfig } from "./boundary.ts";
|
|
@@ -79,27 +80,27 @@ export function continuationGuidance(input: {
|
|
|
79
80
|
* short task a turn and can make some models stop mid-task.
|
|
80
81
|
*/
|
|
81
82
|
minAssistantTurns?: number;
|
|
82
|
-
taskAware?: boolean;
|
|
83
83
|
}): Notice | null {
|
|
84
84
|
const { events, handoff, tools, remainingRequests } = input;
|
|
85
|
-
// A new user request owns its own budget and plan;
|
|
86
|
-
|
|
87
|
-
const
|
|
85
|
+
// A new user request owns its own budget and plan; steering continues it (S.1).
|
|
86
|
+
// Without a user event this segment resumes a request from its handoff.
|
|
87
|
+
const scoped = currentRequestEvents(events);
|
|
88
|
+
const resumed = scoped.length === 0;
|
|
89
|
+
const current = resumed ? events : scoped;
|
|
88
90
|
const notices = new Set(current.filter((e) => e.t === "guidance").map((e) => e.reason));
|
|
89
91
|
const assistants = current.filter((e) => e.t === "assistant");
|
|
90
92
|
if (remainingRequests <= 3 && assistants.length > 0 && !notices.has("budget")) return {
|
|
91
93
|
reason: "budget", sources: [],
|
|
92
|
-
text: `Harness: this request batch has ${remainingRequests} model requests remaining. ` +
|
|
93
|
-
|
|
94
|
-
: "Prioritize decisive evidence and the final response. If evidence is insufficient, state what remains unresolved."),
|
|
94
|
+
text: `Harness: this request batch has ${remainingRequests} model requests remaining. ` +
|
|
95
|
+
"Prioritize remaining requirements and verification. A smaller budget does not establish completion; report unfinished work or missing evidence explicitly.",
|
|
95
96
|
};
|
|
96
97
|
const work = deriveWorkItems(handoff?.state.work_items ?? [], events);
|
|
97
98
|
const next = current.some(e => e.t === "next_action" && e.work_id === undefined && e.next_action.trim().length > 0) ||
|
|
98
|
-
(
|
|
99
|
+
(resumed && handoff?.state.in_flight.some(item => item.work_id === undefined && item.next_action.trim().length > 0)) ||
|
|
99
100
|
work.some(item => item.status === "active" && item.next_action?.trim() &&
|
|
100
|
-
(
|
|
101
|
+
(resumed || current.some(event => "work_id" in event && event.work_id === item.id)));
|
|
101
102
|
if (tools.includes("note") && !next && !notices.has("continuation") &&
|
|
102
|
-
(assistants.length >= (input.minAssistantTurns ?? 2) || (
|
|
103
|
+
(assistants.length >= (input.minAssistantTurns ?? 2) || (resumed && handoff !== null && !handoff.continuation_valid))) return {
|
|
103
104
|
reason: "continuation", sources: [],
|
|
104
105
|
text: "Harness: no active subtask with a next action is recorded for this work. Record note(kind='next', work_id=the stable task ID, what=the active subtask, next_action=the concrete next step) alongside your next useful action. " +
|
|
105
106
|
(tools.includes("history") && handoff !== null ? "Use history(recent=true) to recover prior attempts before repeating exploration. " : "") +
|
package/src/core/control-view.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/** Q24: control surfaces inspect original facts; they never own a second goal or context. */
|
|
2
|
-
import { currentRequestRecords, deriveRequestState, type RequestPart, type RequestState } from "./request-state.ts";
|
|
2
|
+
import { currentRequestRecords, deriveRequestState, type LedgerOptions, type RequestPart, type RequestState } from "./request-state.ts";
|
|
3
3
|
import { contextDetail } from "./context-detail.ts";
|
|
4
4
|
import { projectContext } from "./project.ts";
|
|
5
5
|
import { assertNever } from "./exhaustive.ts";
|
|
6
6
|
|
|
7
7
|
export type GoalView = { text: string; state: RequestState; gaps: string[] };
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/** The request's first words and the ledger's verdict; blockers the model reported are listed, never scored (D11). */
|
|
10
|
+
export function goalView(parts: readonly RequestPart[], options: LedgerOptions = {}): GoalView | null {
|
|
11
|
+
const state = deriveRequestState(parts, options);
|
|
11
12
|
const first = currentRequestRecords(parts)[0]?.event;
|
|
12
13
|
if (!state || first?.t !== "user") return null;
|
|
13
|
-
|
|
14
|
-
? state.acceptance.criteria.filter(item => item.status !== "pass" && item.status !== "accepted").map(item => `${item.description}: ${item.status}`)
|
|
15
|
-
: ["No acceptance contract; command success does not verify the goal."];
|
|
16
|
-
gaps.push(...state.reported_blockers.map(item => `Reported blocker: ${item.reason}`));
|
|
17
|
-
return { text: first.text, state, gaps };
|
|
14
|
+
return { text: first.text, state, gaps: [...state.unmet, ...state.reported_blockers.map(item => `Reported blocker: ${item.reason}`)] };
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
export type ContextLens = ReturnType<typeof contextLens>;
|
package/src/core/deadline.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* this reason. Time arrives as arguments: core reads no clock.
|
|
8
8
|
*/
|
|
9
9
|
import type { GuidanceEvent, LogEvent } from "./events.ts";
|
|
10
|
+
import { currentRequestEvents } from "./request-state.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Assumed, not measured: a quarter of the budget leaves room for one
|
|
@@ -28,7 +29,7 @@ export function deadlineNotice(
|
|
|
28
29
|
if (!Number.isFinite(start) || !Number.isFinite(current) || !Number.isFinite(budgetMs) || budgetMs <= 0) {
|
|
29
30
|
throw new RangeError("deadline needs valid timestamps and a positive budget");
|
|
30
31
|
}
|
|
31
|
-
const request =
|
|
32
|
+
const request = currentRequestEvents(events);
|
|
32
33
|
if (request.some((event) => event.t === "guidance" && event.reason === "deadline")) return null;
|
|
33
34
|
const used = current - start;
|
|
34
35
|
if (used < budgetMs * DEADLINE_FRACTION) return null;
|
package/src/core/derive.ts
CHANGED
|
@@ -299,13 +299,6 @@ export function deriveHandoff(input: DeriveInput): Handoff {
|
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
301
|
for (const e of events) {
|
|
302
|
-
if (e.t === "snapshot") {
|
|
303
|
-
for (const [path, anchor] of anchors) {
|
|
304
|
-
const now = e.changed[path] ?? e.added[path];
|
|
305
|
-
if (e.removed.includes(path) || (now !== undefined && now !== anchor.sha256)) anchor.stale = true;
|
|
306
|
-
else if (now === anchor.sha256) delete anchor.stale;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
302
|
if (e.t === "checkpoint" && e.phase === "applied") for (const [path, change] of Object.entries(e.changes)) {
|
|
310
303
|
const anchor = anchors.get(path);
|
|
311
304
|
if (anchor) {
|
package/src/core/environment.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ArtifactRef, LogEvent } from "./events.ts";
|
|
2
2
|
import { isObservationArtifact } from "./observation-view.ts";
|
|
3
|
+
import { workspaceChanged } from "./checkpoints.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Facts about the machine, fixed at session start.
|
|
@@ -153,8 +154,7 @@ export type EnvironmentValidity = { state: "recorded" | "expired" | "scope_chang
|
|
|
153
154
|
/** Recomputed on consumption: recent observations are never guarantees about a future invocation. */
|
|
154
155
|
export function environmentValidity(receipt: EnvironmentReceipt, source: EnvironmentSource, now: number, after: readonly LogEvent[]): EnvironmentValidity {
|
|
155
156
|
for (const event of after) {
|
|
156
|
-
if (event.t === "user_bash" || event
|
|
157
|
-
(Object.keys(event.added).length > 0 || Object.keys(event.changed).length > 0 || event.removed.length > 0)) {
|
|
157
|
+
if (event.t === "user_bash" || workspaceChanged(event)) {
|
|
158
158
|
return { state: "invalidated", reason: "later command or workspace change requires environment revalidation", event: event.id };
|
|
159
159
|
}
|
|
160
160
|
if (event.t !== "tool_result") continue;
|
|
@@ -16,7 +16,6 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
16
16
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
17
|
}
|
|
18
18
|
const strings = (value: unknown): boolean => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
19
|
-
const stringMap = (value: unknown): boolean => isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
20
19
|
const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
|
|
21
20
|
const natural = (value: unknown): boolean => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
22
21
|
const eventRef = (value: unknown): boolean => isRecord(value) && natural(value["node"]) && typeof value["event"] === "string" && value["event"] !== "";
|
|
@@ -46,6 +45,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
46
45
|
["prepared", "applied", "unavailable"].includes(String(value["phase"])) && validFileChanges(value["changes"]) &&
|
|
47
46
|
optionalString(value["tree"]) && optionalString(value["reason"]);
|
|
48
47
|
case "workspace_observation": return value["scope"] === "source-v1" && typeof value["changed"] === "boolean" && optionalString(value["reason"]) &&
|
|
48
|
+
(value["baseline"] === undefined || value["baseline"] === true) &&
|
|
49
49
|
(value["fingerprint"] === undefined || typeof value["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(value["fingerprint"]));
|
|
50
50
|
case "process_exit": {
|
|
51
51
|
const output = value["output"];
|
|
@@ -67,6 +67,18 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
67
67
|
case "acceptance_decision": return has("request", "contract_id", "criterion") && value["actor"] === "user" &&
|
|
68
68
|
(value["target_binding"] === undefined ? value["target_observation"] === undefined : has("target_binding", "target_observation"));
|
|
69
69
|
case "target_observation": return isRuntimeTargetReceipt(value["observation"]);
|
|
70
|
+
case "counter_check": return has("request", "criterion", "command") && (value["guard"] === undefined || value["guard"] === true) &&
|
|
71
|
+
(Number.isSafeInteger(value["exit"]) ? value["reason"] === undefined : typeof value["reason"] === "string" && value["exit"] === undefined);
|
|
72
|
+
case "blind_check": return has("request", "check") && typeof value["exposed"] === "boolean" && optionalString(value["exposure"]) &&
|
|
73
|
+
(Number.isSafeInteger(value["exit"]) ? value["reason"] === undefined : typeof value["reason"] === "string" && value["exit"] === undefined);
|
|
74
|
+
case "goal": {
|
|
75
|
+
if (value["action"] === "clear") return value["reason"] === "met" || value["reason"] === "user";
|
|
76
|
+
if (value["action"] !== "set" || !has("objective", "draft") || !strings(value["accepted"]) || !strings(value["rejected"])) return false;
|
|
77
|
+
try {
|
|
78
|
+
const contract = parseAcceptanceContract(value["contract"]), accepted = value["accepted"] as string[];
|
|
79
|
+
return accepted.length > 0 && contract.criteria.map(item => item.id).join("\0") === accepted.join("\0");
|
|
80
|
+
} catch { return false; }
|
|
81
|
+
}
|
|
70
82
|
case "extension": return has("customType", "content") && typeof value["display"] === "boolean" && typeof value["inContext"] === "boolean" && (value["data"] === undefined || isJsonValue(value["data"]));
|
|
71
83
|
case "user": return has("text") && (value["request"] === undefined || typeof value["request"] === "string" && value["request"] !== "");
|
|
72
84
|
case "outcome": return has("reason") && isExecutionOutcome(value["outcome"]);
|
|
@@ -80,7 +92,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
80
92
|
isRecord(ref) && typeof ref["sha256"] === "string" &&
|
|
81
93
|
typeof ref["path"] === "string" && natural(ref["bytes"]);
|
|
82
94
|
}
|
|
83
|
-
case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline", "runtime_acceptance"].includes(String(value["reason"])) && strings(value["sources"]);
|
|
95
|
+
case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline", "runtime_acceptance", "goal"].includes(String(value["reason"])) && strings(value["sources"]);
|
|
84
96
|
case "interrupt": return has("user_id");
|
|
85
97
|
case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) &&
|
|
86
98
|
(value["replay_key"] === undefined || typeof value["replay_key"] === "string" && /^[a-f0-9]{64}$/.test(value["replay_key"])) &&
|
|
@@ -119,6 +131,10 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
119
131
|
}
|
|
120
132
|
if (value["diagnostic"] !== undefined && !isToolDiagnostic(value["diagnostic"])) return false;
|
|
121
133
|
if (value["warnings"] !== undefined && (!Array.isArray(value["warnings"]) || !value["warnings"].every(isToolDiagnostic))) return false;
|
|
134
|
+
const instructions = value["instructions"];
|
|
135
|
+
if (instructions !== undefined && (!isRecord(instructions) || !Array.isArray(instructions["keys"]) || instructions["keys"].length === 0 ||
|
|
136
|
+
!instructions["keys"].every(key => typeof key === "string" && key.length > 0) ||
|
|
137
|
+
typeof instructions["text"] !== "string" || instructions["text"].length === 0)) return false;
|
|
122
138
|
if (!has("call_id", "content") || typeof value["ok"] !== "boolean") return false;
|
|
123
139
|
if (!optionalString(value["content_sha256"])) return false;
|
|
124
140
|
if (!optionalString(value["observation_sha256"])) return false;
|
|
@@ -140,7 +156,7 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
140
156
|
return effect["kind"] === "edit" && typeof effect["path"] === "string" && typeof effect["sha256_after"] === "string" &&
|
|
141
157
|
(effect["sha256_before"] === null || typeof effect["sha256_before"] === "string");
|
|
142
158
|
}
|
|
143
|
-
case "cut": return has("handoff_ref") && (value["working_set_version"] === undefined || value["working_set_version"] ===
|
|
159
|
+
case "cut": return has("handoff_ref") && (value["working_set_version"] === undefined || value["working_set_version"] === 2) && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
|
|
144
160
|
(value["guidance_tools"] === undefined || Array.isArray(value["guidance_tools"]) && value["guidance_tools"].every(item => typeof item === "string")) &&
|
|
145
161
|
optionalString(value["retained_from"]) && optionalString(value["next_context_ref"]) &&
|
|
146
162
|
(value["next_node"] === undefined || natural(value["next_node"])) && optionalString(value["next_model"]) &&
|
|
@@ -162,9 +178,6 @@ export function isLogEvent(value: unknown): value is LogEvent {
|
|
|
162
178
|
case "work_status": return has("work_id", "what") && ["unblock", "reopen", "cancel"].includes(String(value["operation"])) &&
|
|
163
179
|
optionalString(value["reason"]) && (value["operation"] === "cancel" ||
|
|
164
180
|
(typeof value["next_action"] === "string" && value["next_action"].trim().length > 0));
|
|
165
|
-
case "snapshot": return stringMap(value["added"]) && stringMap(value["changed"]) && strings(value["removed"]) &&
|
|
166
|
-
strings(value["undeclared"]) && strings(value["declared_but_unchanged"]) && optionalString(value["workspace_sha256"]) &&
|
|
167
|
-
(value["phase"] === undefined || value["phase"] === "turn" || value["phase"] === "idle");
|
|
168
181
|
case "rewind": return ["both", "files", "context"].includes(String(value["mode"])) && validNodeRef(value["from"]) &&
|
|
169
182
|
(value["files_from"] === undefined || validNodeRef(value["files_from"])) &&
|
|
170
183
|
has("prompt") && strings(value["restored"]) && strings(value["removed"]) && strings(value["unrecoverable"]) && strings(value["divergence"]);
|