@sema-agent/core 7.17.7 → 7.19.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 +233 -13
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/agents/teacher.js +1 -0
- package/dist/agents/verify.js +3 -0
- package/dist/brain/anthropic.js +6 -3
- package/dist/brain/open-responses.js +10 -4
- package/dist/brain/openai.js +5 -2
- package/dist/brain/reasoning.d.ts +13 -13
- package/dist/brain/reasoning.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +18 -0
- package/dist/brain/route-adjudicator.js +16 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -0
- package/dist/brain/stream-shared.js +1 -0
- package/dist/core/agent-definition.d.ts +10 -0
- package/dist/core/artifact-host.d.ts +41 -4
- package/dist/core/artifact-host.js +18 -2
- package/dist/core/ask-origin.d.ts +16 -0
- package/dist/core/ask-origin.js +8 -0
- package/dist/core/auto-mode-arming.d.ts +6 -0
- package/dist/core/auto-mode-arming.js +5 -5
- package/dist/core/auto-mode-defaults.d.ts +43 -15
- package/dist/core/auto-mode-defaults.js +4 -1
- package/dist/core/auto-mode.d.ts +184 -59
- package/dist/core/auto-mode.js +188 -13
- package/dist/core/checkpoint-store.d.ts +23 -2
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/engine-notice.d.ts +77 -1
- package/dist/core/engine-notice.js +7 -0
- package/dist/core/gate-fold.js +5 -1
- package/dist/core/gate-lanes.js +9 -8
- package/dist/core/gate-outcome.d.ts +42 -2
- package/dist/core/gate-outcome.js +23 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hook-seats.d.ts +66 -0
- package/dist/core/hook-seats.js +37 -0
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/read-root-candidate.d.ts +12 -0
- package/dist/core/read-root-candidate.js +24 -0
- package/dist/core/roles.d.ts +9 -1
- package/dist/core/roles.js +4 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
- package/dist/core/runner/assemble-result.d.ts +2 -1
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/classifier-ladder.d.ts +227 -0
- package/dist/core/runner/classifier-ladder.js +214 -0
- package/dist/core/runner/contracts.d.ts +71 -13
- package/dist/core/runner/decide-continuation.d.ts +1 -3
- package/dist/core/runner/decide-continuation.js +1 -7
- package/dist/core/runner/deferral-census.d.ts +52 -0
- package/dist/core/runner/deferral-census.js +44 -0
- package/dist/core/runner/denial-limit-arms.js +2 -2
- package/dist/core/runner/derived-route-fallback.d.ts +8 -4
- package/dist/core/runner/derived-route-fallback.js +8 -3
- package/dist/core/runner/gate-exit.d.ts +12 -0
- package/dist/core/runner/gate-exit.js +6 -3
- package/dist/core/runner/prepare-artifact.d.ts +10 -2
- package/dist/core/runner/prepare-artifact.js +5 -4
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
- package/dist/core/runner/prepare-config-doors.d.ts +7 -1
- package/dist/core/runner/prepare-config-doors.js +13 -13
- package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
- package/dist/core/runner/prepare-defer-classify.js +8 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
- package/dist/core/runner/prepare-hands-readface.js +60 -24
- package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
- package/dist/core/runner/prepare-inherited-gate.js +10 -2
- package/dist/core/runner/prepare-lsp.d.ts +2 -2
- package/dist/core/runner/prepare-lsp.js +1 -1
- package/dist/core/runner/prepare-policy-chain.js +2 -0
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +7 -4
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
- package/dist/core/runner/prepare-prompt-inputs.js +4 -1
- package/dist/core/runner/prepare-run-refs.d.ts +9 -1
- package/dist/core/runner/prepare-run-refs.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
- package/dist/core/runner/prepare-suspend-saga.js +5 -2
- package/dist/core/runner/prepare-task.js +19 -14
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
- package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
- package/dist/core/runner/prepare-turn-wiring.js +3 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/resume-checkpoint-screen.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-identity-wiring.js +6 -2
- package/dist/core/runner/run-leg.d.ts +2 -0
- package/dist/core/runner/run-leg.js +45 -45
- package/dist/core/runner/run-notification-lane.js +55 -27
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +16 -13
- package/dist/core/runner/run-turn-boundary.js +15 -5
- package/dist/core/runner/runtask.d.ts +6 -5
- package/dist/core/runner/runtask.js +26 -14
- package/dist/core/runner/skill-base-dir.d.ts +54 -0
- package/dist/core/runner/skill-base-dir.js +42 -0
- package/dist/core/runner/synthetic-tools.d.ts +83 -48
- package/dist/core/runner/synthetic-tools.js +69 -32
- package/dist/core/runner/tool-defer-gate.d.ts +22 -9
- package/dist/core/runner/tool-defer-gate.js +15 -5
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +2 -0
- package/dist/core/runner/tool-progress-lane.d.ts +42 -0
- package/dist/core/runner/tool-progress-lane.js +61 -0
- package/dist/core/runner/turn-attachments.d.ts +43 -12
- package/dist/core/runner/turn-attachments.js +58 -14
- package/dist/core/runner-deps.d.ts +48 -7
- package/dist/core/sensitive-path-policy.d.ts +58 -17
- package/dist/core/sensitive-path-policy.js +33 -5
- package/dist/core/skill-listing-budget.d.ts +112 -0
- package/dist/core/skill-listing-budget.js +58 -0
- package/dist/core/swappable-deps.d.ts +9 -7
- package/dist/core/swappable-deps.js +7 -3
- package/dist/core/task-event.d.ts +96 -29
- package/dist/core/task-notification.d.ts +142 -16
- package/dist/core/task-notification.js +106 -0
- package/dist/core/task-result.d.ts +9 -1
- package/dist/core/task-spec.d.ts +46 -3
- package/dist/core/tighten-task-spec.js +7 -5
- package/dist/core/tool-model-gate.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +19 -4
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-search-setting.d.ts +78 -0
- package/dist/core/tool-search-setting.js +90 -0
- package/dist/core/tool-spec.d.ts +45 -4
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +46 -0
- package/dist/core/wiring-manifest.js +9 -1
- package/dist/core/write-protect.d.ts +8 -3
- package/dist/core/write-protect.js +8 -5
- package/dist/engine/execution-env/node-execution-env.js +10 -4
- package/dist/engine/harness/agent-harness.d.ts +42 -0
- package/dist/engine/harness/agent-harness.js +61 -29
- package/dist/engine/loop/agent-loop.d.ts +68 -3
- package/dist/engine/loop/agent-loop.js +109 -16
- package/dist/index.d.ts +13 -8
- package/dist/index.js +11 -7
- package/dist/internal/harness.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +6 -1
- package/dist/orchestration/workflow-governance.d.ts +7 -1
- package/dist/orchestration/workflow-governance.js +9 -5
- package/dist/orchestration/workflow-primitives.d.ts +3 -0
- package/dist/orchestration/workflow-primitives.js +6 -1
- package/dist/orchestration/workflow-types.d.ts +48 -13
- package/dist/orchestration/workflow-types.js +1 -1
- package/dist/orchestration/workflow.js +45 -17
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/section-usage.d.ts +26 -0
- package/dist/prompt-assembly/section-usage.js +10 -0
- package/dist/prompt-assembly/types.d.ts +8 -6
- package/dist/prompts/default.d.ts +13 -0
- package/dist/prompts/default.js +16 -8
- package/dist/tools/artifact/artifact-text.d.ts +10 -1
- package/dist/tools/artifact/artifact-text.js +6 -3
- package/dist/tools/artifact/artifact-tool.d.ts +12 -2
- package/dist/tools/artifact/artifact-tool.js +203 -70
- package/dist/tools/artifact/conformance.d.ts +56 -0
- package/dist/tools/artifact/conformance.js +349 -0
- package/dist/tools/artifact/local-stub.js +1 -1
- package/dist/tools/artifact/reject-breaker.d.ts +28 -0
- package/dist/tools/artifact/reject-breaker.js +42 -0
- package/dist/tools/fs/bash-lexer.d.ts +132 -0
- package/dist/tools/fs/bash-lexer.js +563 -0
- package/dist/tools/fs/bash-program-position.d.ts +142 -0
- package/dist/tools/fs/bash-program-position.js +125 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
- package/dist/tools/fs/bash-readonly-classifier.js +180 -558
- package/dist/tools/fs/containment-face.d.ts +89 -0
- package/dist/tools/fs/containment-face.js +81 -0
- package/dist/tools/fs/fs-bash.d.ts +46 -1
- package/dist/tools/fs/fs-bash.js +165 -25
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +24 -5
- package/dist/tools/fs/index.js +18 -6
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/repo-map.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +18 -3
- package/dist/tools/fs/safety.js +11 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -13
- package/wire-changes/core@7.17.7.json +49 -0
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
- package/dist/tools/fs/read-face.d.ts +0 -68
- package/dist/tools/fs/read-face.js +0 -47
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skills-listing BUDGET SEAT as vocabulary: the two knobs a deployment may declare
|
|
3
|
+
* (`TaskSpec.skillListingBudgetFraction` / `TaskSpec.skillListingMaxDescChars`), the ONE reader that
|
|
4
|
+
* lowers them to the numbers the renderer measures against, and the disclosure announced when the
|
|
5
|
+
* budget costs a skill its description. The reader is pure; the announcer's only effect is the notice.
|
|
6
|
+
* `runner/synthetic-tools.ts` owns the renderer that consumes the numbers.
|
|
7
|
+
*
|
|
8
|
+
* Sibling of `tool-search-setting.ts` in shape and in posture: a value outside its domain REFUSES
|
|
9
|
+
* loudly rather than folding back to the default, because a knob that is silently dropped reads to the
|
|
10
|
+
* deployment as a knob that took effect.
|
|
11
|
+
*/
|
|
12
|
+
import { type EngineNotice } from "./engine-notice.js";
|
|
13
|
+
/** Fraction of the model's context window the listing may spend when the deployment declares nothing. */
|
|
14
|
+
export declare const SKILL_LISTING_BUDGET_FRACTION_DEFAULT = 0.01;
|
|
15
|
+
/** Per-skill description cap (code units, ellipsis included) when the deployment declares nothing. */
|
|
16
|
+
export declare const SKILL_LISTING_DESC_MAX_CHARS_DEFAULT = 1536;
|
|
17
|
+
/** Context window assumed for a model that declares none. */
|
|
18
|
+
export declare const SKILL_LISTING_DEFAULT_CONTEXT_TOKENS = 200000;
|
|
19
|
+
/**
|
|
20
|
+
* The token→byte exchange rate this budget allocates at.
|
|
21
|
+
*
|
|
22
|
+
* @contract skills.listing_budget_exchange_rate — deliberately NOT `DEFAULT_CHARS_PER_TOKEN` (3), and
|
|
23
|
+
* the difference is directional, not an inconsistency. That constant serves the compaction/context
|
|
24
|
+
* estimators, which convert bytes→tokens and must OVER-estimate the token count to stay inside a
|
|
25
|
+
* window; a low rate is the conservative choice there. This seat converts tokens→a byte ALLOWANCE,
|
|
26
|
+
* where a low rate silently SHRINKS what the deployment may spend. Two directions, two numbers, both
|
|
27
|
+
* named. At 3 the default allowance would be 6 000 bytes rather than 8 000 — a quarter of every
|
|
28
|
+
* default deployment's listing, removed by an estimator's safety margin.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SKILL_LISTING_BYTES_PER_TOKEN = 4;
|
|
31
|
+
/**
|
|
32
|
+
* The hard ceiling on the rendered `<skills>` block, in UTF-8 bytes.
|
|
33
|
+
*
|
|
34
|
+
* @contract skills.listing_budget_ceiling — this is a STRUCTURAL property of the delivery lane, not a
|
|
35
|
+
* policy choice and not a superset optimization. The block is not part of the system prompt: it is the
|
|
36
|
+
* body of the `skills_listing` turn attachment, and every attachment bundle is capped at
|
|
37
|
+
* `ATTACHMENT_BYTE_CAP` (8 KiB) by the boundary's `capAttachments`, which CLIPS anything over. A
|
|
38
|
+
* clipped skills frame never satisfies the run loop's intact-survival commit predicate, so the
|
|
39
|
+
* announced set never advances and the frame is re-rendered and re-clipped at every subsequent
|
|
40
|
+
* boundary — permanently. The cap is therefore enforced byte-exact over the WHOLE render (fences,
|
|
41
|
+
* headers, markers, joiners included) and MUST stay ≤ `ATTACHMENT_BYTE_CAP`; the renderer's module
|
|
42
|
+
* pins that relation, since importing the attachment module here would invert the dependency.
|
|
43
|
+
*
|
|
44
|
+
* The consequence is a one-way knob: {@link resolveSkillListingBudget} clamps the declared budget to
|
|
45
|
+
* this number, so a raised fraction (or a model with a very large window) can LOWER the listing budget
|
|
46
|
+
* but can never raise it above this ceiling. When the clamp costs a description, both numbers ride the
|
|
47
|
+
* disclosure ({@link announceSkillListingTruncation}), so the deployment reads what it asked for beside
|
|
48
|
+
* what it got instead of inferring the ceiling from a short listing.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SKILLS_BLOCK_MAX_BYTES = 8000;
|
|
51
|
+
/** The typed refusal a budget seat outside its domain raises: a bad value never selects a smaller
|
|
52
|
+
* listing silently, and never folds back to the default budget. */
|
|
53
|
+
export declare const SKILL_LISTING_BUDGET_INVALID_CODE = "config.skill_listing_budget_invalid";
|
|
54
|
+
/** The notice code minted when the budget cost at least one skill its description or its line. */
|
|
55
|
+
export declare const SKILL_LISTING_TRUNCATED_CODE = "skills.listing_truncated";
|
|
56
|
+
/** The lowered seat: the two numbers the renderer measures against, plus what was asked for. */
|
|
57
|
+
export interface ResolvedSkillListingBudget {
|
|
58
|
+
/** The budget the renderer enforces over the whole block, in UTF-8 bytes — already clamped to
|
|
59
|
+
* {@link SKILLS_BLOCK_MAX_BYTES}. */
|
|
60
|
+
blockMaxBytes: number;
|
|
61
|
+
/** What the declared fraction asked for BEFORE the structural clamp. Equal to `blockMaxBytes`
|
|
62
|
+
* whenever the ceiling did not bind; strictly greater when it did. */
|
|
63
|
+
requestedBytes: number;
|
|
64
|
+
/** Per-skill description cap in code units, ellipsis included. */
|
|
65
|
+
descMaxChars: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The ONE reader of the two listing-budget seats.
|
|
69
|
+
*
|
|
70
|
+
* `skillListingBudgetFraction` must be a finite number in `(0, 1]` — zero is excluded deliberately: a
|
|
71
|
+
* zero budget is not "spend nothing", it is a listing every skill loses its description to, which the
|
|
72
|
+
* deployment expresses by declaring fewer skills. `skillListingMaxDescChars` must be a positive safe
|
|
73
|
+
* integer. Both absent ⇒ the documented defaults, which reproduce the pre-seat render byte for byte
|
|
74
|
+
* (`200 000 × 4 × 0.01` = 8 000 = {@link SKILLS_BLOCK_MAX_BYTES}).
|
|
75
|
+
*
|
|
76
|
+
* The window is `contextTokens ?? contextWindow`, the same physical coordinate the deferral gate
|
|
77
|
+
* measures against. A window that is absent OR unusable (non-finite, non-positive) reads as
|
|
78
|
+
* {@link SKILL_LISTING_DEFAULT_CONTEXT_TOKENS}: one rule, not a case per bad shape — a model's own
|
|
79
|
+
* declaration is judged by the model doors, and this seat must still produce a number a renderer can
|
|
80
|
+
* compare against.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveSkillListingBudget(opts: {
|
|
83
|
+
fraction?: unknown;
|
|
84
|
+
maxDescChars?: unknown;
|
|
85
|
+
model?: {
|
|
86
|
+
contextTokens?: number;
|
|
87
|
+
contextWindow?: number;
|
|
88
|
+
} | undefined;
|
|
89
|
+
}): ResolvedSkillListingBudget;
|
|
90
|
+
/**
|
|
91
|
+
* Announce a listing the budget degraded — delivered only when a skill actually lost its description
|
|
92
|
+
* (`nameOnly`) or its line (`dropped`), so a deployment whose roster fits hears nothing.
|
|
93
|
+
*
|
|
94
|
+
* Called from the two lanes that DELIVER a listing frame (the leg's first-frame producer and the turn
|
|
95
|
+
* boundary), never from the config door: the door cannot see the deployment's
|
|
96
|
+
* `attachments.skillsListing` switch (resolved at run start), it runs per prepared task rather than per
|
|
97
|
+
* delivered listing, and a resume leg reaches only the boundary.
|
|
98
|
+
*
|
|
99
|
+
* Audience is `operator`: the person who can raise the fraction, shorten a description or remove a
|
|
100
|
+
* skill runs the deployment, not the session. `requestedBudgetBytes` rides beside `budgetBytes`
|
|
101
|
+
* precisely so the structural ceiling ({@link SKILLS_BLOCK_MAX_BYTES}) is legible at the moment it
|
|
102
|
+
* costs something — the two differ only when the clamp bound.
|
|
103
|
+
*/
|
|
104
|
+
export declare function announceSkillListingTruncation(onNotice: ((notice: EngineNotice) => void) | undefined, opts: {
|
|
105
|
+
/** The denominator: how many skills the DELIVERED frame set out to list — the whole roster on an
|
|
106
|
+
* initial block, the newly announced set on a drift frame. Read off that frame's own render. */
|
|
107
|
+
total: number;
|
|
108
|
+
nameOnly: readonly string[];
|
|
109
|
+
dropped: readonly string[];
|
|
110
|
+
budget: ResolvedSkillListingBudget;
|
|
111
|
+
sessionId?: string | undefined;
|
|
112
|
+
}): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "./engine-notice.js";
|
|
2
|
+
import { describeRejectedValue } from "./tool-search-setting.js";
|
|
3
|
+
export const SKILL_LISTING_BUDGET_FRACTION_DEFAULT = 0.01;
|
|
4
|
+
export const SKILL_LISTING_DESC_MAX_CHARS_DEFAULT = 1_536;
|
|
5
|
+
export const SKILL_LISTING_DEFAULT_CONTEXT_TOKENS = 200_000;
|
|
6
|
+
export const SKILL_LISTING_BYTES_PER_TOKEN = 4;
|
|
7
|
+
export const SKILLS_BLOCK_MAX_BYTES = 8_000;
|
|
8
|
+
export const SKILL_LISTING_BUDGET_INVALID_CODE = "config.skill_listing_budget_invalid";
|
|
9
|
+
export const SKILL_LISTING_TRUNCATED_CODE = "skills.listing_truncated";
|
|
10
|
+
function budgetRefusal(problem) {
|
|
11
|
+
const e = new Error(`TaskSpec.${problem}; a bad value refuses loudly, nothing falls back to the default listing budget.`);
|
|
12
|
+
e.code = SKILL_LISTING_BUDGET_INVALID_CODE;
|
|
13
|
+
return e;
|
|
14
|
+
}
|
|
15
|
+
export function resolveSkillListingBudget(opts) {
|
|
16
|
+
const { fraction, maxDescChars } = opts;
|
|
17
|
+
if (fraction !== undefined && (typeof fraction !== "number" || !Number.isFinite(fraction) || fraction <= 0 || fraction > 1)) {
|
|
18
|
+
throw budgetRefusal(`skillListingBudgetFraction: invalid value ${describeRejectedValue(fraction)} — expected a finite fraction in (0, 1]`);
|
|
19
|
+
}
|
|
20
|
+
if (maxDescChars !== undefined && (typeof maxDescChars !== "number" || !Number.isSafeInteger(maxDescChars) || maxDescChars <= 0)) {
|
|
21
|
+
throw budgetRefusal(`skillListingMaxDescChars: invalid value ${describeRejectedValue(maxDescChars)} — expected a positive integer`);
|
|
22
|
+
}
|
|
23
|
+
const declaredWindow = opts.model?.contextTokens ?? opts.model?.contextWindow;
|
|
24
|
+
const windowTokens = typeof declaredWindow === "number" && Number.isFinite(declaredWindow) && declaredWindow > 0
|
|
25
|
+
? declaredWindow
|
|
26
|
+
: SKILL_LISTING_DEFAULT_CONTEXT_TOKENS;
|
|
27
|
+
const requestedBytes = Math.max(1, Math.floor(windowTokens * SKILL_LISTING_BYTES_PER_TOKEN * (fraction ?? SKILL_LISTING_BUDGET_FRACTION_DEFAULT)));
|
|
28
|
+
return {
|
|
29
|
+
blockMaxBytes: Math.min(requestedBytes, SKILLS_BLOCK_MAX_BYTES),
|
|
30
|
+
requestedBytes,
|
|
31
|
+
descMaxChars: maxDescChars ?? SKILL_LISTING_DESC_MAX_CHARS_DEFAULT,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function announceSkillListingTruncation(onNotice, opts) {
|
|
35
|
+
const { total, nameOnly, dropped, budget } = opts;
|
|
36
|
+
if (nameOnly.length === 0 && dropped.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
const clamped = budget.requestedBytes > budget.blockMaxBytes;
|
|
39
|
+
deliverEngineNotice(onNotice, {
|
|
40
|
+
code: SKILL_LISTING_TRUNCATED_CODE,
|
|
41
|
+
message: `The skills listing does not fit its ${budget.blockMaxBytes}-byte budget` +
|
|
42
|
+
(clamped ? ` (${budget.requestedBytes} bytes were declared; the listing's delivery lane caps it at ${SKILLS_BLOCK_MAX_BYTES})` : "") +
|
|
43
|
+
`: ${nameOnly.length} of ${total} are listed name-only` +
|
|
44
|
+
(dropped.length > 0 ? `, and ${dropped.length} did not fit the listing at all` : "") +
|
|
45
|
+
`. The \`Skill\` tool still serves every one of them. Raise skillListingBudgetFraction, lower` +
|
|
46
|
+
` skillListingMaxDescChars, or declare fewer skills.`,
|
|
47
|
+
detail: {
|
|
48
|
+
budgetBytes: budget.blockMaxBytes,
|
|
49
|
+
requestedBudgetBytes: budget.requestedBytes,
|
|
50
|
+
descMaxChars: budget.descMaxChars,
|
|
51
|
+
total,
|
|
52
|
+
kept: total - nameOnly.length - dropped.length,
|
|
53
|
+
nameOnly: [...nameOnly],
|
|
54
|
+
dropped: [...dropped],
|
|
55
|
+
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -38,12 +38,14 @@ import type { RunnerDeps } from "./runner-deps.js";
|
|
|
38
38
|
* current bindings are re-applied over the new catalog; present (including `undefined`)
|
|
39
39
|
* ⇒ replaced.
|
|
40
40
|
* - `readFace` — the deployment's read-face declaration ({@link RunnerDeps.readFace}); screened by the
|
|
41
|
-
* same value gate every prepare applies (`
|
|
41
|
+
* same value gate every prepare applies (`assertContainmentFaceValue`: `"open"` | `"roots"`
|
|
42
42
|
* | absent, anything else refused loudly). The RESOLVED face of a leg is still the
|
|
43
43
|
* resolver's business (task seat, governance, the read-only mount); the swap changes the
|
|
44
44
|
* deployment seat that resolution reads.
|
|
45
|
+
* - `writeFace` — the deployment's write-face declaration ({@link RunnerDeps.writeFace}); the read seat's
|
|
46
|
+
* twin (same value gate, same in-flight snapshot rule, its own notice) and independent of it.
|
|
45
47
|
*/
|
|
46
|
-
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace"];
|
|
48
|
+
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace"];
|
|
47
49
|
export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
|
|
48
50
|
/** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
|
|
49
51
|
export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
|
|
@@ -52,12 +54,13 @@ export declare function isSwappableDepSeat(k: unknown): k is SwappableDepSeat;
|
|
|
52
54
|
/**
|
|
53
55
|
* Which notice a seat's successful swap is announced under — the disposition table over the set (a seat
|
|
54
56
|
* with no row does not compile: {@link SwapNoticeTableCoversEverySeat}). `models` and `tiers` share the
|
|
55
|
-
* catalog-generation line (one swap, one line, both counts);
|
|
57
|
+
* catalog-generation line (one swap, one line, both counts); each containment face has its own.
|
|
56
58
|
*/
|
|
57
59
|
export declare const SWAP_SEAT_NOTICE: {
|
|
58
60
|
readonly models: "config.models_swapped";
|
|
59
61
|
readonly tiers: "config.models_swapped";
|
|
60
62
|
readonly readFace: "config.read_face_swapped";
|
|
63
|
+
readonly writeFace: "config.write_face_swapped";
|
|
61
64
|
};
|
|
62
65
|
/** The fence over the notice table: `never` while every seat has a row. */
|
|
63
66
|
export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
|
|
@@ -66,7 +69,7 @@ export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<Swappa
|
|
|
66
69
|
export interface ScreenedSwappableDeps {
|
|
67
70
|
readonly seats: readonly SwappableDepSeat[];
|
|
68
71
|
/** A fresh plain object: the screened value of every present seat (`models`/`tiers` shallow-copied,
|
|
69
|
-
* `readFace` the screened word; an explicit `undefined` is kept as a PRESENT own key so the door can
|
|
72
|
+
* `readFace`/`writeFace` the screened word; an explicit `undefined` is kept as a PRESENT own key so the door can
|
|
70
73
|
* tell "clear this seat" from "not named"). */
|
|
71
74
|
readonly candidate: SwappableDeps;
|
|
72
75
|
}
|
|
@@ -83,8 +86,7 @@ export interface ScreenedSwappableDeps {
|
|
|
83
86
|
* announcement, which runs AFTER the write — a throw there would have left a half-announced swap in
|
|
84
87
|
* force). The prepare doors read their deps seats the same way.
|
|
85
88
|
*
|
|
86
|
-
* The per-seat gate is the SAME one the seat's prepare-time reader applies (
|
|
87
|
-
* `
|
|
88
|
-
* seat name on every leg), so a value the door admits is a value every later prepare admits.
|
|
89
|
+
* The per-seat gate is the SAME one the seat's prepare-time reader applies (both face seats ⇒
|
|
90
|
+
* `assertContainmentFaceValue`), so a value the door admits is a value every later prepare admits.
|
|
89
91
|
*/
|
|
90
92
|
export declare function screenSwappableDeps(next: unknown): ScreenedSwappableDeps;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace"];
|
|
1
|
+
import { assertContainmentFaceValue } from "../tools/fs/containment-face.js";
|
|
2
|
+
export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace", "writeFace"];
|
|
3
3
|
const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
|
|
4
4
|
export function isSwappableDepSeat(k) {
|
|
5
5
|
return SWAPPABLE_DEP_SEAT_SET.has(k);
|
|
@@ -8,6 +8,7 @@ export const SWAP_SEAT_NOTICE = {
|
|
|
8
8
|
models: "config.models_swapped",
|
|
9
9
|
tiers: "config.models_swapped",
|
|
10
10
|
readFace: "config.read_face_swapped",
|
|
11
|
+
writeFace: "config.write_face_swapped",
|
|
11
12
|
};
|
|
12
13
|
export function screenSwappableDeps(next) {
|
|
13
14
|
if (next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
@@ -43,7 +44,10 @@ export function screenSwappableDeps(next) {
|
|
|
43
44
|
candidate.tiers = v === undefined ? undefined : { ...v };
|
|
44
45
|
break;
|
|
45
46
|
case "readFace":
|
|
46
|
-
candidate.readFace =
|
|
47
|
+
candidate.readFace = assertContainmentFaceValue(v, "read", "deps");
|
|
48
|
+
break;
|
|
49
|
+
case "writeFace":
|
|
50
|
+
candidate.writeFace = assertContainmentFaceValue(v, "write", "deps");
|
|
47
51
|
break;
|
|
48
52
|
default: {
|
|
49
53
|
const _exhaustive = seat;
|
|
@@ -89,24 +89,19 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
|
|
|
89
89
|
/**
|
|
90
90
|
* WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
|
|
91
91
|
* {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
|
|
92
|
-
* unattended progress line ("no answer for three minutes") needs the reason
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* have no status at all.) Values are about the SHAPE of the failure, never its provider taxonomy:
|
|
92
|
+
* unattended progress line ("no answer for three minutes") needs the reason. (The status has its own seat,
|
|
93
|
+
* {@link BrainStatus.errorStatus}; this bucket is the reason a consumer RENDERS and the only carrier for the
|
|
94
|
+
* transport classes, which have no status at all.) Values are about the SHAPE of the failure, never its
|
|
95
|
+
* provider taxonomy:
|
|
97
96
|
* - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
|
|
98
97
|
* at that address, or the name has no address). This is the class the SHORT retry lane serves.
|
|
99
98
|
* - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
|
|
100
99
|
* tear. No verdict about the target; the full ladder applies.
|
|
101
100
|
* - `stall` — no failure was observed on the link at all: THIS process cancelled its own
|
|
102
101
|
* request because one of its stall watchdogs reached its deadline while the stream produced
|
|
103
|
-
* nothing. The distinction from `transport` is WHO GAVE UP
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* silence is what a watchdog measures, so a `stall` can still turn out to be a network fault; what
|
|
107
|
-
* it always tells you truthfully is that the request was cancelled here, at a configured deadline,
|
|
108
|
-
* rather than lost out there. Reporting these as `transport` asserted the opposite — that the link
|
|
109
|
-
* had dropped — when nothing about the link was known at all.
|
|
102
|
+
* nothing. The distinction from `transport` is WHO GAVE UP: a `transport` frame reports something
|
|
103
|
+
* that happened to the call, a `stall` frame reports a local deadline. It is not a verdict on the
|
|
104
|
+
* remedy — a blackholed connection produces silence too, and silence is what a watchdog measures.
|
|
110
105
|
* - `rate_limit` — the provider asked the caller to slow down.
|
|
111
106
|
* - `server` — the provider reported a failure on its own side.
|
|
112
107
|
* - `http` — a response the status predicate calls terminal, retried anyway because the
|
|
@@ -138,15 +133,12 @@ export interface BrainStatus {
|
|
|
138
133
|
* output-cap re-send, which takes no backoff).
|
|
139
134
|
*
|
|
140
135
|
* WHY IT IS PRODUCED HERE rather than derived by the consumer: `Date.now() + retryInMs` is only
|
|
141
|
-
* correct for a reader that received the frame instantly
|
|
142
|
-
*
|
|
143
|
-
* built that way restarts on every re-announced slice instead of converging. The producer is the
|
|
144
|
-
* only party that can state the instant, so it does.
|
|
136
|
+
* correct for a reader that received the frame instantly, so a countdown built several process hops
|
|
137
|
+
* away restarts on every re-announced slice instead of converging.
|
|
145
138
|
*
|
|
146
|
-
* CLOCK DOMAIN, stated to remove the ambiguity: WALL CLOCK (`Date.now()`, epoch ms), NOT a
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* the authoritative RELATIVE quantity, and this seat is the absolute convenience derived from it.
|
|
139
|
+
* CLOCK DOMAIN, stated to remove the ambiguity: WALL CLOCK (`Date.now()`, epoch ms), NOT a monotonic
|
|
140
|
+
* reading. A consumer must not compare it against its own monotonic timers, and must treat it as
|
|
141
|
+
* approximate across a clock adjustment — {@link retryInMs} stays the authoritative RELATIVE quantity.
|
|
150
142
|
*/
|
|
151
143
|
retryAtMs?: number;
|
|
152
144
|
/** On a `waiting_first_token` frame: how long the first token has been awaited, ms, at the
|
|
@@ -261,13 +253,10 @@ export type TaskEvent = ({
|
|
|
261
253
|
/**
|
|
262
254
|
* The assistant's streaming PROSE SEGMENT is COMPLETE: the model closed the text content
|
|
263
255
|
* block whose bytes just streamed as `text_delta`s. This is the explicit segment boundary a
|
|
264
|
-
* REMOTE consumer needs so it never has to guess segment ends from wire silence
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* consumer reads it off the provider stream natively, and this event is that same boundary
|
|
269
|
-
* surfaced on the TaskEvent wire (CC pays the byte cost of re-carrying the block at the stop;
|
|
270
|
-
* so does this event's `content`).
|
|
256
|
+
* REMOTE consumer needs so it never has to guess segment ends from wire silence. CC-aligned: block
|
|
257
|
+
* completion IS the segmentation signal there (CC's agent stream yields each finished content block at
|
|
258
|
+
* the provider's `content_block_stop`), and this event is that same boundary surfaced on the TaskEvent
|
|
259
|
+
* wire.
|
|
271
260
|
*
|
|
272
261
|
* `content` = the authoritative FULL text of the completed segment (byte-equal to that
|
|
273
262
|
* segment's accumulated deltas, from the brain's own accumulation) — a consumer commits the
|
|
@@ -292,6 +281,61 @@ export type TaskEvent = ({
|
|
|
292
281
|
toolName: string; /** {@link ToolSpec.label} when distinct from `toolName` (design/71 liveness); omitted otherwise. */
|
|
293
282
|
label?: string;
|
|
294
283
|
args: unknown;
|
|
284
|
+
} & TaskEventIdentity) | ({
|
|
285
|
+
/**
|
|
286
|
+
* A LIVENESS TICK for a tool call that is still running. Emitted on the run's own queue between this
|
|
287
|
+
* call's `tool_start` and `tool_end`, under the same ordering law as every other content frame, and
|
|
288
|
+
* forwarded to a parent's display sink under the SAME opt-in as those two
|
|
289
|
+
* (`TaskSpec.forwardSubagentEvents`).
|
|
290
|
+
*
|
|
291
|
+
* ONE FRAME, not two: CC 2.1.259 splits this into a per-tool `tool_heartbeat` plus Bash's own
|
|
292
|
+
* `bash_progress`; this engine mints the single shape and lets the OUTPUT KEYS be what is conditional.
|
|
293
|
+
* Registered divergence.
|
|
294
|
+
*
|
|
295
|
+
* @contract tool_progress.cadence — AT MOST one frame per second per tool call, enforced at the mint over
|
|
296
|
+
* ALL producers rather than inside each one (a producer's own timer is a sampling clock, not the contract),
|
|
297
|
+
* and a call's first frame is never earlier than one interval after it entered execution. A call that
|
|
298
|
+
* finishes inside one interval emits NO frame at all — this frame's absence is never evidence that a call
|
|
299
|
+
* did not run, and a consumer must not expect one per `tool_start`.
|
|
300
|
+
*
|
|
301
|
+
* @contract tool_progress.ephemeral — LIVE ONLY, like `task_progress`/`status`: core never persists this
|
|
302
|
+
* frame (no session-tree entry, no checkpoint state, no resume replay), and a resumed leg re-ticks from
|
|
303
|
+
* its own clock. A consumer wanting a DURABLE record of a long call keeps {@link elapsedTimeSeconds} —
|
|
304
|
+
* never {@link output}, untrusted process bytes bounded for a pane (the tool's result is the kept face).
|
|
305
|
+
*
|
|
306
|
+
* Additive and ignorable: a consumer switching on event type and dropping unknown members is unchanged,
|
|
307
|
+
* and no gate, policy, memory or model-context path reads this frame.
|
|
308
|
+
*/
|
|
309
|
+
type: "tool_progress";
|
|
310
|
+
/** The still-running call's id — the join key to its own `tool_start`/`tool_end`. */
|
|
311
|
+
toolCallId: string;
|
|
312
|
+
/** The running tool's name (the `tool_start` value verbatim). */
|
|
313
|
+
toolName: string;
|
|
314
|
+
/** Whole seconds this tool call has been running, floored and MONOTONIC across its frames. ONE ORIGIN per
|
|
315
|
+
* call, stamped by the mint rather than by whichever producer emitted — the call's entry into execution,
|
|
316
|
+
* earlier than a shell command's spawn. It does NOT equal the `tool_end` duration. */
|
|
317
|
+
elapsedTimeSeconds: number;
|
|
318
|
+
/**
|
|
319
|
+
* The TAIL of what the command has written so far (stdout and stderr interleaved in arrival order),
|
|
320
|
+
* bounded by the mint to a small number of trailing lines — a live pane, never a log.
|
|
321
|
+
*
|
|
322
|
+
* UNTRUSTED RAW, exactly as `tool_end.output`: process output that may carry secrets, paths or PII;
|
|
323
|
+
* core bounds only its SIZE, and a consumer MUST redact before persisting or displaying it. Never
|
|
324
|
+
* re-feed it to a model — it is a moving, truncated window.
|
|
325
|
+
*
|
|
326
|
+
* ABSENT, together with {@link totalLines} and {@link totalBytes}, exactly when the call has produced
|
|
327
|
+
* NO output through this environment — it printed nothing (`sleep 3`), or the environment does not
|
|
328
|
+
* stream (a buffered remote `exec` never invokes the output callbacks). The three ride or stay away
|
|
329
|
+
* together: present = "output has been observed", absent = "none has been observed", never
|
|
330
|
+
* "unchanged since the last frame".
|
|
331
|
+
*/
|
|
332
|
+
output?: string;
|
|
333
|
+
/** Newlines observed in the command's output so far (≥ the line count of {@link output}, which is
|
|
334
|
+
* only its tail). Rides iff {@link output} does — see its absence law. */
|
|
335
|
+
totalLines?: number;
|
|
336
|
+
/** Bytes observed in the command's output so far, counted BEFORE the tail bound, so it keeps
|
|
337
|
+
* growing after {@link output} stops being able to. Rides iff {@link output} does. */
|
|
338
|
+
totalBytes?: number;
|
|
295
339
|
} & TaskEventIdentity) | ({
|
|
296
340
|
type: "tool_end";
|
|
297
341
|
toolCallId: string;
|
|
@@ -433,8 +477,7 @@ export type TaskEvent = ({
|
|
|
433
477
|
* faces — so a renderer never branches on prose to tell "a person refused" from "the window closed".
|
|
434
478
|
*
|
|
435
479
|
* WHERE IT COMES FROM — never inferred here: the gate mints it at its exit, the decide lane mints it
|
|
436
|
-
* from the host's decision facts joined with the row's persisted origin.
|
|
437
|
-
* from `isError`, from the result, or from anything a tool or a post-tool hook can write.
|
|
480
|
+
* from the host's decision facts joined with the row's persisted origin.
|
|
438
481
|
*
|
|
439
482
|
* ABSENT on every frame of a call the gate never saw (a tool call on an unarmed gate, a deferred
|
|
440
483
|
* re-issue, a reconcile-recovered orphan). A gated call's frame ALWAYS carries it — an absence is
|
|
@@ -485,6 +528,14 @@ export type TaskEvent = ({
|
|
|
485
528
|
usedTokens: number;
|
|
486
529
|
windowTokens: number;
|
|
487
530
|
compactAtTokens: number;
|
|
531
|
+
/**
|
|
532
|
+
* Additive — the leg's system prompt broken down by pack SECTION. Meaning, and the readings it
|
|
533
|
+
* forbids, are owned by `contextUsageSections` (`prompt-assembly/section-usage.ts`, `@contract
|
|
534
|
+
* context_usage.sections`): PREPARE-TIME estimates in a different coordinate from {@link usedTokens},
|
|
535
|
+
* never compared to {@link compactAtTokens}. Absent when the leg assembled no section IR — "no section
|
|
536
|
+
* view", never "the sections weigh nothing".
|
|
537
|
+
*/
|
|
538
|
+
sections?: readonly import("../prompt-assembly/section-usage.js").ContextUsageSection[];
|
|
488
539
|
} & TaskEventIdentity) | ({
|
|
489
540
|
type: "turn_end";
|
|
490
541
|
/**
|
|
@@ -907,6 +958,22 @@ export type TaskEvent = ({
|
|
|
907
958
|
*/
|
|
908
959
|
type: "tool_roster_delta";
|
|
909
960
|
delta: import("./tool-roster.js").ToolRosterDelta;
|
|
961
|
+
} & TaskEventIdentity) | ({
|
|
962
|
+
/**
|
|
963
|
+
* This leg's DEFERRAL CENSUS: which mounted tools ship as name-only placeholders, which the model has activated, and the policy
|
|
964
|
+
* (`RunnerDeps.toolSearch`) that decided it — meaning owned by `ToolDisclosureSnapshot` (`runner/tool-disclosure.ts`). EXISTS ONLY ON A LEG THAT
|
|
965
|
+
* DEFERS SOMETHING: an empty deferred set emits no frame, and that absence reads as "nothing was deferred here", never "no disclosure in this build"
|
|
966
|
+
* and never "unknown". First frame = the leg-start census, right after `wiring_manifest` — `activated` is EMPTY on a fresh leg and, on a RESUMED one,
|
|
967
|
+
* whatever the engine reseeded (never read a non-empty first census as "this leg activated them"); every later frame = a census that CHANGED, i.e. an
|
|
968
|
+
* activation landed (either lane — one section, one census). WHOLE SNAPSHOT: replace your copy. `deferred` is frozen at prepare; lists sorted.
|
|
969
|
+
*/
|
|
970
|
+
type: "tool_disclosure";
|
|
971
|
+
policy: import("./tool-search-setting.js").ToolSearchPolicy;
|
|
972
|
+
/** Meaning owned by `ToolDisclosureSnapshot.thresholdPercent`: absent under `always` / `never`, where
|
|
973
|
+
* no threshold was consulted, and an absence must not be read as "the default percent". */
|
|
974
|
+
thresholdPercent?: number;
|
|
975
|
+
deferred: readonly string[];
|
|
976
|
+
activated: readonly string[];
|
|
910
977
|
} & TaskEventIdentity) | {
|
|
911
978
|
type: "done";
|
|
912
979
|
result: TaskResult;
|
|
@@ -118,18 +118,14 @@ export interface TaskNotificationPayload {
|
|
|
118
118
|
* preconditions, so the claim and the next SendMessage agree. Lets the parent decide
|
|
119
119
|
* continue-vs-restart without trial-and-error. Background-agent lane only. */
|
|
120
120
|
resumable?: boolean;
|
|
121
|
-
/** A cross-channel correlation key for
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* — a consumer previously had to reconstruct "are these the same completion?" from weaker signals
|
|
125
|
-
* (task_id + status + timing). Minted ONCE per completion, fill-once (first writer wins), on the
|
|
121
|
+
/** A cross-channel correlation key for ONE completion event — the same task's completion fans out
|
|
122
|
+
* across several independent read faces (this live notification frame, a durable inbox entry, and the
|
|
123
|
+
* `TaskOutput` poll projection). Minted ONCE per completion, fill-once (first writer wins), on the
|
|
126
124
|
* same task-registry handle/record every read face serves from — see
|
|
127
125
|
* {@link "./task-registry.js".UnifiedTaskOutput.completionId} for the shared contract (open-shape
|
|
128
126
|
* `string`, absent until terminal settle, absent on a pre-P1-3 core build). Deliberately ABSENT on
|
|
129
|
-
* the `"external"` lane (`TaskStream.notify()`)
|
|
130
|
-
* correlation key
|
|
131
|
-
* mint this from; core-minting one there would create two competing correlation keys on the same
|
|
132
|
-
* frame. */
|
|
127
|
+
* the `"external"` lane (`TaskStream.notify()`): that lane has its own caller-supplied `seq`
|
|
128
|
+
* correlation key and no registry-side settle point to mint this from. */
|
|
133
129
|
completionId?: string;
|
|
134
130
|
/** design/176 — the peer-message marker + hop-chain carrier, present ONLY on payloads minted by
|
|
135
131
|
* the SendMessage delivery legs (the engine's single peer-carrier producer). Three uses: it
|
|
@@ -331,13 +327,12 @@ export declare function renderCrossSessionMessageFrame(frame: NonNullable<TaskNo
|
|
|
331
327
|
export declare function renderCrossSessionNoticeLine(notice: NonNullable<TaskNotificationPayload["crossSessionNotice"]>): string;
|
|
332
328
|
export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
|
|
333
329
|
/**
|
|
334
|
-
* The BETWEEN-TURNS pending lane. A task notification born while NO
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* in-turn and between-turn events keep one shape.
|
|
330
|
+
* The BETWEEN-TURNS pending lane. A task notification born while the session has NO live lane at all
|
|
331
|
+
* (every run of it is over, or the one that is live rejected the injection because its agent was
|
|
332
|
+
* already idle) is parked PER SESSION; the session's next run drains it at start and rides the payloads
|
|
333
|
+
* down the SAME notification lane (steer at the next turn boundary), so in-turn and between-turn events
|
|
334
|
+
* keep one shape. A frame whose session DOES have a live lane never reaches this store —
|
|
335
|
+
* {@link SessionLiveNotificationLanes} takes it to that run's next turn boundary.
|
|
341
336
|
*
|
|
342
337
|
* Bounds (1.254 F4 家族纪律): per-task cap + per-session cap + session-count cap, oldest dropped first —
|
|
343
338
|
* preferring NON-terminal victims (an exit/killed terminal is the single most load-bearing notification of
|
|
@@ -416,8 +411,108 @@ export interface DrainedPendingNotifications {
|
|
|
416
411
|
*/
|
|
417
412
|
droppedWholeSession?: number;
|
|
418
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* One run's seat in {@link SessionLiveNotificationLanes}: hand this payload to that run's live
|
|
416
|
+
* injection queue. Four answers:
|
|
417
|
+
* · `"taken"` — newly enqueued on that run's lane; `onDisposition` (when supplied) is settled by the
|
|
418
|
+
* queue with the real outcome. Nothing is DELIVERED yet: the boundary can still refuse it;
|
|
419
|
+
* · `"pending"` — that lane already holds this exact key IN FLIGHT. `onDisposition` is NOT settled
|
|
420
|
+
* here: the in-flight entry owns the OUTCOME and this copy is no delivery;
|
|
421
|
+
* · `"duplicate"` — that stream has already DELIVERED this exact frame: nothing to enqueue, nothing
|
|
422
|
+
* to park; `onDisposition` is NOT settled, the word belongs to the caller;
|
|
423
|
+
* · `"unavailable"` — the lane can no longer take anything; the caller tries the next lane, and
|
|
424
|
+
* finally the parked store.
|
|
425
|
+
*/
|
|
426
|
+
export type LiveNotificationLane = (payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d: "queued" | "parked") => void) => "taken" | "pending" | "duplicate" | "unavailable";
|
|
427
|
+
/** One run's seat: its two entry points, plus the two bookkeeping verbs its owner drives. */
|
|
428
|
+
export interface LiveNotificationLaneSeat {
|
|
429
|
+
/**
|
|
430
|
+
* The entry for THIS run's OWN producers. It accepts whenever the lane is live: the frame is
|
|
431
|
+
* enqueued (so the queue's own dedup folds a retry), the wire event goes out, and if the agent turns
|
|
432
|
+
* out to be idle the steer's rejection parks it. No route-specific condition is allowed onto it.
|
|
433
|
+
*/
|
|
434
|
+
readonly take: LiveNotificationLane;
|
|
435
|
+
/**
|
|
436
|
+
* The entry {@link SessionLiveNotificationLanes} holds (stable identity for register/withdraw) — a
|
|
437
|
+
* frame ANOTHER run of the session is offering. It additionally requires that this lane can deliver
|
|
438
|
+
* at this instant: accepting a foreign frame and then refusing it asynchronously strands it behind
|
|
439
|
+
* this run's own parked-store intake, whereas declining hands it straight back to the router.
|
|
440
|
+
*/
|
|
441
|
+
readonly takeAsDestination: LiveNotificationLane;
|
|
442
|
+
/**
|
|
443
|
+
* Record that this lane HANDED this frame to the session route: it accepted the frame and then
|
|
444
|
+
* could not deliver it (its agent was already idle). The key must never come back — the queue's
|
|
445
|
+
* dedup set keeps it, so a second enqueue would be reported as ACCEPTED while nothing delivers or
|
|
446
|
+
* parks it. Remembered per LANE rather than per delivery, so a route of any length terminates on the
|
|
447
|
+
* parked store. A lane that is no longer live records NOTHING (it already answers "unavailable" by
|
|
448
|
+
* liveness). What IS remembered is bounded exactly like the queue's own dedup set — same cap, same
|
|
449
|
+
* insertion-order eviction, and an evicted key is as re-deliverable here as it is there.
|
|
450
|
+
*/
|
|
451
|
+
handOff(payload: TaskNotificationPayload): void;
|
|
452
|
+
/** Drop that memory when the lane stops; every later answer is "unavailable" by liveness anyway. */
|
|
453
|
+
release(): void;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Build one run's lane seat over its injection queue: the SINGLE enqueue site a frame reaches,
|
|
457
|
+
* whether the run produced it or another run of the same session routed it here — so the tier
|
|
458
|
+
* routing, the dedup key and the already-delivered fold cannot fork between the two arrivals.
|
|
459
|
+
*/
|
|
460
|
+
export declare function createLiveNotificationLane(seat: {
|
|
461
|
+
/** The run's injection queue; its subscriber is what routes an accepted frame by tier. */
|
|
462
|
+
queue: SystemInjectionQueue;
|
|
463
|
+
/**
|
|
464
|
+
* Is the agent in a turn, so the boundary a frame would ride actually exists? Read LIVE, and read
|
|
465
|
+
* ONLY by {@link LiveNotificationLaneSeat.takeAsDestination}.
|
|
466
|
+
*/
|
|
467
|
+
canTakeNow: () => boolean;
|
|
468
|
+
/**
|
|
469
|
+
* Has this lane NOT been torn down? The gate for this run's own producers, and the gate on
|
|
470
|
+
* remembering a handoff: a lane merely BETWEEN turns may take again in a moment, so what it handed
|
|
471
|
+
* away must still be remembered; a lane that is over will never take anything.
|
|
472
|
+
*/
|
|
473
|
+
isLive: () => boolean;
|
|
474
|
+
/** Keys this run's stream already delivered at turn-open (the cross-layer double-park fold). */
|
|
475
|
+
alreadyDelivered: (dedupKey: string) => boolean;
|
|
476
|
+
}): LiveNotificationLaneSeat;
|
|
477
|
+
/**
|
|
478
|
+
* The per-session LIVE LANE registry — the destination half of {@link PendingSessionNotifications}.
|
|
479
|
+
* A run registers its injection lane once its session anchor is bound and withdraws it the moment the
|
|
480
|
+
* lane stops accepting (a clean end, a throw, an abort — the run's own teardown), so an entry's
|
|
481
|
+
* lifetime is exactly the lane's liveness.
|
|
482
|
+
*
|
|
483
|
+
* The key is the session the producing lane is ANCHORED to, so this is never a cross-session route: a
|
|
484
|
+
* peer session's live run is not a destination, and a delegated child's own session is not its
|
|
485
|
+
* parent's. A session with no live lane keeps the parked form unchanged.
|
|
486
|
+
*
|
|
487
|
+
* Several lanes may sit under one key: the NEWEST registration is tried first — "the session's current
|
|
488
|
+
* run" — and every lane is tried once, so a lane that has stopped, or that has already handed this very
|
|
489
|
+
* frame away, is passed over rather than swallowing it. A lane that refused a frame therefore cannot be
|
|
490
|
+
* reached by it again, which is what makes a route of any length terminate on the parked store.
|
|
491
|
+
*/
|
|
492
|
+
export declare class SessionLiveNotificationLanes {
|
|
493
|
+
private readonly lanes;
|
|
494
|
+
register(sessionId: string, lane: LiveNotificationLane): void;
|
|
495
|
+
/** Idempotent (a lane withdraws at its stop AND again at its outermost teardown). The key is dropped
|
|
496
|
+
* with its last lane so an idle process holds no per-session entry. */
|
|
497
|
+
unregister(sessionId: string, lane: LiveNotificationLane): void;
|
|
498
|
+
/**
|
|
499
|
+
* Offer the payload to the session's live lanes, newest first. The caller's OWN lane is not
|
|
500
|
+
* special-cased: a lane that just refused this frame reports `"unavailable"` for it from then on.
|
|
501
|
+
*/
|
|
502
|
+
deliver(sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, opts?: {
|
|
503
|
+
onDisposition?: (d: "queued" | "parked") => void;
|
|
504
|
+
}): "taken" | "pending" | "duplicate" | "unavailable";
|
|
505
|
+
/** Sessions holding at least one live lane (observability + the tests' rollback detector). */
|
|
506
|
+
get size(): number;
|
|
507
|
+
}
|
|
419
508
|
export declare class PendingSessionNotifications {
|
|
420
509
|
private readonly sessions;
|
|
510
|
+
/**
|
|
511
|
+
* The LIVE half of the same question this store answers. Together they are one seat — "where does a
|
|
512
|
+
* notification addressed to session S go?" — with two answers: the session's live lane when it has
|
|
513
|
+
* one, this parked queue when it does not. One object, one lifetime, one key (the session id).
|
|
514
|
+
*/
|
|
515
|
+
readonly liveLanes: SessionLiveNotificationLanes;
|
|
421
516
|
/** Whole sessions evicted by {@link MAX_PENDING_SESSIONS} (their pendings were never delivered). */
|
|
422
517
|
droppedSessions: number;
|
|
423
518
|
/**
|
|
@@ -438,8 +533,39 @@ export declare class PendingSessionNotifications {
|
|
|
438
533
|
* design/373 §3.5: the batch comes out PRIORITY-MAJOR (stable sort — pend order within a class;
|
|
439
534
|
* a no-priority record ranks as later), the idle-dequeue min-value-first form. */
|
|
440
535
|
drain(sessionId: string): DrainedPendingNotifications | undefined;
|
|
536
|
+
/**
|
|
537
|
+
* Keys a live lane of this session has already put in front of a model (the delivered half of the
|
|
538
|
+
* ledger below). Kept beside the parked keys because the WINDOW is the session's, not any one run's:
|
|
539
|
+
* the destination run's own queue dedup dies with it. Released at {@link drain}, exactly like the
|
|
540
|
+
* parked keys. Bounded like everything else here; an evicted key only ever costs a duplicate in the
|
|
541
|
+
* extreme, never a loss.
|
|
542
|
+
*/
|
|
543
|
+
private readonly routed;
|
|
544
|
+
/**
|
|
545
|
+
* Does this session ALREADY own this exact frame — parked, or routed onto a live lane? Only one of
|
|
546
|
+
* the two may hold a key: a producer retrying a frame the session has already taken must FOLD rather
|
|
547
|
+
* than be handed to whichever run has since gone live, which would deliver it there and again at the
|
|
548
|
+
* next drain. Callers ask BEFORE routing and BEFORE parking.
|
|
549
|
+
*/
|
|
550
|
+
holdsKey(sessionId: string, dedupKey: string): boolean;
|
|
551
|
+
/** Record that this session has DELIVERED this frame — see {@link routed}. Written at ONE place, the
|
|
552
|
+
* harness's consumption sink (the step where a queued frame enters the model's input), so the record
|
|
553
|
+
* can never run ahead of the delivery it claims. */
|
|
554
|
+
markDelivered(sessionId: string, dedupKey: string): void;
|
|
441
555
|
get size(): number;
|
|
442
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* WHERE A FRAME ADDRESSED TO ONE SESSION GOES — the single decision, so the two addresses that ask it
|
|
559
|
+
* (a run's own session, and the delegation tree's root anchor) cannot drift apart. Three outcomes:
|
|
560
|
+
* · the session ALREADY OWNS the key (parked, or routed onto a live lane) ⇒ fold — neither route it
|
|
561
|
+
* nor park a second copy; `"parked"` is the word a folded repeat has always been given;
|
|
562
|
+
* · a live lane takes it ⇒ it rides that run's next turn boundary. This decision records NOTHING in
|
|
563
|
+
* the ledger: a destination that ACCEPTED a frame can still refuse it at its boundary and park it
|
|
564
|
+
* then, so only the lane that actually delivers writes ownership
|
|
565
|
+
* ({@link LiveNotificationLaneSeat.markDelivered});
|
|
566
|
+
* · nothing can take it ⇒ park, carrying the tier, for the session's next run.
|
|
567
|
+
*/
|
|
568
|
+
export declare function routeOrParkNotification(store: PendingSessionNotifications, sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d: "queued" | "parked") => void): void;
|
|
443
569
|
/**
|
|
444
570
|
* Delivery-side overflow disclosure: fold the per-task drop counts into the drained payloads. Two
|
|
445
571
|
* carrier shapes, chosen per survivor:
|