@sema-agent/core 5.64.0 → 5.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/core/checkpoint-store.d.ts +95 -4
- package/dist/core/hooks.d.ts +9 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +1 -0
- package/dist/core/memory-engine/engine.js +9 -5
- package/dist/core/memory-engine/types.d.ts +16 -0
- package/dist/core/remote-env.d.ts +3 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +9 -0
- package/dist/core/runner/prepare-hands-readface.js +4 -1
- package/dist/core/runner/prepare-memory.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +15 -4
- package/dist/core/runner/prepare-task.js +46 -20
- package/dist/core/runner/runtask.d.ts +18 -0
- package/dist/core/runner/runtask.js +150 -3
- package/dist/core/stub-env.d.ts +4 -0
- package/dist/core/stub-env.js +1 -0
- package/dist/core/task-registry-shared.js +20 -2
- package/dist/core/types.d.ts +91 -8
- package/dist/core/workflow-run-store-contract.js +13 -0
- package/dist/core/workflow-run-store.d.ts +17 -0
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/harness/types.d.ts +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +35 -0
- package/dist/orchestration/workflow-types.js +2 -2
- package/dist/orchestration/workflow.js +20 -1
- package/dist/prompts/default.d.ts +8 -0
- package/dist/prompts/default.js +3 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +5 -1
|
@@ -36,13 +36,13 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
|
|
|
36
36
|
import { assembleResult, errorCodeOf } from "./assemble-result.js";
|
|
37
37
|
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
|
|
38
38
|
import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
39
|
-
import { effectiveDelegationFacts, gatedCallIdOf, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
39
|
+
import { effectiveDelegationFacts, gatedCallIdOf, placementValueOrAbsent, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
40
40
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
41
41
|
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
42
42
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
43
43
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
44
44
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
45
|
-
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
45
|
+
import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, MAX_HOOK_TIMEOUT_MS, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
46
46
|
import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
|
|
47
47
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
48
48
|
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
@@ -84,6 +84,7 @@ function sameAcceptedSteerInput(a, b) {
|
|
|
84
84
|
a.actor?.issuer === b.actor?.issuer);
|
|
85
85
|
}
|
|
86
86
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
87
|
+
const RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS = 10_000;
|
|
87
88
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
88
89
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
89
90
|
const COMPACTION_FREED_EPSILON = 256;
|
|
@@ -1455,6 +1456,8 @@ export class Runner {
|
|
|
1455
1456
|
snapshotTooLargeRoots = new Map();
|
|
1456
1457
|
pendingSessionNotifications = new PendingSessionNotifications();
|
|
1457
1458
|
parentConstraintRegistry = new Map();
|
|
1459
|
+
suspendedEnvReaps = new Map();
|
|
1460
|
+
locallyClaimedTokens = new Set();
|
|
1458
1461
|
static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
|
|
1459
1462
|
constructor(deps) {
|
|
1460
1463
|
this.deps = deps;
|
|
@@ -1663,8 +1666,11 @@ export class Runner {
|
|
|
1663
1666
|
await this.runLocked(spec, queue, (r) => {
|
|
1664
1667
|
resultValue = r;
|
|
1665
1668
|
const pcs = internals?.inheritedGate?.parentConstraints;
|
|
1669
|
+
if (resume !== undefined)
|
|
1670
|
+
this.locallyClaimedTokens.delete(resume.cp.token);
|
|
1666
1671
|
if (resume !== undefined && r.checkpointToken !== resume.cp.token) {
|
|
1667
1672
|
this.parentConstraintRegistry.delete(resume.cp.token);
|
|
1673
|
+
this.suspendedEnvReaps.delete(resume.cp.token);
|
|
1668
1674
|
}
|
|
1669
1675
|
if (r.status === "suspended" && r.checkpointToken !== undefined && pcs !== undefined && pcs.length > 0) {
|
|
1670
1676
|
if (this.parentConstraintRegistry.size >= Runner.PARENT_CONSTRAINT_REGISTRY_CAP) {
|
|
@@ -1681,6 +1687,7 @@ export class Runner {
|
|
|
1681
1687
|
publishReady(h);
|
|
1682
1688
|
}, (s) => {
|
|
1683
1689
|
reapHandle = s;
|
|
1690
|
+
this.suspendedEnvReaps.set(s.token, s);
|
|
1684
1691
|
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
|
|
1685
1692
|
}
|
|
1686
1693
|
finally {
|
|
@@ -1698,6 +1705,7 @@ export class Runner {
|
|
|
1698
1705
|
const reopenReason = code === "resume.tool_unavailable" ? "tool_unavailable" : "env_failed";
|
|
1699
1706
|
const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
|
|
1700
1707
|
let reopenCommitted = false;
|
|
1708
|
+
let reopenStateUnknown = false;
|
|
1701
1709
|
if (reopenable && resume?.onEnvRestoreFailed) {
|
|
1702
1710
|
try {
|
|
1703
1711
|
await resume.onEnvRestoreFailed(reopenReason);
|
|
@@ -1708,15 +1716,26 @@ export class Runner {
|
|
|
1708
1716
|
const original = err instanceof Error ? err : new Error(String(err));
|
|
1709
1717
|
const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
|
|
1710
1718
|
const definitive = errorCodeOf(reopenErr) === "checkpoint.reopen_failed";
|
|
1719
|
+
reopenStateUnknown = !definitive;
|
|
1711
1720
|
err = Object.assign(new Error(definitive
|
|
1712
1721
|
? `${reopenMsg} (original resume failure: ${original.message})`
|
|
1713
1722
|
: `checkpoint reopen threw mid-flight — state UNKNOWN, confirm via the checkpoint store before retrying: ${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), { code: "checkpoint.reopen_failed" });
|
|
1714
1723
|
code = "checkpoint.reopen_failed";
|
|
1715
1724
|
}
|
|
1716
1725
|
}
|
|
1726
|
+
if (resume)
|
|
1727
|
+
this.locallyClaimedTokens.delete(resume.cp.token);
|
|
1717
1728
|
if (resume && !reopenCommitted) {
|
|
1718
1729
|
this.parentConstraintRegistry.delete(resume.cp.token);
|
|
1719
1730
|
await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
|
|
1731
|
+
if (!reopenStateUnknown) {
|
|
1732
|
+
const failedReap = this.suspendedEnvReaps.get(resume.cp.token);
|
|
1733
|
+
this.suspendedEnvReaps.delete(resume.cp.token);
|
|
1734
|
+
if (failedReap?.env !== undefined && hasDestroy(failedReap.env)) {
|
|
1735
|
+
const failedEnv = failedReap.env;
|
|
1736
|
+
await settleTeardownLeg(() => failedEnv.destroy(), "terminal resume-failure env destroy", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1720
1739
|
}
|
|
1721
1740
|
if (resultValue !== undefined) {
|
|
1722
1741
|
try {
|
|
@@ -1802,9 +1821,13 @@ export class Runner {
|
|
|
1802
1821
|
reportErr(err);
|
|
1803
1822
|
return;
|
|
1804
1823
|
}
|
|
1805
|
-
if (!won)
|
|
1824
|
+
if (!won) {
|
|
1825
|
+
if (!this.locallyClaimedTokens.has(rh.token))
|
|
1826
|
+
this.suspendedEnvReaps.delete(rh.token);
|
|
1806
1827
|
return;
|
|
1828
|
+
}
|
|
1807
1829
|
this.parentConstraintRegistry.delete(rh.token);
|
|
1830
|
+
this.suspendedEnvReaps.delete(rh.token);
|
|
1808
1831
|
if (rh.env && hasDestroy(rh.env)) {
|
|
1809
1832
|
try {
|
|
1810
1833
|
await rh.env.destroy();
|
|
@@ -4314,6 +4337,7 @@ export class Runner {
|
|
|
4314
4337
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
|
|
4315
4338
|
}
|
|
4316
4339
|
const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
|
|
4340
|
+
internals = internals !== undefined ? { ...internals } : undefined;
|
|
4317
4341
|
const store = resolveCheckpointStore(config, this.deps);
|
|
4318
4342
|
if (!store) {
|
|
4319
4343
|
throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
|
|
@@ -4694,6 +4718,15 @@ export class Runner {
|
|
|
4694
4718
|
"running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
|
|
4695
4719
|
"refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
|
|
4696
4720
|
}
|
|
4721
|
+
{
|
|
4722
|
+
const recordedPlacementRoot = placementValueOrAbsent(cp.state.placementRootSessionId);
|
|
4723
|
+
const suppliedPlacementRoot = placementValueOrAbsent(internals?.placementRoot);
|
|
4724
|
+
if (recordedPlacementRoot !== undefined && suppliedPlacementRoot !== undefined && suppliedPlacementRoot !== recordedPlacementRoot) {
|
|
4725
|
+
throw new CheckpointError("resume.placement_mismatch", `the resume supplied explicit placement root ${JSON.stringify(suppliedPlacementRoot)} but this checkpoint recorded placement root ${JSON.stringify(recordedPlacementRoot)} at suspend — ` +
|
|
4726
|
+
"resuming under a different placement fixed point would re-place the leg (and its descendants) on another target; " +
|
|
4727
|
+
"refused pre-CAS (the checkpoint stays pending): re-resume with the recorded root, or omit internals.placementRoot to inherit it");
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4697
4730
|
const owesDelivery = outcomeGate !== "resource_limit" ||
|
|
4698
4731
|
readPendingSteerQueue(cp.state).length > 0 ||
|
|
4699
4732
|
(cp.state.runningBackgroundTasks?.length ?? 0) > 0;
|
|
@@ -4869,7 +4902,121 @@ export class Runner {
|
|
|
4869
4902
|
if (outcomeForStore === undefined) {
|
|
4870
4903
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
|
|
4871
4904
|
}
|
|
4905
|
+
if (this.deps.resumePreflight !== undefined) {
|
|
4906
|
+
const preflight = this.deps.resumePreflight;
|
|
4907
|
+
const rawDeadline = this.deps.resumePreflightTimeoutMs;
|
|
4908
|
+
let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS;
|
|
4909
|
+
if (rawDeadline !== undefined) {
|
|
4910
|
+
if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_HOOK_TIMEOUT_MS) {
|
|
4911
|
+
preflightTimeoutMs = rawDeadline;
|
|
4912
|
+
}
|
|
4913
|
+
else {
|
|
4914
|
+
try {
|
|
4915
|
+
this.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the seat ceiling of ${MAX_HOOK_TIMEOUT_MS}ms (got ${describeSuppliedValue(rawDeadline)}) — the resume preflight falls back to its own ${RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS}ms default`), { phase: "config", sessionId: cp.sessionId });
|
|
4916
|
+
}
|
|
4917
|
+
catch {
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
const info = {
|
|
4922
|
+
token,
|
|
4923
|
+
sessionId: cp.sessionId,
|
|
4924
|
+
...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
|
|
4925
|
+
...(cp.principal ? { principal: cp.principal } : {}),
|
|
4926
|
+
...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
|
|
4927
|
+
gateKind: cp.gate.kind,
|
|
4928
|
+
};
|
|
4929
|
+
const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
|
|
4930
|
+
let verdict;
|
|
4931
|
+
try {
|
|
4932
|
+
const seat = await runHookSeat("resumePreflight", {
|
|
4933
|
+
timeoutMs: preflightTimeoutMs,
|
|
4934
|
+
...(resumeSignal !== undefined ? { signal: resumeSignal } : {}),
|
|
4935
|
+
abortEnds: true,
|
|
4936
|
+
onBadTimeout: (badErr) => {
|
|
4937
|
+
try {
|
|
4938
|
+
this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
|
|
4939
|
+
}
|
|
4940
|
+
catch {
|
|
4941
|
+
}
|
|
4942
|
+
},
|
|
4943
|
+
owner: this.deps,
|
|
4944
|
+
}, (sig) => preflight(info, sig));
|
|
4945
|
+
if (seat.expired) {
|
|
4946
|
+
throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
|
|
4947
|
+
? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`
|
|
4948
|
+
: `the resume was cancelled while the deployment's resumePreflight was still screening it — refused (retry-later arm)${refusalTail}`);
|
|
4949
|
+
}
|
|
4950
|
+
verdict = seat.value;
|
|
4951
|
+
}
|
|
4952
|
+
catch (preflightErr) {
|
|
4953
|
+
if (preflightErr instanceof CheckpointError)
|
|
4954
|
+
throw preflightErr;
|
|
4955
|
+
const err = preflightErr instanceof Error ? preflightErr : new Error(String(preflightErr));
|
|
4956
|
+
try {
|
|
4957
|
+
this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
|
|
4958
|
+
}
|
|
4959
|
+
catch {
|
|
4960
|
+
}
|
|
4961
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight crashed while screening this resume (${inlineUntrusted(err.message)}) — refused fail-closed (retry-later arm)${refusalTail}`);
|
|
4962
|
+
}
|
|
4963
|
+
const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined;
|
|
4964
|
+
if (verdictOk !== true) {
|
|
4965
|
+
const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined;
|
|
4966
|
+
const disposition = bag?.disposition;
|
|
4967
|
+
const suppliedMessage = bag?.message;
|
|
4968
|
+
const retryAfterMs = bag?.retryAfterMs;
|
|
4969
|
+
const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined;
|
|
4970
|
+
const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)";
|
|
4971
|
+
if (disposition === "terminal" && readableMessage !== undefined) {
|
|
4972
|
+
this.locallyClaimedTokens.add(token);
|
|
4973
|
+
let settled;
|
|
4974
|
+
try {
|
|
4975
|
+
settled = await store.expire(token, cp.scope);
|
|
4976
|
+
}
|
|
4977
|
+
catch (expireErr) {
|
|
4978
|
+
this.locallyClaimedTokens.delete(token);
|
|
4979
|
+
throw expireErr;
|
|
4980
|
+
}
|
|
4981
|
+
if (!settled) {
|
|
4982
|
+
this.locallyClaimedTokens.delete(token);
|
|
4983
|
+
const live = await store.get(token);
|
|
4984
|
+
if (live?.status === "pending") {
|
|
4985
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said}, but the row moved under this refusal (a concurrent resolve/reopen cycle) — nothing was settled here; the row is pending again, re-resume against the current state (the preflight screens every attempt)`);
|
|
4986
|
+
}
|
|
4987
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — a concurrent actor had already settled the row (${describeSuppliedValue(live?.status ?? "consumed")}); nothing was settled by this refusal`);
|
|
4988
|
+
}
|
|
4989
|
+
this.parentConstraintRegistry.delete(token);
|
|
4990
|
+
const terminalReap = this.suspendedEnvReaps.get(token);
|
|
4991
|
+
this.suspendedEnvReaps.delete(token);
|
|
4992
|
+
this.locallyClaimedTokens.delete(token);
|
|
4993
|
+
if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) {
|
|
4994
|
+
const terminalEnv = terminalReap.env;
|
|
4995
|
+
await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => {
|
|
4996
|
+
try {
|
|
4997
|
+
this.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId });
|
|
4998
|
+
}
|
|
4999
|
+
catch {
|
|
5000
|
+
}
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
try {
|
|
5004
|
+
this.deps.onError?.(new Error(`resumePreflight ruled this row PERMANENTLY blocked${said} — the row was settled terminally (expired) by this refusal's single-shot CAS (token no longer redeemable)`), { phase: "hook", sessionId: cp.sessionId });
|
|
5005
|
+
}
|
|
5006
|
+
catch {
|
|
5007
|
+
}
|
|
5008
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — the row was settled terminally (expired by this refusal's single-shot CAS); the token is not redeemable`);
|
|
5009
|
+
}
|
|
5010
|
+
const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined;
|
|
5011
|
+
throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight refused this resume${said}${refusalTail}${waitHint !== undefined ? ` (deployment wait hint: ${String(waitHint)}ms)` : ""}`, waitHint !== undefined ? { retryAfterMs: waitHint } : undefined);
|
|
5012
|
+
}
|
|
5013
|
+
if (recheckGovernanceWindow !== undefined)
|
|
5014
|
+
await recheckGovernanceWindow();
|
|
5015
|
+
}
|
|
5016
|
+
this.locallyClaimedTokens.add(token);
|
|
4872
5017
|
const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
5018
|
+
if (!won)
|
|
5019
|
+
this.locallyClaimedTokens.delete(token);
|
|
4873
5020
|
if (!won) {
|
|
4874
5021
|
const live = await store.get(token);
|
|
4875
5022
|
if (live?.status === "pending") {
|
package/dist/core/stub-env.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { type ExecResult, type ExecutionEnv, ExecutionError, FileError, type Fil
|
|
|
8
8
|
*/
|
|
9
9
|
export declare class StubExecutionEnv implements ExecutionEnv {
|
|
10
10
|
cwd: string;
|
|
11
|
+
/** design/380 O9a — declared on the class so the prepare fold can read the optional interface
|
|
12
|
+
* member off a `ExecutionEnv | StubExecutionEnv` union; the no-I/O stub produces no target
|
|
13
|
+
* content, so it never declares (always `undefined` — the trusted-side default). */
|
|
14
|
+
readonly externalContentTarget?: boolean;
|
|
11
15
|
constructor(cwd?: string);
|
|
12
16
|
private fsErr;
|
|
13
17
|
absolutePath(path: string): Promise<Result<string, FileError>>;
|
package/dist/core/stub-env.js
CHANGED
|
@@ -166,6 +166,22 @@ function workflowAgentRow(a, ordinal) {
|
|
|
166
166
|
...(a.replayed === true ? { replayed: true } : {}),
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
+
function budgetOvershootNote(overshoot) {
|
|
170
|
+
if (overshoot === undefined)
|
|
171
|
+
return "";
|
|
172
|
+
const unsettled = overshoot.unsettledTokens ?? 0;
|
|
173
|
+
const total = overshoot.spentTokens + unsettled;
|
|
174
|
+
return (` token budget OVERSHOT: this run spent ${total.toLocaleString()} total tokens against a ${overshoot.budgetTokens.toLocaleString()} ceiling ` +
|
|
175
|
+
`(over by ${(total - overshoot.budgetTokens).toLocaleString()}` +
|
|
176
|
+
`${unsettled > 0 ? `, of which ${unsettled.toLocaleString()} was observed on agents still in flight at the terminal and never settled` : ""}).`);
|
|
177
|
+
}
|
|
178
|
+
function timeoutInterruptionNote(interruption) {
|
|
179
|
+
if (interruption === undefined)
|
|
180
|
+
return "";
|
|
181
|
+
return (` INTERRUPTED by the workflow's total timeout (${interruption.timeoutMs.toLocaleString()}ms): when the deadline fired ` +
|
|
182
|
+
`${interruption.agentsCompleted} agent(s) had completed, ${interruption.agentsFailed} had failed, and ` +
|
|
183
|
+
`${interruption.agentsInFlight} were still in flight — the failed run status is the deadline's, not a verdict on those agents.`);
|
|
184
|
+
}
|
|
169
185
|
export function formatWorkflowRun(run) {
|
|
170
186
|
const summary = summarizeWorkflowRun(run);
|
|
171
187
|
const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length;
|
|
@@ -223,7 +239,7 @@ export function formatWorkflowRun(run) {
|
|
|
223
239
|
: {}),
|
|
224
240
|
}
|
|
225
241
|
: {}),
|
|
226
|
-
note: run.status === "running"
|
|
242
|
+
note: (run.status === "running"
|
|
227
243
|
? "still running — poll again shortly."
|
|
228
244
|
: run.status === "completed"
|
|
229
245
|
? run.result !== undefined
|
|
@@ -231,7 +247,9 @@ export function formatWorkflowRun(run) {
|
|
|
231
247
|
: "completed — this run predates result persistence; its result was delivered on the completion notification."
|
|
232
248
|
: finishedAgents.length > 0
|
|
233
249
|
? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.`
|
|
234
|
-
: "failed — see error."
|
|
250
|
+
: "failed — see error.") +
|
|
251
|
+
timeoutInterruptionNote(summary.timeoutInterruption) +
|
|
252
|
+
budgetOvershootNote(summary.budgetOvershoot),
|
|
235
253
|
}),
|
|
236
254
|
details: {
|
|
237
255
|
task_id: run.id,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -5216,8 +5216,13 @@ export interface EngineNotice {
|
|
|
5216
5216
|
* two numbers are read off the report's rows, never subtracted from each other). One notice PER
|
|
5217
5217
|
* HARVEST that withheld at least one entry file (a checkpoint harvest and the terminal harvest
|
|
5218
5218
|
* are distinct facts), never minted for a clean session; `detail: { count, moved, escalated,
|
|
5219
|
-
* reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read
|
|
5220
|
-
* time — the withheld count stays true either way).
|
|
5219
|
+
* reportId?, reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read
|
|
5220
|
+
* at report time — the withheld count stays true either way). `reportId` (#479) is the minting
|
|
5221
|
+
* harvest's own occurrence identity ({@link import("./memory-engine/types.js").HarvestReport}
|
|
5222
|
+
* `.reportId`, uuidv7): two harvests whose counts and paths coincide carry DISTINCT ids, a
|
|
5223
|
+
* durable replay of the same notice carries the SAME id, and the hold family below shares this
|
|
5224
|
+
* harvest's id — dedup on `(code, detail.reportId)`, never on `reportId` alone (absent only for
|
|
5225
|
+
* a hand-built report; every engine-minted report carries it). The formerly registered index-only gap is
|
|
5221
5226
|
* CLOSED under `memoryProvenance: "carry"` (design/336 §6.3, #331): the mint condition reads
|
|
5222
5227
|
* `HarvestReport.containment`, so a containment whose only act was the derived-index rollback
|
|
5223
5228
|
* announces with `count: 0` and `detail.indexRolledBack: true`; under `"off"` the pre-336
|
|
@@ -5226,15 +5231,18 @@ export interface EngineNotice {
|
|
|
5226
5231
|
* §4/§6.3) — the instruction-hold lifecycle, derived from the same structured
|
|
5227
5232
|
* `HarvestReport.containment` signal (hold seats are only ever filled under
|
|
5228
5233
|
* `memoryProvenance: "carry"`): a PENDING session's instruction-form entry files were captured
|
|
5229
|
-
* off the model-visible plane (`hold_opened`, `detail: { count, paths, sessionId? }`
|
|
5230
|
-
* neutralized/length-bounded); previously held entries re-walked the full gate set and
|
|
5234
|
+
* off the model-visible plane (`hold_opened`, `detail: { count, paths, reportId?, sessionId? }`
|
|
5235
|
+
* — paths neutralized/length-bounded); previously held entries re-walked the full gate set and
|
|
5231
5236
|
* committed after their writer session settled clean or a host valve released them
|
|
5232
|
-
* (`hold_released
|
|
5233
|
-
* `detail.disposed: [{ path, terminal }]` with the closed terminal set
|
|
5237
|
+
* (`hold_released`, same detail shape); held entries moved to control-plane quarantine
|
|
5238
|
+
* (`hold_disposed`, `detail.disposed: [{ path, terminal }]` with the closed terminal set
|
|
5234
5239
|
* dirty/expired/conflict/capture_lost/discarded — an `expired` terminal is explicitly a
|
|
5235
5240
|
* TIMEOUT, not a conviction, and the message names `resolveHold` as the recovery valve). At
|
|
5236
5241
|
* most one notice per family per harvest report; wording is factual, never threat-flavored
|
|
5237
|
-
* (the design/336 §13-2 model-psyche guardrail).
|
|
5242
|
+
* (the design/336 §13-2 model-psyche guardrail). `detail.reportId` (#479) is shared by all
|
|
5243
|
+
* three families AND `"memory.harvest_quarantined"` when they derive from the same harvest
|
|
5244
|
+
* report — that sharing is the contract (one report, several distinct facts): dedup on
|
|
5245
|
+
* `(code, detail.reportId)`; a repeat with the same pair is a replay, a new pair is a new fact.
|
|
5238
5246
|
* - `"memory.delegation_static_mark_waived"` (design/324, #324 ruling ①) — the deployment set
|
|
5239
5247
|
* {@link RunnerDeps.memoryDelegationEvidence} to `"attested-only"` and a delegation call whose
|
|
5240
5248
|
* STATIC tool-face verdict would have marked this session's memory polluted (attestation
|
|
@@ -5298,7 +5306,9 @@ export interface EngineNotice {
|
|
|
5298
5306
|
* session count crossed the consolidation thresholds (time gate open ∧ enough distinct
|
|
5299
5307
|
* sessions); minted at most once per crossing (a completed run re-arms the edge), NEVER when
|
|
5300
5308
|
* the deployment leaves consolidation off. ADVISORY: the host owns the verbs, nothing runs
|
|
5301
|
-
* automatically; `detail: { scope, sessionsSince, sessionId? }
|
|
5309
|
+
* automatically; `detail: { scope, sessionsSince, reportId?, sessionId? }` — `reportId` (#479)
|
|
5310
|
+
* is the minting harvest report's occurrence identity, shared with the harvest/hold family's
|
|
5311
|
+
* notices of the same report (dedup on `(code, detail.reportId)`).
|
|
5302
5312
|
* - `"memory.consolidation_committed"` (design/339 §6.2) — a consolidation plan reached
|
|
5303
5313
|
* `completed`: products landed, superseded targets left the default read face (retained as
|
|
5304
5314
|
* evidence), intents settled; `detail: { planId, scope, products, superseded, intents }` —
|
|
@@ -5376,6 +5386,56 @@ export declare function undrainedUserInputNotices(counts: {
|
|
|
5376
5386
|
steer: number;
|
|
5377
5387
|
followUp: number;
|
|
5378
5388
|
}, taskId?: string, sessionId?: string): EngineNotice[];
|
|
5389
|
+
/**
|
|
5390
|
+
* design/380 O2 — the facts handed to a deployment's {@link RunnerDeps.resumePreflight}: the row's
|
|
5391
|
+
* own recorded identity + placement record, read off the persisted checkpoint (never off the resume
|
|
5392
|
+
* caller's bag), so the hook judges the SAME row the CAS is about to consume.
|
|
5393
|
+
*/
|
|
5394
|
+
export interface ResumePreflightInfo {
|
|
5395
|
+
token: import("./checkpoint-store.js").CheckpointToken;
|
|
5396
|
+
sessionId: string;
|
|
5397
|
+
/** design/380 O1③ — the row's recorded placement root, when stamped. */
|
|
5398
|
+
placementRootSessionId?: string;
|
|
5399
|
+
/** The row's recorded identity ({@link import("./checkpoint-store.js").Checkpoint.principal}), when stamped —
|
|
5400
|
+
* the resume entry's identity-continuity rung already refused a contradicting supplied principal
|
|
5401
|
+
* before this hook runs, so a present value IS the resumed leg's identity. */
|
|
5402
|
+
principal?: string;
|
|
5403
|
+
/** provider / deviceId / mountPath — the placement facts. */
|
|
5404
|
+
workspaceHandle?: import("./remote-env.js").WorkspaceHandle;
|
|
5405
|
+
gateKind: import("./checkpoint-store.js").CheckpointGate["kind"];
|
|
5406
|
+
}
|
|
5407
|
+
/** design/380 O2 — a {@link RunnerDeps.resumePreflight} answer. */
|
|
5408
|
+
export type ResumePreflightVerdict = {
|
|
5409
|
+
ok: true;
|
|
5410
|
+
}
|
|
5411
|
+
/** Transient obstacle (device offline, dependency briefly down): the checkpoint stays `pending`,
|
|
5412
|
+
* the SAME token is redeemable later; retryAfterMs threads the wait hint (the #449 G1 carrier). */
|
|
5413
|
+
| {
|
|
5414
|
+
ok: false;
|
|
5415
|
+
disposition?: "retry_later";
|
|
5416
|
+
message: string;
|
|
5417
|
+
retryAfterMs?: number;
|
|
5418
|
+
}
|
|
5419
|
+
/** PERMANENT obstacle (binding revoked, placement identity gone): the row is settled TERMINALLY —
|
|
5420
|
+
* core CASes it out of `pending` before answering, so a dead binding cannot
|
|
5421
|
+
* be redialed forever (prose-terminal + mechanically-retriable = unbounded redeem). The `expire`
|
|
5422
|
+
* CAS is a pure STATUS flip — the row records no reason (the store seam has no seat for one);
|
|
5423
|
+
* the REASON travels on the typed `resume.preflight_rejected` refusal (the hook's `message`
|
|
5424
|
+
* inlined), the `onError` disclosure sink, and the deployment hook's own audit plane — it
|
|
5425
|
+
* authored the verdict and owns the durable record of why. The exact
|
|
5426
|
+
* terminal CAS form is the single-shot `expire` CAS (reaper parity — exactly one settler wins; a
|
|
5427
|
+
* LOST race is re-read and reported as concurrent movement, never claimed as this refusal's
|
|
5428
|
+
* settle); the CONTRACT is: terminal verdict ⇒ atomic single-shot settle, never a silent
|
|
5429
|
+
* pending-forever. PHYSICAL reclamation of a suspended workspace stays with the DEPLOYMENT that
|
|
5430
|
+
* ruled the binding dead — core holds no env pre-restore and a factory cannot reconnect to a
|
|
5431
|
+
* revoked target; the session pin follows the store contract's documented reap posture. Trust
|
|
5432
|
+
* grant is acceptable: the hook lives on RunnerDeps, the same deployment plane that owns the
|
|
5433
|
+
* checkpoint store itself. */
|
|
5434
|
+
| {
|
|
5435
|
+
ok: false;
|
|
5436
|
+
disposition: "terminal";
|
|
5437
|
+
message: string;
|
|
5438
|
+
};
|
|
5379
5439
|
/** Runtime dependencies shared across tasks. */
|
|
5380
5440
|
export interface RunnerDeps {
|
|
5381
5441
|
brain: Brain;
|
|
@@ -5639,6 +5699,29 @@ export interface RunnerDeps {
|
|
|
5639
5699
|
* `checkpointStore` overrides this. Omitted → no durable suspension (policy `ask` stays synchronous).
|
|
5640
5700
|
*/
|
|
5641
5701
|
checkpointStore?: import("./checkpoint-store.js").CheckpointStore;
|
|
5702
|
+
/**
|
|
5703
|
+
* design/380 O2 — deployment-supplied resume preflight, called INSIDE the pre-CAS ladder (after the
|
|
5704
|
+
* row-integrity rungs, immediately before the CAS) with a bounded deadline
|
|
5705
|
+
* ({@link resumePreflightTimeoutMs}). Refusal / throw / timeout ⇒ typed `resume.preflight_rejected`
|
|
5706
|
+
* (CheckpointError closed-set addition); the DEFAULT/absent disposition is `retry_later` (the safe
|
|
5707
|
+
* arm — fail-closed hooks that just throw can never accidentally terminalize a row) — the
|
|
5708
|
+
* entrance-screen posture of the wake-message hook (design/373 D2), generalized from "screen the
|
|
5709
|
+
* wake message" to "screen the resume". A deployment binding runs to targets (a device lane) checks
|
|
5710
|
+
* its placement/binding tables here: a transient obstacle answers `retry_later` (+`retryAfterMs`,
|
|
5711
|
+
* the #449 G1 wait-hint carrier — the row stays `pending`, the SAME token redeems later); a
|
|
5712
|
+
* PERMANENT one answers `terminal` (the row is settled by the single-shot `expire` CAS — never a
|
|
5713
|
+
* silent pending-forever; bounded waiting for a target to come back also belongs HERE, inside the
|
|
5714
|
+
* deadline, never in the env factory). Absent ⇒ no preflight, byte-identical resume behavior.
|
|
5715
|
+
*/
|
|
5716
|
+
resumePreflight?: (info: ResumePreflightInfo, signal: AbortSignal) => Promise<ResumePreflightVerdict>;
|
|
5717
|
+
/**
|
|
5718
|
+
* design/380 O2 — the {@link resumePreflight} deadline in ms. Default 10s (its own bound,
|
|
5719
|
+
* deliberately NOT the hooks-record default: a resume preflight sits on every redeem attempt of a
|
|
5720
|
+
* parked row and must answer promptly or get out of the way). Bad values (non-finite, ≤ 0,
|
|
5721
|
+
* non-number) are DISCLOSED loudly through `onError` and fall back to the default (the bad-value
|
|
5722
|
+
* loudness default: announce, never silently absorb).
|
|
5723
|
+
*/
|
|
5724
|
+
resumePreflightTimeoutMs?: number;
|
|
5642
5725
|
/**
|
|
5643
5726
|
* design/101 §E19 — working-tree snapshot backend for {@link TaskSpec.rewindFiles}. A task with
|
|
5644
5727
|
* `rewindFiles` captures a snapshot per completed turn (keyed by the leaf `SessionTreeEntry.id`) and, when it
|
|
@@ -118,6 +118,19 @@ export async function workflowRunStoreContract(make, runAssertion = defaultSeque
|
|
|
118
118
|
byId.get("d-over").budgetOvershoot.spentTokens = 7;
|
|
119
119
|
assert.equal((await store.get("d-over")).budgetOvershoot.spentTokens, 1_200);
|
|
120
120
|
});
|
|
121
|
+
run("the TOTAL-TIMEOUT disclosure reaches the LIST row: timeoutInterruption projects (de-aliased), and a run without it keeps the key ABSENT", async () => {
|
|
122
|
+
const store = make();
|
|
123
|
+
const seat = { timeoutMs: 600_000, agentsCompleted: 3, agentsFailed: 1, agentsInFlight: 2 };
|
|
124
|
+
const cut = createWorkflowRun({ id: "d-timeout", createdAt: 8_000, status: "failed", error: "workflow total timeout", timeoutInterruption: seat });
|
|
125
|
+
const threw = createWorkflowRun({ id: "d-threw", createdAt: 8_100, status: "failed", error: "script blew up" });
|
|
126
|
+
for (const r of [cut, threw])
|
|
127
|
+
await store.put(r.id, r);
|
|
128
|
+
const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.id, s]));
|
|
129
|
+
assert.deepEqual(byId.get("d-timeout").timeoutInterruption, seat);
|
|
130
|
+
assert.equal("timeoutInterruption" in byId.get("d-threw"), false, "a run its own error ended ⇒ the key is OMITTED, not null/zeroed");
|
|
131
|
+
byId.get("d-timeout").timeoutInterruption.agentsCompleted = 7;
|
|
132
|
+
assert.equal((await store.get("d-timeout")).timeoutInterruption.agentsCompleted, 3);
|
|
133
|
+
});
|
|
121
134
|
run("reap: only terminal runs, NEVER running; maxAgeMs + keep retention; no policy → no-op", async () => {
|
|
122
135
|
const store = make();
|
|
123
136
|
const running = createWorkflowRun({ id: "k-run", status: "running", createdAt: 100, endedAt: undefined });
|
|
@@ -67,6 +67,23 @@ export interface WorkflowRunSummary {
|
|
|
67
67
|
spentTokens: number;
|
|
68
68
|
unsettledTokens?: number;
|
|
69
69
|
};
|
|
70
|
+
/** The run's TERMINAL total-timeout interruption ({@link WorkflowRun.timeoutInterruption}) — present ONLY
|
|
71
|
+
* when the `totalTimeoutMs` deadline had fired by the time the run reached its terminal. The third member
|
|
72
|
+
* of the same family as {@link agentFailures} and {@link budgetOvershoot}: it lets a list row separate
|
|
73
|
+
* "this run was CUT SHORT, and here is what it had achieved" from "this run failed", without an N+1 `get`
|
|
74
|
+
* of the full run. Nothing else on this projection can answer that — {@link status} is `failed` either
|
|
75
|
+
* way, and {@link agentCount} counts records regardless of how they ended.
|
|
76
|
+
*
|
|
77
|
+
* Projected as a de-aliased shallow COPY (same anti-aliasing posture as {@link budgetOvershoot}: a list
|
|
78
|
+
* row must not be a mutable window into the stored run). Read the members per
|
|
79
|
+
* {@link WorkflowRun.timeoutInterruption} — in particular `agentsFailed` is a PRE-abandonment count and is
|
|
80
|
+
* NOT {@link agentFailures}. */
|
|
81
|
+
timeoutInterruption?: {
|
|
82
|
+
timeoutMs: number;
|
|
83
|
+
agentsCompleted: number;
|
|
84
|
+
agentsFailed: number;
|
|
85
|
+
agentsInFlight: number;
|
|
86
|
+
};
|
|
70
87
|
/** Title of the latest phase recorded ({@link WorkflowRun.phases}`.at(-1).title`) — what the run is on RIGHT
|
|
71
88
|
* NOW for a `running` row (so the `/workflows` list shows the live phase without subscribing to the event
|
|
72
89
|
* stream). Absent when no phase has started yet. */
|
|
@@ -10,6 +10,7 @@ export function summarizeWorkflowRun(run) {
|
|
|
10
10
|
status: run.status,
|
|
11
11
|
...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
|
|
12
12
|
...(run.budgetOvershoot !== undefined ? { budgetOvershoot: { ...run.budgetOvershoot } } : {}),
|
|
13
|
+
...(run.timeoutInterruption !== undefined ? { timeoutInterruption: { ...run.timeoutInterruption } } : {}),
|
|
13
14
|
...(latestPhase !== undefined ? { currentPhase: latestPhase.title } : {}),
|
|
14
15
|
phaseCount: run.phases.length,
|
|
15
16
|
agentCount: run.agents.length,
|
|
@@ -455,6 +455,24 @@ export interface ExecutionEnv extends FileSystem, Shell {
|
|
|
455
455
|
* harmless explicit spelling of the local default.
|
|
456
456
|
*/
|
|
457
457
|
readonly hostLocalPaths?: boolean;
|
|
458
|
+
/**
|
|
459
|
+
* design/380 O9 — declared by the ADAPTER: content produced by THIS env (every FileSystem/Shell
|
|
460
|
+
* result — stdout, file bytes, listings, names) originates from a target OUTSIDE the deployment's
|
|
461
|
+
* trust boundary (a personal device, an unmanaged host). Omitted ⇒ historic behavior (trusted-side
|
|
462
|
+
* results). `true` feeds three EXISTING channels, no new vocabulary:
|
|
463
|
+
* - memory: the run's hand-tool results are treated as external-class content — the session mark
|
|
464
|
+
* (`memory.session_polluted`) + the design/336 origin carriage at harvest, via the same fold
|
|
465
|
+
* `execIsExternalContent` upgrades through (this flag ORs into that fold for the run);
|
|
466
|
+
* - env facts / prompt renderer: one declarative line disclosing the execution locus + trust posture;
|
|
467
|
+
* - audit: TaskResult/checkpoint carry it implicitly through WorkspaceHandle{provider, deviceId}.
|
|
468
|
+
* Suspend/resume: the run-level fold is PRESENCE-persisted on the checkpoint
|
|
469
|
+
* (`CheckpointState.externalContentTarget`, monotonic) and resume joins persisted OR live
|
|
470
|
+
* declaration — a run minted under a declaring adapter never silently downgrades on an
|
|
471
|
+
* undeclared/older one. A trust-posture declaration ONLY: no gate/policy/roster behavior reads it
|
|
472
|
+
* (the design/378 content-origin red line), and it is ORTHOGONAL to `capabilities.isolation` — an
|
|
473
|
+
* SSH env to an organization-managed build host can be non-isolated yet omit this flag.
|
|
474
|
+
*/
|
|
475
|
+
readonly externalContentTarget?: boolean;
|
|
458
476
|
}
|
|
459
477
|
/** Base fields shared by append-only session tree entries. */
|
|
460
478
|
export interface SessionTreeEntryBase {
|
package/dist/index.d.ts
CHANGED
|
@@ -263,7 +263,7 @@ export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } f
|
|
|
263
263
|
export { type BrainTimeoutConfig } from "./brain/timeout.js";
|
|
264
264
|
export { createAssistantMessageEventStream } from "./internal/llm.js";
|
|
265
265
|
export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
266
|
-
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
|
|
266
|
+
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
|
|
267
267
|
export { Type } from "typebox";
|
|
268
268
|
export type { TSchema, Static } from "typebox";
|
|
269
269
|
export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
|
|
@@ -192,6 +192,41 @@ export interface WorkflowRun {
|
|
|
192
192
|
spentTokens: number;
|
|
193
193
|
unsettledTokens?: number;
|
|
194
194
|
};
|
|
195
|
+
/** The run's TERMINAL total-timeout interruption — present ONLY when the `totalTimeoutMs` deadline had
|
|
196
|
+
* FIRED by the time this run reached its (failed) terminal, absent on every other run: one that finished
|
|
197
|
+
* inside its deadline, one that failed for its own reason before the deadline, and one that armed no
|
|
198
|
+
* deadline at all. Same ADDITIVE-observation contract as {@link agentFailures} and
|
|
199
|
+
* {@link budgetOvershoot} (never a gate input, never re-read by the engine, status enums untouched).
|
|
200
|
+
*
|
|
201
|
+
* WHY IT EXISTS: the deadline abandons the script body, so the run finalizes `failed` carrying the
|
|
202
|
+
* deadline's own message — and that is ALL any downstream face had to go on. A run whose agents had in
|
|
203
|
+
* fact answered (their outputs already on the record, inlined as `partial_results` on a poll) read as a
|
|
204
|
+
* total loss. This seat is the missing distinction: it states that the deadline is what ended the run,
|
|
205
|
+
* and what the run had ACHIEVED at that moment. Extending {@link WorkflowRunStatus} with a separate
|
|
206
|
+
* terminal word would say the same thing at the cost of a wire contract every consumer switches on, so
|
|
207
|
+
* the fact rides an optional member instead.
|
|
208
|
+
*
|
|
209
|
+
* `timeoutMs` is the deadline that fired (the run's configured `totalTimeoutMs`), so a reader can raise
|
|
210
|
+
* it deliberately rather than guess it.
|
|
211
|
+
*
|
|
212
|
+
* ⚠️ THE COUNTS ARE A POINT-IN-TIME SNAPSHOT, taken BEFORE the terminal fold that closes abandoned
|
|
213
|
+
* agents. That is the whole point (it reports what the deadline INTERRUPTED), and it is also why
|
|
214
|
+
* `agentsFailed` here is NOT {@link agentFailures}: the terminal fold marks every still-running agent
|
|
215
|
+
* `failed`, so the record's tally counts them and this member does not. The two are different questions
|
|
216
|
+
* about the same run — "how many had failed on their own when the clock ran out" versus "how many agent
|
|
217
|
+
* records ended failed" — and a consumer that swaps one for the other reports abandoned work as failed
|
|
218
|
+
* work. `agentsInFlight` counts records still `running` at that moment, which includes calls still
|
|
219
|
+
* QUEUED for a concurrency slot (they had not been refused, they had not finished).
|
|
220
|
+
*
|
|
221
|
+
* The seat is an OBJECT rather than a flat pair of members so a later disclosure about the same terminal
|
|
222
|
+
* (how much of the deadline was spent waiting on something outside the run, say) joins it additively with
|
|
223
|
+
* no shape change on any of the three faces. Nothing beyond the four members below is declared today. */
|
|
224
|
+
timeoutInterruption?: {
|
|
225
|
+
timeoutMs: number;
|
|
226
|
+
agentsCompleted: number;
|
|
227
|
+
agentsFailed: number;
|
|
228
|
+
agentsInFlight: number;
|
|
229
|
+
};
|
|
195
230
|
phases: WorkflowPhase[];
|
|
196
231
|
agents: WorkflowAgentRun[];
|
|
197
232
|
/** design/97 CORE-3: nested `ctx.workflow` sub-groups (the persisted group tree). Empty when the script
|
|
@@ -67,11 +67,11 @@ export class WorkflowMaxAgentsError extends Error {
|
|
|
67
67
|
`Add a hard iteration cap to the loop, or pass a token budget.`
|
|
68
68
|
: spentTokens !== undefined && spentTokens > budgetTotal
|
|
69
69
|
? `Workflow agent() call cap reached (${max}), and the token budget is ALREADY EXCEEDED ` +
|
|
70
|
-
`(${spentTokens.toLocaleString()} spent / ${budgetTotal.toLocaleString()}
|
|
70
|
+
`(${spentTokens.toLocaleString()} spent / ${budgetTotal.toLocaleString()} total tokens): agents already in flight when ` +
|
|
71
71
|
`the ceiling was reached are not bound by the per-call gate, so their spend landed on top of it. BOTH bounds are ` +
|
|
72
72
|
`binding — raising maxAgents alone would only buy more overshoot. Fan out over fewer items, or lower concurrency ` +
|
|
73
73
|
`(it bounds the overshoot) and raise the token budget deliberately.`
|
|
74
|
-
: `Workflow agent() call cap reached (${max}). A token budget IS set (${budgetTotal.toLocaleString()}
|
|
74
|
+
: `Workflow agent() call cap reached (${max}). A token budget IS set (${budgetTotal.toLocaleString()} total tokens), ` +
|
|
75
75
|
`so this is the CALL-COUNT cap, not the token ceiling: the script asked for more than ${max} agent() calls. ` +
|
|
76
76
|
`Fan out over fewer items, or raise maxAgents.`);
|
|
77
77
|
this.max = max;
|