@sema-agent/core 7.0.2 → 7.2.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 +58 -0
- package/dist/agents/cross-session-envelope.d.ts +138 -0
- package/dist/agents/cross-session-envelope.js +191 -0
- package/dist/agents/cross-session-judge.d.ts +119 -0
- package/dist/agents/cross-session-judge.js +184 -0
- package/dist/agents/cross-session-ref.d.ts +52 -0
- package/dist/agents/cross-session-ref.js +64 -0
- package/dist/agents/repair-loop.d.ts +8 -7
- package/dist/agents/roster-store.d.ts +7 -2
- package/dist/agents/send-message-tool.d.ts +13 -0
- package/dist/agents/send-message-tool.js +36 -12
- package/dist/brain/errors.d.ts +18 -0
- package/dist/brain/errors.js +3 -0
- package/dist/brain/stream-engine.js +6 -4
- package/dist/core/checkpoint-store.d.ts +189 -3
- package/dist/core/checkpoint-store.js +56 -16
- package/dist/core/context-edit.d.ts +3 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +34 -7
- package/dist/core/hooks.js +14 -8
- package/dist/core/image-downsample.d.ts +4 -3
- package/dist/core/permission-rule-consent.d.ts +72 -23
- package/dist/core/permission-rule-consent.js +115 -26
- package/dist/core/permission-rule-model.d.ts +245 -51
- package/dist/core/permission-rule-model.js +312 -54
- package/dist/core/permission-rule-org.js +13 -6
- package/dist/core/remote-env.d.ts +8 -1
- package/dist/core/roles.d.ts +30 -8
- package/dist/core/roles.js +12 -8
- package/dist/core/runner/assemble-result.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +41 -2
- package/dist/core/runner/prepare-task.js +353 -152
- package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
- package/dist/core/runner/prepare-workspace-restore.js +2 -1
- package/dist/core/runner/runtask.d.ts +12 -3
- package/dist/core/runner/runtask.js +45 -7
- package/dist/core/safety-axis-vocab.d.ts +1 -1
- package/dist/core/strategy-store.d.ts +4 -1
- package/dist/core/task-notification.d.ts +64 -5
- package/dist/core/task-notification.js +25 -4
- package/dist/core/task-registry-shared.d.ts +7 -3
- package/dist/core/tool-errors.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +51 -7
- package/dist/core/tool-policy.js +63 -9
- package/dist/core/types.d.ts +110 -9
- package/dist/core/untrusted-text.js +17 -1
- package/dist/engine/compaction/compaction.js +6 -2
- package/dist/engine/harness/agent-harness.d.ts +28 -6
- package/dist/engine/harness/agent-harness.js +34 -2
- package/dist/engine/harness/messages.js +4 -0
- package/dist/engine/harness/types.d.ts +37 -0
- package/dist/engine/harness/types.js +5 -0
- package/dist/engine/session/session.js +3 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/internal/harness.d.ts +1 -0
- package/dist/internal/harness.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +17 -9
- package/dist/orchestration/run-workflow-tool.js +7 -2
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +2 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +125 -1
|
@@ -32,7 +32,12 @@ export declare function remoteEnvFailureNote(op: RemoteEnvFailureNote["op"], err
|
|
|
32
32
|
* decides. A code outside the retryable family (`unsupported`, `auth_failed`) returns on the first
|
|
33
33
|
* attempt — `withRetry` will not spend a second call on a permanent refusal.
|
|
34
34
|
*/
|
|
35
|
-
export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions
|
|
35
|
+
export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions,
|
|
36
|
+
/** design/384 slice 2 (additive): observe each attempt AS IT STARTS. The returned `attempts` is
|
|
37
|
+
* only readable after settlement, and a caller whose wait on this promise is BOUNDED (the park
|
|
38
|
+
* compensation) must report the live attempt count when its bound fires mid-call — without this
|
|
39
|
+
* seat a deaf second attempt was reported as `attempts: 1`. */
|
|
40
|
+
onAttempt?: (attempt: number) => void): Promise<{
|
|
36
41
|
outcome: Awaited<ReturnType<RemoteExecutionEnv["resumeVM"]>>;
|
|
37
42
|
attempts: number;
|
|
38
43
|
}>;
|
|
@@ -19,10 +19,11 @@ export function remoteEnvFailureNote(op, error, attempts) {
|
|
|
19
19
|
}
|
|
20
20
|
const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
|
|
21
21
|
const REMOTE_RESTORE_BACKOFF_MS = 200;
|
|
22
|
-
export async function restoreWorkspaceWithRetry(env, snapshotId, options) {
|
|
22
|
+
export async function restoreWorkspaceWithRetry(env, snapshotId, options, onAttempt) {
|
|
23
23
|
let attempts = 0;
|
|
24
24
|
const outcome = await withRetry(async (attempt) => {
|
|
25
25
|
attempts = attempt;
|
|
26
|
+
onAttempt?.(attempt);
|
|
26
27
|
return env.resumeVM(snapshotId, options);
|
|
27
28
|
}, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
|
|
28
29
|
return { outcome, attempts };
|
|
@@ -147,6 +147,10 @@ export declare function awaitChargeWithSlowDisclosure<T>(charge: Promise<T>, onS
|
|
|
147
147
|
* already hold would be a fabricated stall. The second pass is what makes the loop terminate.
|
|
148
148
|
*/
|
|
149
149
|
export declare function raceUntilDeadline<T>(p: Promise<T>, deadline: number): Promise<T | typeof GOVERNANCE_READ_STALLED>;
|
|
150
|
+
/**
|
|
151
|
+
* A stateless task runner. Holds shared deps (the external brain, model catalog) and an
|
|
152
|
+
* in-memory session store so that passing a `sessionId` continues a prior conversation.
|
|
153
|
+
*/
|
|
150
154
|
export declare class Runner {
|
|
151
155
|
private deps;
|
|
152
156
|
readonly sessions: SessionStore;
|
|
@@ -223,9 +227,14 @@ export declare class Runner {
|
|
|
223
227
|
/**
|
|
224
228
|
* Hot-swap the model catalog (and optionally the tier bindings) without restarting the process or
|
|
225
229
|
* rebuilding the Runner — the deployment seat that makes "switching models" a zero-restart
|
|
226
|
-
* operation
|
|
227
|
-
*
|
|
228
|
-
*
|
|
230
|
+
* operation, and the ONLY sanctioned generation change.
|
|
231
|
+
*
|
|
232
|
+
* (Precisely: the constructor runs {@link expandTiers} once and keeps a private expanded copy **only
|
|
233
|
+
* when `RunnerDeps.tiers` is configured** — that is the arm where mutating the shared table after
|
|
234
|
+
* construction provably never took effect. A tiers-less deployment's Runner holds the caller's own
|
|
235
|
+
* `models` object BY REFERENCE, so mutating it after construction does leak through; that is an
|
|
236
|
+
* accident of the expansion being a no-op, not a contract, and this verb is still the supported way
|
|
237
|
+
* to change a generation — it is what validates, announces, and computes the pairing disclosure.)
|
|
229
238
|
*
|
|
230
239
|
* Semantics:
|
|
231
240
|
* - **Atomic**: the candidate catalog is tier-expanded and validated FIRST (an illegal tier
|
|
@@ -4,7 +4,7 @@ import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
|
|
|
4
4
|
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
5
5
|
import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
6
6
|
import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
|
|
7
|
-
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
7
|
+
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, isSyntheticApiErrorMessage, uuidv7 } from "../../internal/harness.js";
|
|
8
8
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
9
9
|
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
10
10
|
import { GIT_STATUS_ECHO_PREVIEW, branchCarriesVisiblePositiveGitFrame, newestEngineGitFrame, stripGitStatusUnits } from "./git-status-frame.js";
|
|
@@ -2166,8 +2166,21 @@ export class Runner {
|
|
|
2166
2166
|
const apply = () => {
|
|
2167
2167
|
const abortOwnedBeforeHalt = h.abortController.signal.aborted;
|
|
2168
2168
|
const receipt = h.harness.halt();
|
|
2169
|
-
if (receipt.accepted && !abortOwnedBeforeHalt)
|
|
2169
|
+
if (receipt.accepted && !abortOwnedBeforeHalt) {
|
|
2170
2170
|
h.loop.userHalted = true;
|
|
2171
|
+
}
|
|
2172
|
+
else {
|
|
2173
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
2174
|
+
code: "task.halt_unconsumed",
|
|
2175
|
+
message: "a user halt arrived while the run was already ending for its own reason: nothing was cut or stopped " +
|
|
2176
|
+
"by it — the run's own ending stands, and the result will not carry haltedByUser for this halt.",
|
|
2177
|
+
detail: {
|
|
2178
|
+
sessionId: h.sessionId,
|
|
2179
|
+
runId: h.runId,
|
|
2180
|
+
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
2181
|
+
},
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2171
2184
|
if (receipt.turnCut) {
|
|
2172
2185
|
deliverEngineNotice(this.deps.onNotice, {
|
|
2173
2186
|
code: "task.turn_interrupted",
|
|
@@ -2861,6 +2874,7 @@ export class Runner {
|
|
|
2861
2874
|
...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
|
|
2862
2875
|
...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
|
|
2863
2876
|
...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
|
|
2877
|
+
...(s.errorStatus !== undefined ? { errorStatus: s.errorStatus } : {}),
|
|
2864
2878
|
...ident(),
|
|
2865
2879
|
};
|
|
2866
2880
|
Object.freeze(frame);
|
|
@@ -4317,7 +4331,21 @@ export class Runner {
|
|
|
4317
4331
|
model = resolved.model;
|
|
4318
4332
|
thinking = resolved.thinking;
|
|
4319
4333
|
}
|
|
4320
|
-
catch {
|
|
4334
|
+
catch (roleErr) {
|
|
4335
|
+
if (cfg.role !== undefined) {
|
|
4336
|
+
let asked;
|
|
4337
|
+
try {
|
|
4338
|
+
asked = String(cfg.role);
|
|
4339
|
+
}
|
|
4340
|
+
catch {
|
|
4341
|
+
asked = `<unrenderable ${typeof cfg.role}>`;
|
|
4342
|
+
}
|
|
4343
|
+
try {
|
|
4344
|
+
this.deps.onError?.(new Error(`suggestNextPrompts.role ${JSON.stringify(asked.length > 80 ? `${asked.slice(0, 80)}…` : asked)} did not resolve to a model — the prompt-suggestion pass ran on the task's own model (${prepared.model.id}) instead. Configure that role in RunnerDeps.roles / TaskSpec.roles, or omit the field to use the "summarize" role.`, { cause: roleErr }), { phase: "suggestions", sessionId: prepared.sessionId });
|
|
4345
|
+
}
|
|
4346
|
+
catch {
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4321
4349
|
}
|
|
4322
4350
|
if (!sameRouteIdentity(model, prepared.model)) {
|
|
4323
4351
|
const verdict = await adjudicateDerivedRoute({ brain: this.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
|
|
@@ -4329,7 +4357,7 @@ export class Runner {
|
|
|
4329
4357
|
}
|
|
4330
4358
|
const pricing = this.deps.pricing?.[model.id] ?? modelCostToPricing(model.cost);
|
|
4331
4359
|
const ctx = await prepared.session.buildContext();
|
|
4332
|
-
const transcript = ctx.messages.slice(-SUGGESTIONS_TRANSCRIPT_MESSAGES);
|
|
4360
|
+
const transcript = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m)).slice(-SUGGESTIONS_TRANSCRIPT_MESSAGES);
|
|
4333
4361
|
const out = await generatePromptSuggestions({ brain: this.deps.brain, model, pricing, thinking, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, signal: ac.signal }, transcript, count);
|
|
4334
4362
|
if (out.tokens > 0 || out.costMicroUsd > 0) {
|
|
4335
4363
|
result.stats.suggestions = { tokens: out.tokens, costMicroUsd: out.costMicroUsd };
|
|
@@ -5052,12 +5080,22 @@ export class Runner {
|
|
|
5052
5080
|
await recheckGovernanceWindow();
|
|
5053
5081
|
}
|
|
5054
5082
|
this.locallyClaimedTokens.add(token);
|
|
5055
|
-
|
|
5083
|
+
let won;
|
|
5084
|
+
let claimLossStatus;
|
|
5085
|
+
if (store.claimTerminal !== undefined) {
|
|
5086
|
+
const claim = await store.claimTerminal(token, cp.scope, { kind: "resolve", outcome: outcomeForStore, expect: { rev: cp.rev ?? 0 } });
|
|
5087
|
+
won = claim.claimed;
|
|
5088
|
+
if (!claim.claimed)
|
|
5089
|
+
claimLossStatus = claim.current.status;
|
|
5090
|
+
}
|
|
5091
|
+
else {
|
|
5092
|
+
won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
5093
|
+
}
|
|
5056
5094
|
if (!won)
|
|
5057
5095
|
this.locallyClaimedTokens.delete(token);
|
|
5058
5096
|
if (!won) {
|
|
5059
|
-
const
|
|
5060
|
-
if (
|
|
5097
|
+
const stillPending = claimLossStatus !== undefined ? claimLossStatus === "pending" : (await store.get(token))?.status === "pending";
|
|
5098
|
+
if (stillPending) {
|
|
5061
5099
|
throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (its revision advanced via a resolve/reopen cycle since this resume validated) — not executed; re-resume against the current state");
|
|
5062
5100
|
}
|
|
5063
5101
|
this.parentConstraintRegistry.delete(token);
|
|
@@ -25,7 +25,7 @@ export declare const SAFETY_AXIS_VOCABULARY: {
|
|
|
25
25
|
readonly safetyAxis: readonly ["egress", "irreversible", "shell"];
|
|
26
26
|
/** `RiskDescriptor.severity` — ToolEmu-style tier (5 = most severe), the inbox triage key (checkpoint-store.ts:85). */
|
|
27
27
|
readonly severity: readonly [1, 2, 3, 4, 5];
|
|
28
|
-
/** `TaskSpec.shellGate` — deployment shell-command gate rank, `off` < `classify` < `always
|
|
28
|
+
/** `TaskSpec.shellGate` (declared in core/types.ts) — deployment shell-command gate rank, `off` < `classify` < `always`. */
|
|
29
29
|
readonly shellGate: readonly ["off", "classify", "always"];
|
|
30
30
|
/** `ToolPolicy` decision — per-tool adjudication; `deny` short-circuits the `deny > ask > allow` fold (tool-policy.ts). */
|
|
31
31
|
readonly permissionDecision: readonly ["allow", "ask", "deny"];
|
|
@@ -28,7 +28,10 @@ export interface StoredStrategy {
|
|
|
28
28
|
scope: string;
|
|
29
29
|
/** ISO timestamp stored. */
|
|
30
30
|
ts: string;
|
|
31
|
-
/**
|
|
31
|
+
/** What the teacher leg was ADDRESSED as, for future staleness handling: the caller's word verbatim
|
|
32
|
+
* when the teacher was configured by string (a catalog key, tier word or CC alias), else the
|
|
33
|
+
* `Model.id` of the object it was configured with. NOT normalized to a served model id — two rows
|
|
34
|
+
* written by the same physical model under different spellings do not compare equal. */
|
|
32
35
|
teacherModel?: string;
|
|
33
36
|
/** Reserved for a future generalized signature (v3 semantic matching). */
|
|
34
37
|
signature?: string;
|
|
@@ -140,6 +140,38 @@ export interface TaskNotificationPayload {
|
|
|
140
140
|
peer?: {
|
|
141
141
|
hopChain: string[];
|
|
142
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* design/385 §1.4 d1 — the delegated-child → parent UPLINK carrier (`SendMessage("main")`). Its
|
|
145
|
+
* PRESENCE is the render discriminator: the frame reaches the parent's model as a top-level
|
|
146
|
+
* `<agent-message from="…">` user frame (the same-process lane's carrier, distinct from both the
|
|
147
|
+
* `<task-notification>` shell and the cross-session envelope) followed by the peer discipline block.
|
|
148
|
+
* `body` is the child's message as bounded by the producer (no discipline block inside it — the
|
|
149
|
+
* block is frame-adjacent by rule). The classic members (`summary`/`result`) stay filled beside it
|
|
150
|
+
* for wire consumers that project the frame as a card; they are not what the model reads.
|
|
151
|
+
* Minted ONLY by the SendMessage uplink leg (engine-side); an external `notify()` cannot wear it.
|
|
152
|
+
*/
|
|
153
|
+
agentMessage?: {
|
|
154
|
+
from: string;
|
|
155
|
+
body: string;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* design/385 §1.4 d1 — the engine-minted PROVENANCE side record of an agent-message frame, a typed
|
|
159
|
+
* key (never model text) so a host can attribute and correlate the injection on the wire:
|
|
160
|
+
* `kind` names the lane, `from` is the sender label the frame's attribute spells, `taskId` the
|
|
161
|
+
* sender's run/agent id, `seq` the producer's per-frame counter (= this payload's `seq`),
|
|
162
|
+
* `agentType` the sender's resolved agent type when the producer knows it. Present exactly when
|
|
163
|
+
* {@link agentMessage} is.
|
|
164
|
+
*/
|
|
165
|
+
_sema_provenance?: SemaProvenance;
|
|
166
|
+
}
|
|
167
|
+
/** design/385 §1.4 d1 — see {@link TaskNotificationPayload._sema_provenance}. `kind` is a closed set
|
|
168
|
+
* with one member today; a future lane adds a member, never a second key. */
|
|
169
|
+
export interface SemaProvenance {
|
|
170
|
+
kind: "agent_message";
|
|
171
|
+
from: string;
|
|
172
|
+
taskId: string;
|
|
173
|
+
seq: number;
|
|
174
|
+
agentType?: string;
|
|
143
175
|
}
|
|
144
176
|
/**
|
|
145
177
|
* design/144 §2 — the caller-facing input of `TaskStream.notify()`: a STRUCTURED external event to inject
|
|
@@ -229,6 +261,23 @@ export declare function attrEscape(value: string): string;
|
|
|
229
261
|
/** Max rendered length of an untrusted attribution label (the external `from="…"` header and the
|
|
230
262
|
* design/171 speaker envelope share it — same concern: a display label, not a payload). */
|
|
231
263
|
export declare const EXTERNAL_SOURCE_MAX = 120;
|
|
264
|
+
/** design/385 §1.4 d1 — the same-process lane's carrier tag (CC `iTe`). */
|
|
265
|
+
export declare const AGENT_MESSAGE_TAG = "agent-message";
|
|
266
|
+
/**
|
|
267
|
+
* design/385 §1.4 d1 — render the child → parent uplink as CC's same-process form: a top-level
|
|
268
|
+
* `<agent-message from="…">` frame (CC `ZSe`: attribute-escaped sender, nested-tag-neutralized body,
|
|
269
|
+
* no header prose — the attribution IS the attribute) followed by the peer discipline block OUTSIDE
|
|
270
|
+
* the frame (design/176 §3.4 placement: a sender-embedded copy inside the body arrives neutralized,
|
|
271
|
+
* so position distinguishes the real block). The body first passes the harness AUTHORITY-family
|
|
272
|
+
* neutralization ({@link neutralizePeerBody}: every engine envelope a model reads as harness speech —
|
|
273
|
+
* `task-notification`, `user_memory`, `skills`, … — not the reminder tag alone), because the child's
|
|
274
|
+
* text is model output; the pre-carrier `<task-notification>` shell entity-escaped every byte of it,
|
|
275
|
+
* and the carrier form must contain at least as much.
|
|
276
|
+
*/
|
|
277
|
+
export declare function renderAgentMessageFrame(frame: {
|
|
278
|
+
from: string;
|
|
279
|
+
body: string;
|
|
280
|
+
}): string;
|
|
232
281
|
export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
|
|
233
282
|
/**
|
|
234
283
|
* The BETWEEN-TURNS pending lane. A task notification born while NO turn is
|
|
@@ -341,11 +390,21 @@ export declare class PendingSessionNotifications {
|
|
|
341
390
|
get size(): number;
|
|
342
391
|
}
|
|
343
392
|
/**
|
|
344
|
-
* Delivery-side overflow disclosure: fold the per-task drop counts into the drained payloads.
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
393
|
+
* Delivery-side overflow disclosure: fold the per-task drop counts into the drained payloads. Two
|
|
394
|
+
* carrier shapes, chosen per survivor:
|
|
395
|
+
* · a CLASSIC survivor (renders through the `<task-notification>` shell) is annotated IN PLACE — the
|
|
396
|
+
* first surviving payload of a task that lost events gets a `[task_id]`-prefixed disclosure line
|
|
397
|
+
* prepended to its summary (backgroundTasks 同规: the id keeps the loss addressable via TaskOutput);
|
|
398
|
+
* · an AGENT-MESSAGE survivor (design/385 §1.4 d1; renders as the `<agent-message>` carrier, its
|
|
399
|
+
* `summary` wire-only) is left byte-unchanged, and the disclosure rides as a SEPARATE engine-authored
|
|
400
|
+
* `event` payload inserted immediately AHEAD of it (engine speech never goes inside a peer frame;
|
|
401
|
+
* peer speech never goes inside the authority shell that line renders through).
|
|
402
|
+
* The returned array is therefore NOT a 1:1 image of `drained.items`: it can be longer (one inserted
|
|
403
|
+
* line per disclosed agent-message lane, plus the per-lane "nothing survived" lines and the
|
|
404
|
+
* whole-session line below) — consume it by iteration, never by index-pairing against `items`. The
|
|
405
|
+
* zero-disclosure path returns `drained.items` itself (identity, no allocation). A task whose EVERY
|
|
406
|
+
* pending item was evicted still gets one honest synthetic `event` payload saying so — a fully silent
|
|
407
|
+
* loss is never allowed.
|
|
349
408
|
*/
|
|
350
409
|
export declare function discloseDroppedPending(drained: DrainedPendingNotifications): TaskNotificationPayload[];
|
|
351
410
|
export declare class SystemInjectionQueue<TPayload = TaskNotificationPayload> {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
1
|
+
import { escapeEnvelopeTag, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
2
|
+
import { PEER_MESSAGE_NOTICE } from "../agents/peer-admission.js";
|
|
3
|
+
import { neutralizePeerBody } from "../agents/cross-session-envelope.js";
|
|
2
4
|
export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
|
|
3
5
|
export function isSystemInjectionPriority(value) {
|
|
4
6
|
return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
|
|
@@ -46,7 +48,14 @@ export function attrEscape(value) {
|
|
|
46
48
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
47
49
|
}
|
|
48
50
|
export const EXTERNAL_SOURCE_MAX = 120;
|
|
51
|
+
export const AGENT_MESSAGE_TAG = "agent-message";
|
|
52
|
+
export function renderAgentMessageFrame(frame) {
|
|
53
|
+
const body = escapeEnvelopeTag(AGENT_MESSAGE_TAG, neutralizePeerBody(frame.body));
|
|
54
|
+
return `<${AGENT_MESSAGE_TAG} from="${attrEscape(frame.from)}">\n${body}\n</${AGENT_MESSAGE_TAG}>\n\n${PEER_MESSAGE_NOTICE}`;
|
|
55
|
+
}
|
|
49
56
|
export function renderTaskNotificationXml(n) {
|
|
57
|
+
if (n.agentMessage !== undefined)
|
|
58
|
+
return renderAgentMessageFrame(n.agentMessage);
|
|
50
59
|
const usage = n.usage === undefined
|
|
51
60
|
? undefined
|
|
52
61
|
: (() => {
|
|
@@ -196,12 +205,24 @@ export function discloseDroppedPending(drained) {
|
|
|
196
205
|
if (drained.dropped.size === 0)
|
|
197
206
|
return [...drained.items, ...sessionLine];
|
|
198
207
|
const disclosed = new Set();
|
|
199
|
-
const out = drained.items.
|
|
208
|
+
const out = drained.items.flatMap((n) => {
|
|
200
209
|
const lane = taskNotificationLaneKey(n);
|
|
201
210
|
const dropped = drained.dropped.get(lane);
|
|
202
211
|
if (dropped === undefined || disclosed.has(lane))
|
|
203
|
-
return n;
|
|
212
|
+
return [n];
|
|
204
213
|
disclosed.add(lane);
|
|
214
|
+
if (n.agentMessage !== undefined) {
|
|
215
|
+
const line = {
|
|
216
|
+
task_id: n.task_id,
|
|
217
|
+
task_type: n.task_type,
|
|
218
|
+
status: "event",
|
|
219
|
+
summary: `[${n.task_id}] ${dropped.count} earlier pending notification(s) from this task were dropped (pending-queue overflow); its latest message follows.`,
|
|
220
|
+
};
|
|
221
|
+
const linePriority = drained.priorities?.get(n);
|
|
222
|
+
if (linePriority !== undefined)
|
|
223
|
+
drained.priorities?.set(line, linePriority);
|
|
224
|
+
return [line, n];
|
|
225
|
+
}
|
|
205
226
|
const annotated = {
|
|
206
227
|
...n,
|
|
207
228
|
summary: `[${n.task_id}] ${dropped.count} earlier pending notification(s) from this task were dropped (pending-queue overflow). ${n.summary}`,
|
|
@@ -209,7 +230,7 @@ export function discloseDroppedPending(drained) {
|
|
|
209
230
|
const priority = drained.priorities?.get(n);
|
|
210
231
|
if (priority !== undefined)
|
|
211
232
|
drained.priorities?.set(annotated, priority);
|
|
212
|
-
return annotated;
|
|
233
|
+
return [annotated];
|
|
213
234
|
});
|
|
214
235
|
for (const [lane, dropped] of drained.dropped) {
|
|
215
236
|
if (disclosed.has(lane))
|
|
@@ -874,9 +874,13 @@ export interface RegisterBackgroundAgentInput extends TaskAccess {
|
|
|
874
874
|
* internals chain; equals parentSessionId at depth 1). Persisted on the handle, the durable row
|
|
875
875
|
* and the roster so recovery faces enumerate the whole tree under the root without alias walks. */
|
|
876
876
|
rootSessionId?: string;
|
|
877
|
-
/** design/151 S3b — revival lookup keys (CC meta-sidecar shape): the
|
|
878
|
-
*
|
|
879
|
-
* named-teammate spawn would. Lookup keys only, never a serialized spec.
|
|
877
|
+
/** design/151 S3b — revival lookup keys (CC meta-sidecar shape): the model RECORD KEY and the team
|
|
878
|
+
* name, persisted on the durable row so a tier-3 revival rebuilds the spec the way a fresh
|
|
879
|
+
* named-teammate spawn would. Lookup keys only, never a serialized spec. `model` is NOT the
|
|
880
|
+
* resolved model id: a spawn that named a model in WORDS records the caller's spelling verbatim
|
|
881
|
+
* (catalog key / tier word / CC alias) so the revival can re-resolve it against the catalog in
|
|
882
|
+
* force at WAKE time; only a spawn that carried a Model OBJECT records that object's id. Same
|
|
883
|
+
* value and same rule as the roster row's `model` column. */
|
|
880
884
|
model?: string;
|
|
881
885
|
teamName?: string;
|
|
882
886
|
toolUseId?: string;
|
|
@@ -40,7 +40,7 @@ export declare function formatToolError(error: unknown): string;
|
|
|
40
40
|
* D-G data contract; the WorkerReport "errorClass" small-slice). A terminal `errorCode` (1.37+) is a
|
|
41
41
|
* **dotted namespace** (`limits.max_tokens_exceeded` / `limits.max_cost_exceeded` / `output.invalid` …)
|
|
42
42
|
* so a consumer can prefix-match a whole CLASS — but every aggregator/consumer hand-rolls
|
|
43
|
-
* `errorCode.startsWith("budget.")`
|
|
43
|
+
* `errorCode.startsWith("budget.")` — a prefix convention no declaration site records, which silently
|
|
44
44
|
* mis-classifies any code that does NOT follow the convention and drifts as new prefixes are added.
|
|
45
45
|
* This is the single shared folder.
|
|
46
46
|
*
|
|
@@ -341,7 +341,8 @@ export declare function decisionText(d: PermissionResult): string | undefined;
|
|
|
341
341
|
*
|
|
342
342
|
* `check` may be async, which is also how **human-in-the-loop approval** works: a deployment can
|
|
343
343
|
* hold the promise open until an operator approves/denies. `signal` fires when the task aborts
|
|
344
|
-
* (timeout / max turns / cancel)
|
|
344
|
+
* (timeout / max turns / cancel) and, since design/384, when the asking turn is interrupted (a bare
|
|
345
|
+
* user halt / steer-now boundary cut) — honor it to release a pending approval instead of hanging (F4).
|
|
345
346
|
* The Runner also races `check` against `signal` itself, so a policy that ignores it still cannot
|
|
346
347
|
* hang the worker past the deadline; passing it through just lets you clean up the wait early.
|
|
347
348
|
*/
|
|
@@ -725,8 +726,9 @@ export declare function createApprovalPolicy(opts: {
|
|
|
725
726
|
requireApproval: string[];
|
|
726
727
|
/**
|
|
727
728
|
* The approval decision (e.g. await an operator). Resolve true to allow, false to deny. `signal`
|
|
728
|
-
* fires when the task aborts —
|
|
729
|
-
* never-answered request is released at
|
|
729
|
+
* fires when the task aborts — and, since design/384, when the asking turn is interrupted — race
|
|
730
|
+
* your wait against it (e.g. an OA approval callback) so a never-answered request is released at
|
|
731
|
+
* the deadline rather than holding the worker.
|
|
730
732
|
*/
|
|
731
733
|
approve: (req: ToolCallRequest, signal?: AbortSignal) => boolean | Promise<boolean>;
|
|
732
734
|
/** Always-denied tools. */
|
|
@@ -942,7 +944,11 @@ export interface AskDelegationProvenance {
|
|
|
942
944
|
* never adjudication input — same posture as {@link AskRequest.sourceAgentName}. */
|
|
943
945
|
readonly agentName?: string;
|
|
944
946
|
}
|
|
945
|
-
/** The structured context an `onAsk` approver receives for an `ask` decision (design/37).
|
|
947
|
+
/** The structured context an `onAsk` approver receives for an `ask` decision (design/37).
|
|
948
|
+
* Lifecycle (design/384): the wait this request fronts can be released by the run's abort AND by a
|
|
949
|
+
* turn-level interrupt; after that release the engine no longer awaits the approver, so anything an
|
|
950
|
+
* approver retains off this object it must release itself, keyed on its `signal` argument's abort
|
|
951
|
+
* (see {@link OnAsk} for the full detached-settlement contract). */
|
|
946
952
|
export interface AskRequest {
|
|
947
953
|
toolName: string;
|
|
948
954
|
/** #144: a persisted allow rule MATCHED this call but could not clear the ask (mandated — see
|
|
@@ -1052,6 +1058,17 @@ export interface AskRequest {
|
|
|
1052
1058
|
* from it across a coverage change — harmlessly, since the record is what gets confirmed.
|
|
1053
1059
|
*/
|
|
1054
1060
|
readonly ruleOffers?: readonly import("./permission-rule-model.js").RuleOffer[];
|
|
1061
|
+
/**
|
|
1062
|
+
* design/382 §2.4 (adversarial-review r3, additive) — the RELATIVE-CD RESOLUTION BASE the offers
|
|
1063
|
+
* above were minted with: the live tracked working directory at adjudication time (the RB-108
|
|
1064
|
+
* value; equal to the task root until an observable `cd` moves the tracker). Present only beside
|
|
1065
|
+
* {@link ruleOffers} when the run has a tracked cwd. A consumer preparing the AUTHORITATIVE
|
|
1066
|
+
* consent record threads it as `prepareCardApproval`'s `execCwd`, so the record re-mints the SAME
|
|
1067
|
+
* directory member this projection displayed — without it, a moved tracker would make the record
|
|
1068
|
+
* resolve `cd ./x` from the task root and persist a rule for a directory the command never
|
|
1069
|
+
* enters. Display/reconstruction context only, never adjudication input.
|
|
1070
|
+
*/
|
|
1071
|
+
readonly execCwd?: string;
|
|
1055
1072
|
/**
|
|
1056
1073
|
* #490 修② — WHY {@link ruleOffers} is absent, when the rule-offer lane is in play and has nothing
|
|
1057
1074
|
* to give. A CLOSED set, mutually exclusive with {@link ruleOffers} (never both, never neither once
|
|
@@ -1212,7 +1229,16 @@ export interface AskRequest {
|
|
|
1212
1229
|
* deterministically to `deny` with a model-readable reason. The safe default for stateless automation.
|
|
1213
1230
|
* - `"allow"` — auto-approve every `ask` (e.g. a trusted batch run).
|
|
1214
1231
|
* - a function — await an operator's decision (true=allow, false=deny). `signal` fires when the task
|
|
1215
|
-
* aborts
|
|
1232
|
+
* aborts AND (design/384, when the gate threaded a per-call signal) when the asking turn is
|
|
1233
|
+
* interrupted — a bare user halt or a steer-now boundary cut releases the ask exactly as the run's
|
|
1234
|
+
* own end does; race your wait against it so an unanswered ask is released at the deadline, not hung.
|
|
1235
|
+
* Since design/384 the engine no longer waits for you after the signal fires: `resolveAsk` races its
|
|
1236
|
+
* await against the signal and settles the gate as an abort-family deny on its own. Your still-pending
|
|
1237
|
+
* promise is DETACHED — a resolve the released wait never consumes is you releasing your wait, never
|
|
1238
|
+
* a verdict (an unconsumed approval is disclosed as a notice, not honored — no claim is made about
|
|
1239
|
+
* which settled first); an unconsumed reject is disclosed on the deployment's error face, never
|
|
1240
|
+
* silently swallowed. Cleanup of anything you hold for the wait (the request
|
|
1241
|
+
* payload, the signal, your own timers) is YOUR responsibility, keyed on the signal's abort.
|
|
1216
1242
|
* G1 three-value: the function may also return `"unavailable"` — an affirmative "no operator
|
|
1217
1243
|
* is reachable for THIS ask right now" (judged PER-ASK inside the callback, not at wire time). It is a
|
|
1218
1244
|
* ROUTING verdict, not a decision: the gate re-routes the ask onto the durable park leg (same behavior
|
|
@@ -1371,7 +1397,10 @@ export declare function describeThrown(err: unknown): string;
|
|
|
1371
1397
|
* - `"blanket_allow_refused"` — a blanket allow posture met a `requiresRealApproval` ask;
|
|
1372
1398
|
* - `"approver_unavailable"` — the approver answered the ROUTING question "nobody reachable"
|
|
1373
1399
|
* (the G1 marker's fail-closed carry — the gate may re-route it to a durable park instead);
|
|
1374
|
-
* - `"task_aborted"` — the
|
|
1400
|
+
* - `"task_aborted"` — the wait's abort signal ended it (pre-wait, mid-wait and race arms). The
|
|
1401
|
+
* signal is the run's own end AND, since design/384, any turn-level interrupt composed into the
|
|
1402
|
+
* wait (a bare user halt, a steer-now boundary cut): one abort family, one word — a consumer
|
|
1403
|
+
* that must tell the sources apart reads the run's own terminal facts, not this classification;
|
|
1375
1404
|
* - `"presentation_failed"` — the args/edit could not be safely presented or adopted (unclonable);
|
|
1376
1405
|
* - `"approver_error"` — the approver callback threw;
|
|
1377
1406
|
* - `"approver_contract"` — the approver returned something outside the contract (non-boolean
|
|
@@ -1466,5 +1495,20 @@ export declare function carriesBidiControls(value: unknown, limits?: {
|
|
|
1466
1495
|
* headless auto-deny — carry NO source on purpose: nobody was asked, so there is no wait for anyone to
|
|
1467
1496
|
* have ended, and `decisionReason: "mode"` is already the honest word for what produced them.
|
|
1468
1497
|
*/
|
|
1469
|
-
export declare function resolveAsk(req: AskRequest, onAsk: OnAsk | undefined, signal?: AbortSignal
|
|
1498
|
+
export declare function resolveAsk(req: AskRequest, onAsk: OnAsk | undefined, signal?: AbortSignal,
|
|
1499
|
+
/** design/384 — observer for a DETACHED approver's settlement, consulted only after the race
|
|
1500
|
+
* arm released the wait on `signal`'s abort: `"approve"` = a value that reads as an approval
|
|
1501
|
+
* went unconsumed by the released wait (a person said yes to an action that will never run —
|
|
1502
|
+
* the caller turns this into its own disclosure, e.g. an engine notice; no arrival-order claim
|
|
1503
|
+
* is made); `"error"` = the detached promise
|
|
1504
|
+
* rejected, or its members threw on the post-release read (the caller's error face, never
|
|
1505
|
+
* silence). An unconsumed NON-approve resolve is the approver releasing its wait — no verdict,
|
|
1506
|
+
* no record, this observer is not consulted. Optional and advisory: absent, the detached
|
|
1507
|
+
* settlement is still swallow-guarded (no unhandled rejection), it just leaves no trace. */
|
|
1508
|
+
onLateSettlement?: (late: {
|
|
1509
|
+
kind: "approve";
|
|
1510
|
+
} | {
|
|
1511
|
+
kind: "error";
|
|
1512
|
+
error: unknown;
|
|
1513
|
+
}) => void): Promise<ResolvedAsk>;
|
|
1470
1514
|
export {};
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -314,7 +314,7 @@ export function createApprovalPolicy(opts) {
|
|
|
314
314
|
}
|
|
315
315
|
if (need.has(toolName) || namespacedCoveringHit(needCovering, toolName)) {
|
|
316
316
|
if (signal?.aborted) {
|
|
317
|
-
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (
|
|
317
|
+
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, settledBy: "aborted" }, "task_aborted", req);
|
|
318
318
|
}
|
|
319
319
|
let ok;
|
|
320
320
|
try {
|
|
@@ -336,7 +336,7 @@ export function createApprovalPolicy(opts) {
|
|
|
336
336
|
}, "window_expired", req);
|
|
337
337
|
}
|
|
338
338
|
if (signal?.aborted) {
|
|
339
|
-
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (
|
|
339
|
+
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, settledBy: "aborted" }, "task_aborted", req);
|
|
340
340
|
}
|
|
341
341
|
const okRaw = ok;
|
|
342
342
|
if (okRaw === true)
|
|
@@ -1003,13 +1003,58 @@ export function carriesBidiControls(value, limits) {
|
|
|
1003
1003
|
return false;
|
|
1004
1004
|
}
|
|
1005
1005
|
}
|
|
1006
|
-
export async function resolveAsk(req, onAsk, signal) {
|
|
1007
|
-
const r = await resolveAskArms(req, onAsk, signal);
|
|
1006
|
+
export async function resolveAsk(req, onAsk, signal, onLateSettlement) {
|
|
1007
|
+
const r = await resolveAskArms(req, onAsk, signal, onLateSettlement);
|
|
1008
1008
|
if (r.action === "deny" && isAskDenyResolution(r.resolution))
|
|
1009
1009
|
return withCoreMintedResolution(r, r.resolution, req);
|
|
1010
1010
|
return r;
|
|
1011
1011
|
}
|
|
1012
|
-
|
|
1012
|
+
function raceAskWaitAgainstSignal(wait, signal) {
|
|
1013
|
+
const settledWait = wait.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
|
|
1014
|
+
if (signal === undefined)
|
|
1015
|
+
return settledWait;
|
|
1016
|
+
if (signal.aborted)
|
|
1017
|
+
return Promise.resolve({ tag: "aborted" });
|
|
1018
|
+
return new Promise((resolve) => {
|
|
1019
|
+
let settled = false;
|
|
1020
|
+
const finish = (r) => {
|
|
1021
|
+
if (settled)
|
|
1022
|
+
return;
|
|
1023
|
+
settled = true;
|
|
1024
|
+
signal.removeEventListener("abort", onAbort);
|
|
1025
|
+
resolve(r);
|
|
1026
|
+
};
|
|
1027
|
+
const onAbort = () => finish({ tag: "aborted" });
|
|
1028
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1029
|
+
void settledWait.then(finish);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
function detachLateAskWait(wait, onLate) {
|
|
1033
|
+
const disclose = (late) => {
|
|
1034
|
+
try {
|
|
1035
|
+
onLate?.(late);
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
void wait
|
|
1041
|
+
.then((late) => {
|
|
1042
|
+
let approved = false;
|
|
1043
|
+
try {
|
|
1044
|
+
approved = late === true || (typeof late === "object" && late !== null && late.allow === true);
|
|
1045
|
+
}
|
|
1046
|
+
catch (err) {
|
|
1047
|
+
disclose({ kind: "error", error: err });
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (approved)
|
|
1051
|
+
disclose({ kind: "approve" });
|
|
1052
|
+
}, (error) => {
|
|
1053
|
+
disclose({ kind: "error", error });
|
|
1054
|
+
})
|
|
1055
|
+
.catch(() => undefined);
|
|
1056
|
+
}
|
|
1057
|
+
async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
1013
1058
|
if (onAsk === "allow") {
|
|
1014
1059
|
if (req.requiresRealApproval === true) {
|
|
1015
1060
|
return {
|
|
@@ -1032,7 +1077,7 @@ async function resolveAskArms(req, onAsk, signal) {
|
|
|
1032
1077
|
};
|
|
1033
1078
|
}
|
|
1034
1079
|
if (signal?.aborted) {
|
|
1035
|
-
return { action: "deny", message: `approval aborted for "${req.toolName}" (
|
|
1080
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, decisionReason: "mode",
|
|
1036
1081
|
resolution: "task_aborted", settledBy: "aborted" };
|
|
1037
1082
|
}
|
|
1038
1083
|
const presented = tryCloneArgs(req.args);
|
|
@@ -1059,12 +1104,21 @@ async function resolveAskArms(req, onAsk, signal) {
|
|
|
1059
1104
|
}
|
|
1060
1105
|
const bidi = carriesBidiControls(presented.value) || carriesBidiControls(req.preview);
|
|
1061
1106
|
const { hasBidiControls: _carried, ...bare } = req;
|
|
1062
|
-
|
|
1107
|
+
const wait = Promise.resolve(onAsk({
|
|
1063
1108
|
...bare,
|
|
1064
1109
|
boundInputHash: boundInputHashOf(presented.value),
|
|
1065
1110
|
args: approverView.value,
|
|
1066
1111
|
...(bidi ? { hasBidiControls: true } : {}),
|
|
1067
|
-
}, signal);
|
|
1112
|
+
}, signal));
|
|
1113
|
+
const raced = await raceAskWaitAgainstSignal(wait, signal);
|
|
1114
|
+
if (raced.tag === "aborted") {
|
|
1115
|
+
detachLateAskWait(wait, onLateSettlement);
|
|
1116
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, decisionReason: "mode",
|
|
1117
|
+
resolution: "task_aborted", settledBy: "aborted" };
|
|
1118
|
+
}
|
|
1119
|
+
if (raced.tag === "threw")
|
|
1120
|
+
throw raced.error;
|
|
1121
|
+
ok = raced.value;
|
|
1068
1122
|
}
|
|
1069
1123
|
catch (err) {
|
|
1070
1124
|
return {
|
|
@@ -1076,7 +1130,7 @@ async function resolveAskArms(req, onAsk, signal) {
|
|
|
1076
1130
|
};
|
|
1077
1131
|
}
|
|
1078
1132
|
if (signal?.aborted) {
|
|
1079
|
-
return { action: "deny", message: `approval aborted for "${req.toolName}" (
|
|
1133
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)`, decisionReason: "mode",
|
|
1080
1134
|
resolution: "task_aborted", settledBy: "aborted" };
|
|
1081
1135
|
}
|
|
1082
1136
|
if (ok === "unavailable") {
|