@sema-agent/core 7.16.0 → 7.17.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 +173 -0
- package/dist/agents/builtin-agents.d.ts +3 -3
- package/dist/agents/cross-session-envelope.d.ts +0 -1
- package/dist/agents/cross-session-envelope.js +0 -1
- package/dist/agents/peer-notices.d.ts +0 -9
- package/dist/agents/peer-notices.js +0 -63
- package/dist/agents/subagent.js +7 -15
- package/dist/brain/request-params.d.ts +2 -2
- package/dist/config/defaults.d.ts +0 -7
- package/dist/config/defaults.js +0 -2
- package/dist/core/artifact-host.d.ts +218 -0
- package/dist/core/artifact-host.js +92 -0
- package/dist/core/ask-origin.d.ts +29 -3
- package/dist/core/ask-origin.js +17 -9
- package/dist/core/ask-question.d.ts +25 -0
- package/dist/core/ask-question.js +4 -0
- package/dist/core/ask-unresolvable-notice.d.ts +30 -21
- package/dist/core/ask-unresolvable-notice.js +16 -8
- package/dist/core/auto-compaction.d.ts +10 -10
- package/dist/core/auto-mode-arming.d.ts +2 -2
- package/dist/core/background-agent-store.d.ts +5 -5
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-execution-record.d.ts +0 -3
- package/dist/core/checkpoint-execution-record.js +0 -4
- package/dist/core/checkpoint-store.d.ts +71 -99
- package/dist/core/checkpoint-store.js +9 -16
- package/dist/core/context-edit.d.ts +36 -104
- package/dist/core/context-edit.js +37 -60
- package/dist/core/effective-path-target.d.ts +68 -8
- package/dist/core/effective-path-target.js +42 -30
- package/dist/core/engine-notice.d.ts +46 -32
- package/dist/core/env-budget.d.ts +31 -0
- package/dist/core/env-budget.js +39 -0
- package/dist/core/exec-output-tail.d.ts +5 -5
- package/dist/core/file-history-store.js +3 -6
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.js +3 -3
- package/dist/core/gate-lanes.js +9 -15
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hands-band.d.ts +1 -1
- package/dist/core/hooks.d.ts +18 -17
- package/dist/core/lsp-session.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/lsp.js +5 -4
- package/dist/core/mcp.d.ts +5 -5
- package/dist/core/mcp.js +4 -48
- package/dist/core/memory-recall.d.ts +0 -8
- package/dist/core/memory.d.ts +1 -1
- package/dist/core/oracle-isolation.js +2 -2
- package/dist/core/parent-spec-seat.d.ts +66 -0
- package/dist/core/parent-spec-seat.js +47 -0
- package/dist/core/permission-rule-model.d.ts +11 -6
- package/dist/core/permission-rule-model.js +53 -37
- package/dist/core/permission-rules.js +8 -3
- package/dist/core/protocol-naming.d.ts +2 -2
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/retired-keys.d.ts +19 -0
- package/dist/core/retired-keys.js +15 -0
- package/dist/core/runner/active-skill-scope.js +3 -3
- package/dist/core/runner/assemble-result.d.ts +15 -13
- package/dist/core/runner/assemble-result.js +5 -4
- package/dist/core/runner/compaction-call-options.d.ts +25 -89
- package/dist/core/runner/contracts.d.ts +10 -9
- package/dist/core/runner/gate-exit.js +15 -1
- package/dist/core/runner/halt-attribution.d.ts +35 -0
- package/dist/core/runner/halt-attribution.js +16 -0
- package/dist/core/runner/memory-consolidation.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.js +0 -1
- package/dist/core/runner/prepare-artifact.d.ts +53 -0
- package/dist/core/runner/prepare-artifact.js +77 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-config-doors.js +5 -2
- package/dist/core/runner/prepare-context-lane.d.ts +2 -2
- package/dist/core/runner/prepare-context-lane.js +2 -13
- package/dist/core/runner/prepare-file-history.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.js +3 -52
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
- package/dist/core/runner/prepare-inherited-gate.js +7 -8
- package/dist/core/runner/prepare-memory.d.ts +4 -6
- package/dist/core/runner/prepare-memory.js +4 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
- package/dist/core/runner/prepare-policy-chain.js +15 -30
- package/dist/core/runner/prepare-prompt-assembly.js +1 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.js +0 -2
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-wiring-manifest.js +9 -7
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-internals-and-config.js +4 -0
- package/dist/core/runner/retired-runner-deps.d.ts +3 -0
- package/dist/core/runner/retired-runner-deps.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +2 -3
- package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -1
- package/dist/core/runner/session-file-state-replay.js +3 -3
- package/dist/core/runner/session-rule-policy.js +3 -4
- package/dist/core/runner/stream-halt-verbs.js +1 -1
- package/dist/core/runner/stream-steer-verb.js +1 -4
- package/dist/core/runner/workspace-path.js +3 -0
- package/dist/core/runner-deps.d.ts +30 -49
- package/dist/core/safe-notify.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/sensitive-path-policy.js +4 -4
- package/dist/core/session.d.ts +2 -2
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/strategy-store.d.ts +8 -11
- package/dist/core/task-event.d.ts +20 -17
- package/dist/core/task-limits.d.ts +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +5 -5
- package/dist/core/task-registry-monitor.d.ts +2 -2
- package/dist/core/task-registry-shared.d.ts +27 -27
- package/dist/core/task-registry-shared.js +3 -13
- package/dist/core/task-registry.d.ts +8 -8
- package/dist/core/task-result.d.ts +37 -37
- package/dist/core/task-spec.d.ts +5 -5
- package/dist/core/task-stream.d.ts +16 -11
- package/dist/core/tool-catalog-entries.d.ts +1 -0
- package/dist/core/tool-catalog-entries.js +5 -0
- package/dist/core/tool-conformance.d.ts +0 -2
- package/dist/core/tool-conformance.js +1 -3
- package/dist/core/tool-errors.d.ts +5 -0
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +51 -22
- package/dist/core/tool-policy.js +28 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +11 -11
- package/dist/core/tool-spec.d.ts +34 -38
- package/dist/core/tools.d.ts +2 -2
- package/dist/core/trace.d.ts +12 -15
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +4 -3
- package/dist/core/workflow-journal-store.d.ts +11 -11
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
- package/dist/engine/execution-env/node-execution-env.js +4 -2
- package/dist/engine/harness/agent-harness.d.ts +47 -4
- package/dist/engine/harness/agent-harness.js +52 -9
- package/dist/engine/loop/agent-loop.js +15 -8
- package/dist/engine/loop/types.d.ts +5 -0
- package/dist/index.d.ts +19 -14
- package/dist/index.js +15 -10
- package/dist/internal/harness.d.ts +1 -0
- package/dist/orchestration/run-spec.d.ts +3 -0
- package/dist/orchestration/run-spec.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +12 -6
- package/dist/orchestration/run-workflow-tool.js +23 -5
- package/dist/orchestration/workflow-primitives.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +1 -4
- package/dist/orchestration/workflow-types.d.ts +34 -4
- package/dist/orchestration/workflow-types.js +2 -0
- package/dist/orchestration/workflow.d.ts +39 -7
- package/dist/orchestration/workflow.js +234 -113
- package/dist/prompt-assembly/assemble.d.ts +2 -2
- package/dist/prompt-assembly/assemble.js +9 -9
- package/dist/prompt-assembly/composer.d.ts +6 -7
- package/dist/prompt-assembly/composer.js +10 -10
- package/dist/prompt-assembly/epoch.js +1 -1
- package/dist/prompt-assembly/event-registry.d.ts +0 -21
- package/dist/prompt-assembly/packs/sema-default.js +30 -30
- package/dist/prompt-assembly/types.d.ts +14 -14
- package/dist/prompts/coordinator.d.ts +0 -13
- package/dist/prompts/coordinator.js +0 -3
- package/dist/stores/file/checkpoint-store.js +1 -5
- package/dist/stores/file/file-history-store.js +2 -2
- package/dist/stores/file/permission-rule-store.d.ts +4 -35
- package/dist/stores/file/permission-rule-store.js +4 -148
- package/dist/tools/artifact/artifact-text.d.ts +62 -0
- package/dist/tools/artifact/artifact-text.js +68 -0
- package/dist/tools/artifact/artifact-tool.d.ts +52 -0
- package/dist/tools/artifact/artifact-tool.js +351 -0
- package/dist/tools/artifact/local-stub.d.ts +26 -0
- package/dist/tools/artifact/local-stub.js +296 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +74 -8
- package/dist/tools/fs/bash-readonly-classifier.js +171 -60
- package/dist/tools/fs/fs-bash.d.ts +14 -10
- package/dist/tools/fs/fs-bash.js +19 -27
- package/dist/tools/fs/fs-search-tools.js +5 -2
- package/dist/tools/fs/fs-shared.d.ts +2 -0
- package/dist/tools/fs/fs-shared.js +3 -11
- package/dist/tools/fs/index.d.ts +4 -0
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/safety.d.ts +117 -1
- package/dist/tools/fs/safety.js +131 -31
- package/dist/tools/fs/search.js +8 -6
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +115 -39
|
@@ -508,7 +508,7 @@ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("res
|
|
|
508
508
|
* A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate
|
|
509
509
|
* gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too.
|
|
510
510
|
*
|
|
511
|
-
*
|
|
511
|
+
* It also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
|
|
512
512
|
* if something can undo it; an adapter that offers `suspendVM` but no `resumeVM` used to pass this guard,
|
|
513
513
|
* take a real snapshot, commit a real checkpoint — and then blow up on the resume leg with an untyped
|
|
514
514
|
* TypeError. "Suspendable" now means the whole round trip, so a half-adapter is rejected at the suspend site
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE presence rule for a RETIRED key on a caller-supplied bag (a deps object, an options object): the key is
|
|
3
|
+
* PRESENT iff ordinary property lookup answers a defined value — prototype and non-enumerable members included,
|
|
4
|
+
* exactly what the former direct read of the seat honored; an explicit `undefined` reads as absence, as every
|
|
5
|
+
* optional seat reads. Every retired-key door reads through here so no door grows its own idea of "present";
|
|
6
|
+
* each composes its own refusal from the answer. A table row is `{ key: replacement }` — the sentence that says
|
|
7
|
+
* what replaced the seat, quoted in the refusal.
|
|
8
|
+
*/
|
|
9
|
+
/** The first retired key present on `bag`, in table order, or undefined when none is. */
|
|
10
|
+
export declare function retiredKeyOf(bag: object, table: Readonly<Record<string, string>>): {
|
|
11
|
+
key: string;
|
|
12
|
+
replacement: string;
|
|
13
|
+
} | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The deps-bag door: a retired seat still wired by a deployment is refused BY NAME (`config.deps_retired_key`)
|
|
16
|
+
* instead of vanishing silently — a seat that is silently dropped reads to the deployment as wired. `owner` is
|
|
17
|
+
* the bag's type name for the sentence (`RunnerDeps`, `RunWorkflowToolDeps`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertNoRetiredKeys(bag: object, table: Readonly<Record<string, string>>, owner: string): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function retiredKeyOf(bag, table) {
|
|
2
|
+
for (const [key, replacement] of Object.entries(table)) {
|
|
3
|
+
if (Reflect.get(bag, key) !== undefined)
|
|
4
|
+
return { key, replacement };
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
export function assertNoRetiredKeys(bag, table, owner) {
|
|
9
|
+
const hit = retiredKeyOf(bag, table);
|
|
10
|
+
if (hit === undefined)
|
|
11
|
+
return;
|
|
12
|
+
const e = new Error(`${owner}.${hit.key} is retired and is not read by this engine — ${hit.replacement}. Refused rather than ignored: a seat that is silently dropped reads to the deployment as wired.`);
|
|
13
|
+
e.code = "config.deps_retired_key";
|
|
14
|
+
throw e;
|
|
15
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalizeTarget } from "../../tools/fs/safety.js";
|
|
1
|
+
import { canonicalizeTarget, pathFamilyOf } from "../../tools/fs/safety.js";
|
|
2
2
|
import { pathTargetValue } from "../tool-registry.js";
|
|
3
3
|
import { pathTargetBaseOf } from "../effective-path-target.js";
|
|
4
4
|
import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
|
|
@@ -100,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
100
100
|
decisionReason: "safety",
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
|
|
103
|
+
const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootPath, cwd: req.cwd ?? env.cwd }), pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
|
|
104
104
|
if (!canon.ok) {
|
|
105
105
|
return {
|
|
106
106
|
action: "deny",
|
|
@@ -111,7 +111,7 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
111
111
|
for (const m of constraining) {
|
|
112
112
|
const roots = [];
|
|
113
113
|
for (const raw of m.allowPaths ?? []) {
|
|
114
|
-
const r = await canonicalizeTarget(env, raw, signal, rootPath);
|
|
114
|
+
const r = await canonicalizeTarget(env, raw, signal, pathFamilyOf({ root: rootPath ?? env.cwd }), rootPath);
|
|
115
115
|
if (r.ok)
|
|
116
116
|
roots.push(r.key);
|
|
117
117
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage } from "../../internal/llm.js";
|
|
2
2
|
import type { NestedUsage, PausedCause, TaskResult, TaskSpec, TerminalCause } from "../types.js";
|
|
3
|
+
import type { HaltConsumption } from "../../internal/harness.js";
|
|
3
4
|
/** Per-task usage/cost accumulator (filled across the run loop, surfaced as `TaskResult.stats`). */
|
|
4
5
|
export interface Stats {
|
|
5
6
|
turns: number;
|
|
@@ -134,15 +135,14 @@ export interface ResultFlags {
|
|
|
134
135
|
* path the terminal is `completed`, and this is what tells that completion apart from a natural
|
|
135
136
|
* one — the model did not finish; the person stopped it and the run awaits their direction). */
|
|
136
137
|
haltedOnUserRejection?: boolean;
|
|
137
|
-
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
userHalted?: boolean;
|
|
138
|
+
/** the harness's STATEMENT of what the run's halt did (see `HaltConsumption`):
|
|
139
|
+
* the loop's report that the halt's cut settled the final turn, and/or the boundary consult that
|
|
140
|
+
* stopped the loop for the halt's request. This is the ONLY input to `TaskResult.haltedByUser`
|
|
141
|
+
* (`haltConsumed`, halt-attribution.ts) and to slot 9.7 — the assembly never infers causation from the
|
|
142
|
+
* final's shape or from the verb's acceptance. A halt that was accepted but consumed nothing (the run
|
|
143
|
+
* ended for its own reason first) leaves this empty: the original terminal stands and the seat is
|
|
144
|
+
* NOT signed (the settle lane mints `task.halt_unconsumed` for it). */
|
|
145
|
+
haltConsumption?: HaltConsumption;
|
|
146
146
|
/** Call ids of answered-but-never-collected questions, echoed on
|
|
147
147
|
* `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
|
|
148
148
|
* optional `onError` alert is NOT the disclosure — this mandatory result face is. */
|
|
@@ -298,13 +298,15 @@ export declare function terminalCauseOf(final: AssistantMessage | undefined, sta
|
|
|
298
298
|
* 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
|
|
299
299
|
* (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
|
|
300
300
|
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
|
|
301
|
-
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal
|
|
301
|
+
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`: the
|
|
302
|
+
* loop reported the halt's cut settled the final turn — never shape inference),
|
|
302
303
|
* and EXCEPT when the answer had already settled (`answerSettled`: a clean `"stop"` final, not
|
|
303
304
|
* partial) — a stop landing on the run's tail cuts no work, so the run falls through to 12
|
|
304
305
|
* (`completed`, the answer as `result`, no `limits.*` code for the axis; three axes, one law)
|
|
305
|
-
* 9.7 `
|
|
306
|
-
* bare user halt — the verb working, not a defect; below the
|
|
307
|
-
* abort/limit that also fired owns the terminal and the halt rides as
|
|
306
|
+
* 9.7 halt CONSUMED (`haltConsumed(flags.haltConsumption)`) + (no `final` | the halt's own aborted
|
|
307
|
+
* `final`) → status `"completed"` (a bare user halt — the verb working, not a defect; below the
|
|
308
|
+
* aborts on purpose, a real abort/limit that also fired owns the terminal and the halt rides as
|
|
309
|
+
* the pass-through seat; an accepted-but-unconsumed halt never reaches this slot)
|
|
308
310
|
* 10. no `final` → status `"failed"`
|
|
309
311
|
* 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
|
|
310
312
|
* 12. else → `"completed"`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
2
2
|
import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
|
|
3
3
|
import { terminalProjection } from "./terminal-projection.js";
|
|
4
|
+
import { haltConsumed } from "./halt-attribution.js";
|
|
4
5
|
const SALVAGE_ELIGIBLE_TERMINALS = new Set([
|
|
5
6
|
"output.degenerate",
|
|
6
7
|
"limits.max_tokens_exceeded",
|
|
@@ -47,7 +48,7 @@ export function answerSettled(final, continuationCut = false) {
|
|
|
47
48
|
export function terminalCauseOf(final, stats, flags) {
|
|
48
49
|
let terminal;
|
|
49
50
|
let apiFailure;
|
|
50
|
-
const haltOwnsAbortedFinal = flags.
|
|
51
|
+
const haltOwnsAbortedFinal = flags.haltConsumption?.turnCut === "halt" &&
|
|
51
52
|
final?.stopReason === "aborted" &&
|
|
52
53
|
!flags.abortedLive &&
|
|
53
54
|
flags.abortedForTimeout !== true &&
|
|
@@ -103,7 +104,7 @@ export function terminalCauseOf(final, stats, flags) {
|
|
|
103
104
|
const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
|
|
104
105
|
terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
|
|
105
106
|
}
|
|
106
|
-
else if (flags.
|
|
107
|
+
else if (haltConsumed(flags.haltConsumption) && (!final || haltOwnsAbortedFinal)) {
|
|
107
108
|
terminal = { kind: "completed" };
|
|
108
109
|
}
|
|
109
110
|
else if (!final) {
|
|
@@ -155,7 +156,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
155
156
|
const result = text;
|
|
156
157
|
let salvagedOutput;
|
|
157
158
|
const { terminal, apiFailure } = terminalCauseOf(final, stats, flags);
|
|
158
|
-
const {
|
|
159
|
+
const { errorCode } = terminalProjection(terminal);
|
|
159
160
|
if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
|
|
160
161
|
salvagedOutput = text.trim() || undefined;
|
|
161
162
|
}
|
|
@@ -164,6 +165,6 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
164
165
|
void _internalCompaction;
|
|
165
166
|
if (flags.unpricedSpend)
|
|
166
167
|
delete publicStats.costMicroUsd;
|
|
167
|
-
const stampHaltedByUser = flags.
|
|
168
|
+
const stampHaltedByUser = haltConsumed(flags.haltConsumption);
|
|
168
169
|
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
169
170
|
}
|
|
@@ -30,96 +30,32 @@ import type { Prepared } from "./contracts.js";
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"];
|
|
32
32
|
/**
|
|
33
|
-
* `MaybeCompactOptions.contextInstructionFiles`: the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* standing instructions every other turn has been reading — and it loses them exactly where the
|
|
37
|
-
* summary REPLACES the transcript that carried them, so the loss is durable.
|
|
33
|
+
* `MaybeCompactOptions.contextInstructionFiles`: the deployment's instruction-file content (the CLAUDE.md
|
|
34
|
+
* family) delivered to the SUMMARIZER, assembled once here and spread on all three lanes — the compaction
|
|
35
|
+
* boundary is where the summary REPLACES the transcript that carried those instructions.
|
|
38
36
|
*
|
|
39
|
-
*
|
|
40
|
-
* the RAW loader text
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* "repository-controlled DATA, not instructions to obey", and this seat hands the same bytes to the
|
|
62
|
-
* summarizer with instruction standing for the summarization task. Delivery THROUGH a host callback
|
|
63
|
-
* is not host AUTHORSHIP: a prior agent, or anyone who can land a commit, can write a
|
|
64
|
-
* "## Compact Instructions" section, and the mint's neutralization stops tag breakout — not a
|
|
65
|
-
* sentence asking for a fact to be left out of the summary that replaces the transcript.
|
|
66
|
-
* Why it is nevertheless this value: the seat's own contract names this path ("hosts pass the same
|
|
67
|
-
* instruction-file content their prompt assembly mounts... on the runner path"), the parity form this
|
|
68
|
-
* channel exists to reach reads the user's and the project's instruction files at every boundary with
|
|
69
|
-
* exactly this standing, and the two alternatives are worse for reasons above rather than better.
|
|
70
|
-
* The standing is also narrower than the framing gap suggests — the section's preamble scopes it to
|
|
71
|
-
* guidance about HOW to summarize and classifies the rest as background — and a deployment that does
|
|
72
|
-
* not want it has two exits that need no code: do not wire the loader, or return content it vouches
|
|
73
|
-
* for. Recorded as an open design candidate rather than built here, because it is a contract change
|
|
74
|
-
* and not a wiring one: a SEPARATE deployment-vouched summarization-instructions seat, leaving
|
|
75
|
-
* repository-derived context at data tier.
|
|
76
|
-
*
|
|
77
|
-
* RULING ① — BOTH summary forms, no per-form split. Fork eligibility is decided per pass INSIDE
|
|
78
|
-
* maybeCompact (main-model-only, recorded-request-present, not lossy), so a call site cannot say
|
|
79
|
-
* "independent form only"; the closest approximation — pass the seat only when `prepared.compModel`
|
|
80
|
-
* is set, since an independent compaction model has no main prefix to fork — leaves the hole the
|
|
81
|
-
* wrong way round: the configuration it does NOT cover (no compaction model) is precisely the one
|
|
82
|
-
* where a fork degrades mid-pass to the independent form, which would then run WITHOUT the files.
|
|
83
|
-
* Feeding both forms closes that case by construction, and the library face already handles the fork
|
|
84
|
-
* half (the section rides the APPENDED instruction message, never the cached prefix, so the fork's
|
|
85
|
-
* whole economic point is untouched).
|
|
86
|
-
*
|
|
87
|
-
* RULING ② — no new size knob. The bound is the library's own `fitContextInstructionFilesSection`
|
|
88
|
-
* (independent form: cap against the compaction model's window, disclosed elision, and a complete
|
|
89
|
-
* yield when not even the framing fits). Its three KNOWN LIMITS are accepted here KNOWINGLY, as the
|
|
90
|
-
* pre-condition of this wiring rather than something it discovered: a token-dense payload (CJK,
|
|
91
|
-
* emoji) can pass a chars-domain fit and still overflow the gateway's tokenizer; an unknown
|
|
92
|
-
* `contextWindow` leaves the section unbounded (the conversation clamp declines to guess there too);
|
|
93
|
-
* and the FORK form cannot fit at all — its bound is a prompt-too-long that degrades ONCE to the
|
|
94
|
-
* independent form, which the engine's own note records as real but NOT total (an oversize rejection
|
|
95
|
-
* the classifier cannot read as prompt-too-long returns terminally instead).
|
|
96
|
-
*
|
|
97
|
-
* HOW BIG THAT ACCEPTANCE ACTUALLY IS — measured, because the sentence above is easy to read more
|
|
98
|
-
* comfortably than it deserves. The fitted form is NOT the default path: when a fork is eligible and
|
|
99
|
-
* the model answers in its envelope, the fork IS the pass, and its section is delivered WHOLE at any
|
|
100
|
-
* size. The independent form — the only one that fits — runs when there is no fork to take or the
|
|
101
|
-
* fork's answer did not conform. Both facts are pinned in the REF-A2 suite (a 55 KB payload: whole on
|
|
102
|
-
* the fork, elided on the independent fallback). And when a limit does bite, the failure is not a
|
|
103
|
-
* quiet degradation: the section is a FIXED part of the request, the reactive retry loop may drop
|
|
104
|
-
* only CONVERSATION groups, so a request the SECTION overflowed cannot converge and the boundary
|
|
105
|
-
* fails — the task loses that compaction (or that prompt-too-long recovery, or its end-of-task
|
|
106
|
-
* summary) rather than sending a shorter one. The exposure is what the wiring widened: this was
|
|
107
|
-
* previously reachable only by hosts calling the library directly, and is now reachable by every
|
|
108
|
-
* deployment that wires `loadProjectMemory` with a large instruction file and an unknown or small
|
|
109
|
-
* window. Making the section participate in the retry loop is the structural answer; it is a change
|
|
110
|
-
* to that loop's contract, tracked at the library face, and a runner-side knob would only move the
|
|
111
|
-
* decision to a place with less information.
|
|
112
|
-
*
|
|
113
|
-
* WHAT IT COSTS, measured rather than rounded: every deployment that wires `loadProjectMemory` now
|
|
114
|
-
* sends its instruction-file content plus EXACTLY 716 chars of framing (the preamble and the fence)
|
|
115
|
-
* on EVERY summarization request, both forms, once per pass — where a real project instruction file
|
|
116
|
-
* in this family runs to tens of KB, so the payload, not the framing, is the bill. The number is
|
|
117
|
-
* pinned in the REF-A2 suite against the engine's own renderer, so a preamble edit shows up as a
|
|
118
|
-
* changed assertion rather than as prose drifting away from the code it describes.
|
|
119
|
-
* Deployments that never wired the loader pay nothing: the key is absent, not
|
|
120
|
-
* `undefined`, so their requests are byte-identical to the pre-wiring shape on the independent and
|
|
121
|
-
* UPDATE passes (the FORK form appends its ~860-char authority clause with or without the seat, and
|
|
122
|
-
* always did).
|
|
37
|
+
* Contract:
|
|
38
|
+
* - Bytes = `Prepared.projectInstructionContent`, the RAW loader text captured under the same non-blank
|
|
39
|
+
* predicate that composes the project layer. Not the assembled `memoryBlock` (model-authored in-band
|
|
40
|
+
* bytes would gain an instructing envelope), not the COMPOSED project block (`contextInstructionFilesSection`
|
|
41
|
+
* neutralizes the whole authority family, `user_memory`/`scope` included), not `instructionSources`
|
|
42
|
+
* (path + hash only). Raw text in, one fence at the mint.
|
|
43
|
+
* - Provenance: REPOSITORY-CONTROLLED text delivered through a host callback — delivery is not authorship;
|
|
44
|
+
* the mint's neutralization stops tag breakout, not a sentence asking for an omission. Two no-code exits
|
|
45
|
+
* for a deployment that does not want it: do not wire the loader, or return vouched content. A separate
|
|
46
|
+
* deployment-vouched summarization-instructions seat is an open design candidate, not built here.
|
|
47
|
+
* - Both summary forms, no per-form split: fork eligibility is decided per pass inside maybeCompact, and the
|
|
48
|
+
* form NOT covered by a `compModel`-gated split is exactly the fork that degrades mid-pass to the
|
|
49
|
+
* independent form; the section rides the APPENDED instruction message, never the cached prefix.
|
|
50
|
+
* - No new size knob: the bound is `fitContextInstructionFilesSection` (independent form only). Its three
|
|
51
|
+
* known limits are accepted knowingly: a token-dense payload can pass a chars-domain fit and still overflow;
|
|
52
|
+
* an unknown `contextWindow` leaves the section unbounded; the FORK form cannot fit and degrades ONCE. When
|
|
53
|
+
* a limit bites the boundary FAILS (the section is a fixed request part the retry loop cannot drop) — the
|
|
54
|
+
* task loses that compaction rather than sending a shorter one; making the section participate in the
|
|
55
|
+
* retry loop is a library-face contract change, tracked there.
|
|
56
|
+
* - Cost: instruction-file content + exactly 716 chars of framing on every summarization request, both
|
|
57
|
+
* forms, once per pass (pinned in the REF-A2 suite). Deployments that never wired the loader pay nothing:
|
|
58
|
+
* the key is ABSENT, so their requests are byte-identical to the pre-wiring shape.
|
|
123
59
|
*/
|
|
124
60
|
export declare function contextInstructionFilesOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "contextInstructionFiles">>;
|
|
125
61
|
/**
|
|
@@ -28,7 +28,7 @@ import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
|
|
|
28
28
|
import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
|
|
29
29
|
import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
|
|
30
30
|
import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
|
|
31
|
-
import type { ClearedProjectionLedger,
|
|
31
|
+
import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } from "../context-edit.js";
|
|
32
32
|
import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
|
|
33
33
|
import type { MaterializedMcp } from "../mcp.js";
|
|
34
34
|
import type { MemoryEngine } from "../memory-engine/engine.js";
|
|
@@ -1051,16 +1051,14 @@ export interface Prepared {
|
|
|
1051
1051
|
* `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
|
|
1052
1052
|
* start EMPTY by construction), the last request's projection seat (what the provider actually
|
|
1053
1053
|
* saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
|
|
1054
|
-
* raw session rebuild), the MC-R knob, and the slice-3 arm-B seat.
|
|
1055
|
-
* (`machine: "legacy"` + MC-R off) ⇒ the ledger never gains an entry and every replay is a
|
|
1056
|
-
* same-reference no-op (opt-out bytes unchanged). */
|
|
1054
|
+
* raw session rebuild), the MC-R knob, and the slice-3 arm-B seat. */
|
|
1057
1055
|
microCompact: PreparedMicroCompact;
|
|
1058
1056
|
}
|
|
1059
1057
|
/** See {@link Prepared.microCompact}. */
|
|
1060
1058
|
export interface PreparedMicroCompact {
|
|
1061
|
-
/** The frontier-machine selection — `"off"` = no proactive frontier clearing (the
|
|
1062
|
-
*
|
|
1063
|
-
machine:
|
|
1059
|
+
/** The frontier-machine selection — `"off"` = no proactive frontier clearing (the machine
|
|
1060
|
+
* instead gets its one blocking-point shot, slice-3 arm A). */
|
|
1061
|
+
machine: MicroCompactMachine;
|
|
1064
1062
|
/** MC-R: one-shot clear-and-retry on a provider input-too-long rejection.
|
|
1065
1063
|
* Default true since the slice-3 flip. */
|
|
1066
1064
|
clearOnRejection: boolean;
|
|
@@ -1105,7 +1103,7 @@ export interface PrepareResume {
|
|
|
1105
1103
|
* crash-interrupted; closing them with `[INTERRUPTED]` would DESTROY the suspended batch, §15.2 #7). */
|
|
1106
1104
|
suspendedBatch: ReadonlySet<string>;
|
|
1107
1105
|
/** The §4.bis correctness-state snapshot to re-seed (activeTools / outputRef / nestedStats /
|
|
1108
|
-
*
|
|
1106
|
+
* readFileState). */
|
|
1109
1107
|
seed: CheckpointState;
|
|
1110
1108
|
/** How many times this task already suspended (the resumed checkpoint's
|
|
1111
1109
|
* `suspendCount`). The next suspend mints `priorSuspendCount + 1`; past `maxSuspends` it fails
|
|
@@ -1393,7 +1391,10 @@ export interface RunInternals {
|
|
|
1393
1391
|
* `"interactive"` tree's engine-stripped legs are exempt). TRUST POSTURE (ruled 2026-08-05):
|
|
1394
1392
|
* RunInternals is a public trusted-caller parameter, so this flag is an honesty channel, not a
|
|
1395
1393
|
* security gate — a caller forging it is a deployment lying to itself; resource-face safety
|
|
1396
|
-
* stays with its own fail-closed mechanisms.
|
|
1394
|
+
* stays with its own fail-closed mechanisms. INHERITS DOWN THE TREE: a stripped leg's descendants are faceless by
|
|
1395
|
+
* the same engine act (the trusted ctx carries it as `ToolExecuteContext.questionFaceStripped`, the Workflow mount as
|
|
1396
|
+
* `parentQuestionFaceStripped`), so the posture-door exemption travels with the facelessness it explains; a leg
|
|
1397
|
+
* that strips on its own mints the same flag.
|
|
1397
1398
|
*/
|
|
1398
1399
|
questionFaceStripped?: true;
|
|
1399
1400
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
|
|
2
|
-
import { classifierDenyCauseOf, decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
2
|
+
import { askUnresolvableCauseOf, classifierDenyCauseOf, decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
3
3
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
4
4
|
import { formatHookFeedback } from "../reminder-mint.js";
|
|
5
5
|
export function mintGateOutcome(facts) {
|
|
@@ -96,6 +96,15 @@ export function hookDenyExit(pass, r) {
|
|
|
96
96
|
preToolContext: pass.preToolContext,
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
function unresolvableCauseOfSettlement(gate) {
|
|
100
|
+
if (gate.disposition.kind !== "denied" || gate.disposition.deniedBy !== "ask_resolution" || gate.settlement === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (gate.settlement.kind === "approver_unavailable")
|
|
103
|
+
return "approver_unavailable";
|
|
104
|
+
if (gate.settlement.kind === "no_approver")
|
|
105
|
+
return "no_approver_wired";
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
99
108
|
export async function exitGate(pass) {
|
|
100
109
|
const { input, event, toolName, toolCallId, ledger, notifyPermissionDenied } = pass;
|
|
101
110
|
if (pass.decision.action === "deny") {
|
|
@@ -106,6 +115,11 @@ export async function exitGate(pass) {
|
|
|
106
115
|
if (pass.deniedBy === undefined)
|
|
107
116
|
throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
|
|
108
117
|
const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(pass.denyCause !== undefined ? { cause: pass.denyCause } : {}), ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
|
|
118
|
+
const unresolvable = askUnresolvableCauseOf(pass.decision, { toolCallId, toolName }) ?? unresolvableCauseOfSettlement(gate);
|
|
119
|
+
if (unresolvable !== undefined) {
|
|
120
|
+
const cause = unresolvable;
|
|
121
|
+
pass.notifier.notify(() => input.onAskUnresolvable?.({ toolName, toolCallId, cause, parkLaneExisted: pass.suspendAsk !== undefined }), "toolGate.onAskUnresolvable");
|
|
122
|
+
}
|
|
109
123
|
await notifyPermissionDenied({
|
|
110
124
|
toolName,
|
|
111
125
|
input: cloneObserverInput(pass.currentInput),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halt attribution, the consumer half: the ONE predicate that turns the harness's
|
|
3
|
+
* consumption statement ({@link HaltConsumption}) into the result's `haltedByUser` seat, and the disclosure
|
|
4
|
+
* for the halt that was accepted but never consumed. Both readers of the statement live here so the law
|
|
5
|
+
* has one spelling: signed ⇔ consumed; unconsumed ⇔ absent + `task.halt_unconsumed`.
|
|
6
|
+
*/
|
|
7
|
+
import type { HaltConsumption } from "../../internal/harness.js";
|
|
8
|
+
import { type RunnerDeps } from "../types.js";
|
|
9
|
+
/**
|
|
10
|
+
* The attribution law: `haltedByUser` ⇔ the halt was CONSUMED by the machinery — its cut settled a turn (the
|
|
11
|
+
* loop's report, stamped `"halt"` at the seat's abort) OR its stop request was among the sources a boundary
|
|
12
|
+
* consult stopped the loop for. A halt merely ACCEPTED (latched, receipt answered) while the run ended for its
|
|
13
|
+
* own reason is not consumed and never signs (fail-closed: rather absent than guessed).
|
|
14
|
+
*/
|
|
15
|
+
export declare function haltConsumed(c: HaltConsumption | undefined): boolean;
|
|
16
|
+
export interface UnconsumedHaltDisclosureInput {
|
|
17
|
+
/** The verb's acceptance fact (`loopLatch.userHalted`): a halt was latched on this run. */
|
|
18
|
+
accepted: boolean;
|
|
19
|
+
/** The harness's statement, read after the prompt settled. */
|
|
20
|
+
consumption: HaltConsumption | undefined;
|
|
21
|
+
onNotice: RunnerDeps["onNotice"];
|
|
22
|
+
sessionId: string;
|
|
23
|
+
runId: string;
|
|
24
|
+
taskId: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The ACCEPTED-but-UNCONSUMED halt: the verb latched the stop (and may have cut a seat), but the run ended for
|
|
28
|
+
* its own reason before the machinery consumed it — no settlement took the halt's cut as its cause and no
|
|
29
|
+
* boundary consult stopped the loop for it (a brain minting its own `aborted` final, a natural completion past
|
|
30
|
+
* the final commit point, an approval park whose commit won the race). The result reads the run's own terminal
|
|
31
|
+
* without `haltedByUser`; this notice is the halt's trace — the same code the verb mints on its own arm (a halt
|
|
32
|
+
* refused by an already-owned ending), one fact with one spelling, the two mint points disjoint: the verb arm mints
|
|
33
|
+
* only while NO halt has been accepted on the run; once one was, this arm alone speaks. One per such run. Returns whether it was minted.
|
|
34
|
+
*/
|
|
35
|
+
export declare function discloseUnconsumedHalt(input: UnconsumedHaltDisclosureInput): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "../types.js";
|
|
2
|
+
export function haltConsumed(c) {
|
|
3
|
+
return c?.turnCut === "halt" || c?.boundaryStop?.sources.includes("halt") === true;
|
|
4
|
+
}
|
|
5
|
+
export function discloseUnconsumedHalt(input) {
|
|
6
|
+
if (!input.accepted || haltConsumed(input.consumption))
|
|
7
|
+
return false;
|
|
8
|
+
deliverEngineNotice(input.onNotice, {
|
|
9
|
+
code: "task.halt_unconsumed",
|
|
10
|
+
message: "a user halt was accepted, but the run ended for its own reason before the halt stopped or cut anything: " +
|
|
11
|
+
"no turn was settled as its cut and no boundary stopped for it — the run's own ending stands, and the result " +
|
|
12
|
+
"will not carry haltedByUser for this halt.",
|
|
13
|
+
detail: { sessionId: input.sessionId, runId: input.runId, ...(input.taskId !== undefined ? { taskId: input.taskId } : {}) },
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
@@ -23,8 +23,6 @@ export declare const DEFAULT_CONSOLIDATION_BAND: {
|
|
|
23
23
|
export declare const DEFAULT_CONSOLIDATION_SEARCH_LIMIT = 20;
|
|
24
24
|
/** Default cap on this-task notes consolidated in one batch (F6 token bound). */
|
|
25
25
|
export declare const DEFAULT_CONSOLIDATION_MAX_NOTES = 50;
|
|
26
|
-
/** Default independent timeout for the whole consolidation pass (seconds). */
|
|
27
|
-
export declare const DEFAULT_CONSOLIDATION_TIMEOUT_SEC = 30;
|
|
28
26
|
/** Normalize a note body for the EXACT-restatement check in the `<lo` near-dup cheap path. Case- + whitespace-
|
|
29
27
|
* insensitive ONLY (lower-case, collapse whitespace runs, trim); it PRESERVES every symbol/punctuation so it can
|
|
30
28
|
* NOT false-equate semantically-distinct facts. (The earlier `[^\p{L}\p{N}]`-stripping
|
|
@@ -4,7 +4,6 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
|
|
|
4
4
|
export const DEFAULT_CONSOLIDATION_BAND = { lo: 0.05, hi: 0.3 };
|
|
5
5
|
export const DEFAULT_CONSOLIDATION_SEARCH_LIMIT = 20;
|
|
6
6
|
export const DEFAULT_CONSOLIDATION_MAX_NOTES = 50;
|
|
7
|
-
export const DEFAULT_CONSOLIDATION_TIMEOUT_SEC = 30;
|
|
8
7
|
export function normalizeForExactMatch(s) {
|
|
9
8
|
return s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
10
9
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrepareTask's Artifact phase: the `RunnerDeps.artifactHost` seat is read once; an absent seat mounts
|
|
3
|
+
* nothing and announces nothing; a seat whose declaration fails an invariant mounts nothing and announces
|
|
4
|
+
* `config.artifact_host_invalid` (operator audience) so a bad wiring can never read as "no hosting"; a
|
|
5
|
+
* valid seat mounts ONE `Artifact` tool assembled from the declaration, on the same optional band the LSP
|
|
6
|
+
* seat uses. The tool's local-file read goes through the run's fs fence (root containment, read-deny,
|
|
7
|
+
* read face) exactly as `Read` does, then reads the bytes once from the execution env. A local stub's store
|
|
8
|
+
* (`capabilities.storageDir`) must lie OUTSIDE every root the run may write or read — the task root and both
|
|
9
|
+
* classes of additional directories — because a ledger the model can edit through the fs tools is not a trust
|
|
10
|
+
* boundary; a store inside one refuses the mount with the same loud notice, naming the directory and the root.
|
|
11
|
+
* Synchronous: the stretch has no await (the file read is a closure the tool calls later).
|
|
12
|
+
*/
|
|
13
|
+
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
14
|
+
import { type ArtifactPlane } from "../artifact-host.js";
|
|
15
|
+
import { RosterBuilder } from "../tool-roster.js";
|
|
16
|
+
import type { AgentTool, RunnerDeps } from "../types.js";
|
|
17
|
+
import type { ReadDenyMatcher, ReadFace } from "../../tools/fs/index.js";
|
|
18
|
+
import type { Prepared } from "./contracts.js";
|
|
19
|
+
export interface PrepareArtifactInput {
|
|
20
|
+
/** borrowed-readonly — the deployment's hosting seat and the notice sink the refusal arm announces on. */
|
|
21
|
+
deps: Pick<RunnerDeps, "artifactHost" | "onNotice">;
|
|
22
|
+
/** borrowed-readonly — the engine-minted run id: the host's `onMount` key and every call context's `runId`. */
|
|
23
|
+
runId: string;
|
|
24
|
+
/** borrowed-readonly — the host task id, the call context's audit attribution. */
|
|
25
|
+
taskId: string;
|
|
26
|
+
/** borrowed-readonly — the acquired session id (notice attribution and the call context's `sessionId`). */
|
|
27
|
+
sessionId: string;
|
|
28
|
+
/** borrowed-readonly — the spawned name this run carries, when any (call-context attribution only). */
|
|
29
|
+
agentName: string | undefined;
|
|
30
|
+
/** borrowed-readonly — the run's resolved env: the byte source of a published local file. */
|
|
31
|
+
executionEnv: ExecutionEnv;
|
|
32
|
+
/** borrowed-readonly — the canonical task root the fs fence contains reads to; undefined ⇒ no local-file publish. */
|
|
33
|
+
rootCanonical: string | undefined;
|
|
34
|
+
/** borrowed-readonly — the extra containment roots (write and read-only) the fence admits. */
|
|
35
|
+
additionalRootsCanonical: readonly string[];
|
|
36
|
+
/** borrowed-readonly — the read-deny matcher in force for the run, when any. */
|
|
37
|
+
readDenyMatcher: ReadDenyMatcher | undefined;
|
|
38
|
+
/** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
|
|
39
|
+
resolvedReadFace: ReadFace | undefined;
|
|
40
|
+
/** borrowed-readonly — the tracked-cwd cell; `current` is read per call as the relative-path base. */
|
|
41
|
+
handsCwdRef: Prepared["cwdRef"];
|
|
42
|
+
/** borrowed-mutable — the run's roster builder; writer here: ONE mount when the seat is valid. */
|
|
43
|
+
roster: RosterBuilder;
|
|
44
|
+
/** borrowed-readonly — the first-party result-offload wrapper every core mount passes through (a large read or a conflict snapshot is offloaded, not inlined). */
|
|
45
|
+
firstPartyOffload: (tool: AgentTool) => AgentTool;
|
|
46
|
+
/** borrowed-mutable — the env-hand membership set the memory content-origin face reads (a tool that reads the execution env's files is a hand); writer here: `add("Artifact")` on the mount. */
|
|
47
|
+
envHandToolNames: Set<string>;
|
|
48
|
+
}
|
|
49
|
+
export interface PrepareArtifactResult {
|
|
50
|
+
/** owned — the mounted face's plane, or undefined when nothing mounted (absent seat, or an invalid one). */
|
|
51
|
+
artifactPlane: ArtifactPlane | undefined;
|
|
52
|
+
}
|
|
53
|
+
export declare function prepareArtifact(input: PrepareArtifactInput): PrepareArtifactResult;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readArtifactHostCapabilities } from "../artifact-host.js";
|
|
2
|
+
import { ARTIFACT_TOOL_NAME } from "../tool-catalog-entries.js";
|
|
3
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
4
|
+
import { RosterBuilder } from "../tool-roster.js";
|
|
5
|
+
import { defineTool } from "../tools.js";
|
|
6
|
+
import { inlineUntrusted } from "../untrusted-text.js";
|
|
7
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
9
|
+
import { resolveKey, violationDetails, violationText, withinAnyRoot } from "../../tools/fs/safety.js";
|
|
10
|
+
import { createArtifactTool } from "../../tools/artifact/artifact-tool.js";
|
|
11
|
+
function realAncestorPath(p) {
|
|
12
|
+
const abs = resolve(p);
|
|
13
|
+
const tail = [];
|
|
14
|
+
let cur = abs;
|
|
15
|
+
while (!existsSync(cur)) {
|
|
16
|
+
const parent = dirname(cur);
|
|
17
|
+
if (parent === cur)
|
|
18
|
+
return abs;
|
|
19
|
+
tail.unshift(basename(cur));
|
|
20
|
+
cur = parent;
|
|
21
|
+
}
|
|
22
|
+
return tail.length === 0 ? realpathSync(cur) : join(realpathSync(cur), ...tail);
|
|
23
|
+
}
|
|
24
|
+
export function prepareArtifact(input) {
|
|
25
|
+
const { deps, runId, taskId, sessionId, agentName, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames } = input;
|
|
26
|
+
const host = deps.artifactHost;
|
|
27
|
+
if (host === undefined)
|
|
28
|
+
return { artifactPlane: undefined };
|
|
29
|
+
const refuse = (problem) => {
|
|
30
|
+
deliverEngineNotice(deps.onNotice, {
|
|
31
|
+
code: "config.artifact_host_invalid",
|
|
32
|
+
message: `RunnerDeps.artifactHost is wired but unusable, so no Artifact tool is mounted for this task: ${problem}`,
|
|
33
|
+
detail: { problem, sessionId, runId },
|
|
34
|
+
});
|
|
35
|
+
return { artifactPlane: undefined };
|
|
36
|
+
};
|
|
37
|
+
const verdict = readArtifactHostCapabilities(host);
|
|
38
|
+
if (!verdict.ok)
|
|
39
|
+
return refuse(verdict.problem);
|
|
40
|
+
const storageDir = verdict.capabilities.storageDir;
|
|
41
|
+
if (storageDir !== undefined && rootCanonical !== undefined) {
|
|
42
|
+
const real = realAncestorPath(storageDir);
|
|
43
|
+
const inside = [rootCanonical, ...additionalRootsCanonical].find((root) => withinAnyRoot([root], real));
|
|
44
|
+
if (inside !== undefined)
|
|
45
|
+
return refuse(`the local artifact store ${storageDir} lies inside a root this run may reach through its file tools (${inside}); put the store outside every task root and additional directory`);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
host.onMount?.(runId);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return refuse(`artifactHost.onMount threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
52
|
+
}
|
|
53
|
+
const readLocalFile = async (path, maxBytes, signal) => {
|
|
54
|
+
if (rootCanonical === undefined)
|
|
55
|
+
return { ok: false, reason: "refused", text: "Error (Artifact): this task has no filesystem root, so a local file cannot be published." };
|
|
56
|
+
const rk = await resolveKey(executionEnv, rootCanonical, path, signal, handsCwdRef?.current, additionalRootsCanonical, undefined, readDenyMatcher, resolvedReadFace);
|
|
57
|
+
if (!rk.ok)
|
|
58
|
+
return { ok: false, reason: "refused", text: violationText("Artifact", rk.violation), details: violationDetails(rk.violation) };
|
|
59
|
+
const meta = await executionEnv.fileInfo(rk.key, signal);
|
|
60
|
+
if (!meta.ok)
|
|
61
|
+
return { ok: false, reason: "refused", text: `Error (Artifact): cannot stat ${inlineUntrusted(path, 200)} (${inlineUntrusted(meta.error.code, 40)}).` };
|
|
62
|
+
if (meta.value.kind !== "file")
|
|
63
|
+
return { ok: false, reason: "not_a_file", kind: meta.value.kind };
|
|
64
|
+
if (meta.value.size > maxBytes)
|
|
65
|
+
return { ok: false, reason: "too_large", size: meta.value.size, limit: maxBytes, judgedOn: "metadata" };
|
|
66
|
+
const r = await executionEnv.readBinaryFile(rk.key, signal);
|
|
67
|
+
if (!r.ok)
|
|
68
|
+
return { ok: false, reason: "refused", text: `Error (Artifact): cannot read ${inlineUntrusted(path, 200)} (${inlineUntrusted(r.error.code, 40)}).` };
|
|
69
|
+
if (r.value.byteLength > maxBytes)
|
|
70
|
+
return { ok: false, reason: "too_large", size: r.value.byteLength, limit: maxBytes, judgedOn: "bytes" };
|
|
71
|
+
return { ok: true, key: rk.key, bytes: r.value };
|
|
72
|
+
};
|
|
73
|
+
const artifactSpec = createArtifactTool({ host, capabilities: verdict.capabilities, runId, taskId, sessionId, ...(agentName !== undefined ? { agentName } : {}), readLocalFile });
|
|
74
|
+
roster.mount(firstPartyOffload(defineTool(artifactSpec)), { source: "builtin", mountedBy: "optional" });
|
|
75
|
+
envHandToolNames.add(ARTIFACT_TOOL_NAME);
|
|
76
|
+
return { artifactPlane: verdict.capabilities.plane };
|
|
77
|
+
}
|