@sema-agent/core 7.18.0 → 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 +93 -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/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/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 +43 -14
- package/dist/core/auto-mode.js +79 -10
- package/dist/core/checkpoint-store.d.ts +7 -0
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/engine-notice.d.ts +30 -0
- package/dist/core/gate-fold.js +5 -1
- package/dist/core/gate-lanes.js +7 -7
- package/dist/core/gate-outcome.d.ts +25 -0
- package/dist/core/gate-outcome.js +7 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -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/classifier-ladder.d.ts +149 -11
- package/dist/core/runner/classifier-ladder.js +159 -10
- package/dist/core/runner/contracts.d.ts +2 -0
- package/dist/core/runner/denial-limit-arms.js +2 -2
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
- package/dist/core/runner/prepare-config-doors.d.ts +6 -0
- package/dist/core/runner/prepare-config-doors.js +8 -10
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +3 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/run-leg.js +10 -4
- package/dist/core/runner/run-turn-boundary.js +12 -4
- package/dist/core/runner/synthetic-tools.d.ts +67 -29
- package/dist/core/runner/synthetic-tools.js +51 -22
- package/dist/core/runner/turn-attachments.d.ts +43 -12
- package/dist/core/runner/turn-attachments.js +58 -14
- 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/task-spec.d.ts +26 -0
- package/dist/core/tool-policy.d.ts +19 -4
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-spec.d.ts +37 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/core/write-protect.d.ts +8 -3
- package/dist/core/write-protect.js +8 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -6
- package/dist/tools/fs/fs-bash.js +7 -3
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/safety.d.ts +15 -0
- package/dist/tools/fs/safety.js +7 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +15 -5
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
|
@@ -574,7 +574,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
574
574
|
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
|
-
const projectContext = await prepareProjectContext({ deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume });
|
|
577
|
+
const projectContext = await prepareProjectContext({ deps, spec, internals, session, sessionId, hostTaskId, taskRootFinal, handsEnabled, delegation, seedContextFiles, memoryBlockFromEngine, skillScope, roster, toolFaceSnapshot, onceLedger, memoryTools, memoryEngineSession, memorySearchToolsPlanned, memoryPairExcludedName, memoryPairOccupiedName, resume, skillListingBudget: doors.skillListingBudget });
|
|
578
578
|
const { memoryBlock: memoryBlockWithProject, instructionSources, projectInstructionContent, hasSkillManifest, skillsListing, sharedMemoryPairMounted, memoryEnginePairMounted } = projectContext;
|
|
579
579
|
const { provider, featureFlags, userSystemPrompt, userAppendSystemPrompt, mcpInstructionsBlock, envFacts, tzValid, userTz, gitStatusRef } = await preparePromptInputs({ spec, deps, failClosedReason: selfOrchestrationFailClosedReason(spec, deps), lockedPreflight, selfOrchestrationActive, executionEnv, reminderMark, resolvedReadFace, resolvedWriteFace, toolFaceSnapshot, promptProfile, fableMitigations, internals, thinking, ownedEnv, delegation, resolvedRole, mcp, model, externalContentTargetActive, resume, handsEnabled, taskRootFinal, additionalRootsCanonical, additionalReadRootsCanonical, sessionId, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}) });
|
|
580
580
|
const { deferred, memoryBlock, toolSearch: toolSearchPolicy } = prepareDeferClassify({ spec, deps, roster, toolFaceSnapshot, handsEnabled, fullShellReachable, promptProfile, model, mcp, a2a, sharedMemoryPairMounted, memoryEnginePairMounted, memoryBlock: memoryBlockWithProject, memoryRecallSegment, onceLedger, sessionId });
|
|
@@ -2,6 +2,7 @@ import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
|
2
2
|
import { isAskOrigin } from "../ask-origin.js";
|
|
3
3
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
4
4
|
import { CheckpointError, remainingBudgetMicroUsd } from "../checkpoint-store.js";
|
|
5
|
+
import { settledByPerson } from "../gate-outcome.js";
|
|
5
6
|
import { cloneObserverInput, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
|
|
6
7
|
import { checkToolPolicyProjection, refuseOutOfContractDecision } from "../tool-policy.js";
|
|
7
8
|
import { callFaceSeat, parkedCallFaceOf } from "../tool-roster.js";
|
|
@@ -170,10 +171,9 @@ export async function resumeApply(input) {
|
|
|
170
171
|
throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending", { reason: "origin_missing" });
|
|
171
172
|
}
|
|
172
173
|
const facts = plainPolicyOutcome.hostDecision;
|
|
173
|
-
const approverCell = facts.approver !== undefined ? { approver: facts.approver } : {};
|
|
174
174
|
const settlement = facts.decidedBy === "sla_timeout"
|
|
175
|
-
? { kind: "park_sla_expired", who: { party: "host", ...
|
|
176
|
-
:
|
|
175
|
+
? { kind: "park_sla_expired", who: { party: "host", ...(facts.approver !== undefined ? { approver: facts.approver } : {}) }, when: Date.now() }
|
|
176
|
+
: settledByPerson(plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", facts.approver, plainPolicyOutcome.decision === "deny" ? plainPolicyOutcome.reason : undefined);
|
|
177
177
|
plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
|
|
178
178
|
}
|
|
179
179
|
const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
|
|
@@ -26,7 +26,8 @@ import { maybeHumanRejectionHalt } from "./prepare-gate-stations.js";
|
|
|
26
26
|
import { nextHumanInputSeq } from "./steer-admission.js";
|
|
27
27
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
28
28
|
import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
|
|
29
|
-
import {
|
|
29
|
+
import { announceSkillListingTruncation } from "../skill-listing-budget.js";
|
|
30
|
+
import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingDelta, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
|
|
30
31
|
export async function runLeg(input) {
|
|
31
32
|
const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
|
|
32
33
|
let final;
|
|
@@ -331,6 +332,7 @@ export async function runLeg(input) {
|
|
|
331
332
|
}
|
|
332
333
|
}
|
|
333
334
|
const firstFrames = [];
|
|
335
|
+
let skillsListingCost;
|
|
334
336
|
if (rs.attach.attachState !== undefined) {
|
|
335
337
|
const attach = rs.attach.attachState;
|
|
336
338
|
if (rs.attach.agentListingOn && prepared.agentListing !== undefined) {
|
|
@@ -351,14 +353,15 @@ export async function runLeg(input) {
|
|
|
351
353
|
}
|
|
352
354
|
if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined) {
|
|
353
355
|
const listing = prepared.skillsListing;
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
+
const frame = renderSkillsListingFrame(attach, listing.entries, listing.budget);
|
|
357
|
+
if (frame !== undefined) {
|
|
356
358
|
firstFrames.push({
|
|
357
359
|
source: "skills_listing",
|
|
358
|
-
body,
|
|
360
|
+
body: frame.text,
|
|
359
361
|
commit: () => {
|
|
360
362
|
commitSkillsListing(attach, listing.entries);
|
|
361
363
|
prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
|
|
364
|
+
skillsListingCost = { total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget };
|
|
362
365
|
},
|
|
363
366
|
});
|
|
364
367
|
}
|
|
@@ -432,6 +435,9 @@ export async function runLeg(input) {
|
|
|
432
435
|
rs.attach.attachmentsInjected += firstFrames.length;
|
|
433
436
|
if (firstFrames.length > 0 && prepared.announcedSnapshotRecovered)
|
|
434
437
|
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
438
|
+
if (skillsListingCost !== undefined) {
|
|
439
|
+
announceSkillListingTruncation(runner.deps.onNotice, { ...skillsListingCost, sessionId: prepared.sessionId });
|
|
440
|
+
}
|
|
435
441
|
const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
|
|
436
442
|
queue.push({
|
|
437
443
|
...buildHumanInputEvent({
|
|
@@ -4,7 +4,8 @@ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
|
4
4
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
|
|
5
5
|
import { emitTrace } from "../trace.js";
|
|
6
6
|
import { redactThenCut } from "../../agents/subagent-steps.js";
|
|
7
|
-
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, rebaseCadenceWindows, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch,
|
|
7
|
+
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, rebaseCadenceWindows, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingFrame, renderToolsDelta } from "./turn-attachments.js";
|
|
8
|
+
import { announceSkillListingTruncation } from "../skill-listing-budget.js";
|
|
8
9
|
import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
9
10
|
import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
10
11
|
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
@@ -202,6 +203,7 @@ export function createTurnBoundary(input) {
|
|
|
202
203
|
}
|
|
203
204
|
let boundaryAttachmentBytes = 0;
|
|
204
205
|
let attachmentsPayload;
|
|
206
|
+
let skillsListingCost;
|
|
205
207
|
if ((rs.attach.dateState !== undefined ||
|
|
206
208
|
rs.attach.instrState !== undefined ||
|
|
207
209
|
rs.attach.sizeGuidelineState !== undefined ||
|
|
@@ -313,7 +315,9 @@ export function createTurnBoundary(input) {
|
|
|
313
315
|
...(prepared.agentListing.models !== undefined ? { agentModels: prepared.agentListing.models } : {}),
|
|
314
316
|
}
|
|
315
317
|
: {}),
|
|
316
|
-
...(rs.attach.skillsListingOn && prepared.skillsListing !== undefined
|
|
318
|
+
...(rs.attach.skillsListingOn && prepared.skillsListing !== undefined
|
|
319
|
+
? { skillsListing: prepared.skillsListing.entries, skillsListingBudget: prepared.skillsListing.budget }
|
|
320
|
+
: {}),
|
|
317
321
|
...(mcpAdds !== undefined || mcpRemovals !== undefined
|
|
318
322
|
? { mcpInstructions: { added: mcpAdds ?? [], removed: mcpRemovals ?? [] } }
|
|
319
323
|
: {}),
|
|
@@ -377,9 +381,10 @@ export function createTurnBoundary(input) {
|
|
|
377
381
|
}
|
|
378
382
|
if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined && due.some((a) => a.source === "skills_listing")) {
|
|
379
383
|
const listing = prepared.skillsListing;
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
384
|
+
const intact = renderSkillsListingFrame(rs.attach.attachState, listing.entries, listing.budget);
|
|
385
|
+
if (intact !== undefined && due.some((a) => a.source === "skills_listing" && a.body === intact.text)) {
|
|
382
386
|
commitSkillsListing(rs.attach.attachState, listing.entries);
|
|
387
|
+
skillsListingCost = { total: intact.listed, nameOnly: intact.nameOnly, dropped: intact.dropped, budget: listing.budget };
|
|
383
388
|
prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
|
|
384
389
|
if (prepared.announcedSnapshotRecovered)
|
|
385
390
|
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
@@ -474,6 +479,9 @@ export function createTurnBoundary(input) {
|
|
|
474
479
|
if ((attachmentsPayload !== undefined || batchContextBlock !== undefined) && prepared.batchHaltRef.current === undefined) {
|
|
475
480
|
const payload = attachmentsPayload !== undefined && batchContextBlock !== undefined ? `${attachmentsPayload}\n${batchContextBlock}` : (attachmentsPayload ?? batchContextBlock);
|
|
476
481
|
void prepared.harness.steer(payload, { engineMinted: true }).catch(() => { });
|
|
482
|
+
if (skillsListingCost !== undefined) {
|
|
483
|
+
announceSkillListingTruncation(deps.runner.deps.onNotice, { ...skillsListingCost, sessionId: prepared.sessionId });
|
|
484
|
+
}
|
|
477
485
|
}
|
|
478
486
|
const forceManual = manualCompactRef.requested;
|
|
479
487
|
if ((!withinTaskCompaction && !forceManual) ||
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type TSchema } from "typebox";
|
|
2
2
|
import type { AgentTool } from "../../internal/harness-types.js";
|
|
3
3
|
import type { SkillManifest } from "../types.js";
|
|
4
|
+
import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
|
|
4
5
|
import type { ActiveSkillScope } from "./active-skill-scope.js";
|
|
5
6
|
/** Reserved name of the injected structured-output tool (CC 2.1.187 `StructuredOutput`; batch-A rename — old
|
|
6
7
|
* "submit_output" is canonicalized for pre-rename durable state). */
|
|
@@ -57,15 +58,11 @@ export declare const SKILL_TOOL_NAME = "Skill";
|
|
|
57
58
|
* an oversized skill is excluded from the disclosure block and the tool, with an operator-visible config error. The compaction-time retention caps are a
|
|
58
59
|
* different door, deliberately. */
|
|
59
60
|
export declare const SKILL_CONTENT_MAX_CHARS: number;
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
* the
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
* (fences, headers, markers, joiners included), so `buildSkillsBlock` output always fits the
|
|
66
|
-
* bundle cap solo and an intact commit is always reachable. MUST stay ≤ ATTACHMENT_BYTE_CAP
|
|
67
|
-
* (turn-attachments.ts; not imported here — that module imports this one — pinned by test instead). */
|
|
68
|
-
export declare const SKILLS_BLOCK_MAX_BYTES = 8000;
|
|
61
|
+
/** The listing's budget vocabulary lives in `core/skill-listing-budget.ts` (the seat reader that lowers
|
|
62
|
+
* the two `TaskSpec` knobs to these numbers); re-exported here because this module is where every
|
|
63
|
+
* existing consumer of the cap imports it from, and the ceiling's structural reason is a property of
|
|
64
|
+
* the delivery lane rather than of the renderer. */
|
|
65
|
+
export { SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
|
|
69
66
|
/** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face budget, distinct from the (gone) body truncation door:
|
|
70
67
|
* the body is delivered untruncated at invoke time (CC parity), while attachments ride along bounded and truncation-disclosed. */
|
|
71
68
|
export declare const SKILL_ATTACHMENTS_MAX_CHARS = 50000;
|
|
@@ -123,31 +120,73 @@ export declare const SKILLS_LISTING_PROBE_HEADER = "The following skills are ava
|
|
|
123
120
|
/** [c209-C] One skill's disclosure line — `- name: description[(files 尾缀)]` with the per-skill
|
|
124
121
|
* description cap. Single-sourced so the skills_listing DELTA frame (turn-attachments) renders adds
|
|
125
122
|
* with byte-identical line shape to the initial `<skills>` block. */
|
|
126
|
-
export declare function skillListingLine(s: SkillListingEntry): string;
|
|
123
|
+
export declare function skillListingLine(s: SkillListingEntry, descMaxChars?: number): string;
|
|
124
|
+
/** What one {@link renderSkillsBlock} call produced: the block, and which skills the budget degraded. */
|
|
125
|
+
export interface SkillsBlockRender {
|
|
126
|
+
/** The rendered block — what {@link buildSkillsBlock} returns. */
|
|
127
|
+
text: string;
|
|
128
|
+
/** Skills listed WITHOUT their description (tier B's unspent candidates, or every listed name under
|
|
129
|
+
* tier C), in display order. Raw names: this is host-facing structured data, not prompt text. */
|
|
130
|
+
nameOnly: readonly string[];
|
|
131
|
+
/** Skills the block could not list at all (tier C's overflow tail). The `Skill` tool still serves them. */
|
|
132
|
+
dropped: readonly string[];
|
|
133
|
+
}
|
|
134
|
+
/** The text-only face of {@link renderSkillsBlock} — the `<skills>` BLOCK's face, not the listing
|
|
135
|
+
* family's: the drift frame renders its own arms through the shared tier engine
|
|
136
|
+
* ({@link renderSkillLinesWithin}), which is where "one ruler for every frame" lives. */
|
|
137
|
+
export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
|
|
127
138
|
/** Render the `<skills>` disclosure block (metadata only — bodies stay behind the tool).
|
|
128
139
|
* [c209-C] REVERSAL: no longer composed into the stable system prompt — this is now the RENDERER for
|
|
129
140
|
* the `skills_listing` initial frame (first-user-turn reminder / boundary re-announce), so the
|
|
130
141
|
* cached prefix carries zero skill bytes (design/C §2.3; CC 209 classic ships the same listing as a
|
|
131
142
|
* first-turn reminder; Q6: 8KB cap ≈ CC's observed 5.9KB listing).
|
|
132
143
|
*
|
|
133
|
-
*
|
|
134
|
-
* {@link
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
144
|
+
* The fence and its four header lines are this lane's frame; the budget arithmetic and the three
|
|
145
|
+
* degradation tiers belong to {@link renderSkillLinesWithin}, which the drift frame renders through
|
|
146
|
+
* as well. {@link buildSkillsBlock} is the text-only face; this one additionally reports what the
|
|
147
|
+
* budget cost, for the disclosure lane. */
|
|
148
|
+
export declare function renderSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsBlockRender | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* The three honesty tiers over ONE set of skills, measured byte-exact inside a caller-supplied FRAME —
|
|
151
|
+
* the engine both {@link renderSkillsBlock} and the drift frame's ADD arm render through, so the
|
|
152
|
+
* initial `<skills>` block and a later "new skills are now available" announcement degrade by the same
|
|
153
|
+
* rules against the same number. A lane that folded by its own rules (or by none) renders a body the
|
|
154
|
+
* bundle cap CLIPS, and a clipped body can never equal the intact-survival re-render, so the
|
|
155
|
+
* announcement replays at every boundary for the life of the task — the failure
|
|
156
|
+
* `@contract skills.listing_budget_ceiling` names, on whichever lane skipped the measurement.
|
|
157
|
+
*
|
|
158
|
+
* `frame` renders the body lines into the shape its lane carries; its fixed cost is priced once, by
|
|
159
|
+
* rendering it empty, so the tiers below decide against the bytes that actually ship.
|
|
160
|
+
*
|
|
161
|
+
* [c209-C] R2 C4① — the cap is BYTE-EXACT over the whole rendered frame (see
|
|
162
|
+
* {@link SKILLS_BLOCK_MAX_BYTES}): three honesty tiers, each chosen with the LATER tiers' fixed
|
|
163
|
+
* overhead (cap marker / name-only tail / overflow count line) already reserved so the final
|
|
164
|
+
* render can never blow the budget it was sized against —
|
|
165
|
+
* A. everything fits → every skill gets its full `- name: description` line (byte-identical to
|
|
166
|
+
* the pre-C4 render for every block that was legal under the old cap);
|
|
167
|
+
* B. (CC 220 `sbs` @317547-317586) — reserve the cap marker + EVERY skill's name-only line
|
|
168
|
+
* first, then spend the remaining slack on descriptions in PRIORITY order
|
|
169
|
+
* ({@link SkillListingEntry.declaredRank}), admitting each description whose incremental cost
|
|
170
|
+
* still fits and skipping (not stopping at) the ones that do not. Every skill stays listed;
|
|
171
|
+
* the ones that lose their description are the low-priority ones, not the alphabetically late
|
|
172
|
+
* ones. This replaces the prior "longest full-line PREFIX" cut, which had two defects the
|
|
173
|
+
* anchor does not: the surviving set was decided by name sort order (a skill named `zip-deploy`
|
|
174
|
+
* lost its description to `aardvark-lint` for no reason connected to either skill), and one
|
|
175
|
+
* oversized description truncated the block at itself instead of being skipped over;
|
|
176
|
+
* C. even all-name-only overflows (pathological name census) → list what fits and close with an
|
|
177
|
+
* honest `(… +N more skills …)` count line — the tool still serves all of them.
|
|
178
|
+
*
|
|
179
|
+
* Pure: the same entries, budget and frame always render the same bytes, which is what the
|
|
180
|
+
* prompt-prefix stability contract requires of the listing frames.
|
|
181
|
+
*/
|
|
182
|
+
export declare function renderSkillLinesWithin(skills: ReadonlyArray<SkillListingEntry>, opts: {
|
|
183
|
+
/** The whole frame's byte budget. Re-clamped to {@link SKILLS_BLOCK_MAX_BYTES} here, not only in
|
|
184
|
+
* the seat reader: this renderer is reachable from every listing lane, and the ceiling invariant
|
|
185
|
+
* has to hold for a caller that computed its own number. */
|
|
186
|
+
blockMaxBytes: number;
|
|
187
|
+
descMaxChars: number;
|
|
188
|
+
frame: (lines: readonly string[]) => string;
|
|
189
|
+
}): SkillsBlockRender | undefined;
|
|
151
190
|
/**
|
|
152
191
|
* The injected `skill` tool: returns a skill's full body as a tool result.
|
|
153
192
|
*
|
|
@@ -164,4 +203,3 @@ export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry
|
|
|
164
203
|
* widening, so it stays safe-by-construction.
|
|
165
204
|
*/
|
|
166
205
|
export declare function createSkillTool(skills: SkillLike[], scope?: ActiveSkillScope): AgentTool;
|
|
167
|
-
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool } from "../tools.js";
|
|
3
3
|
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
4
|
+
import { SKILLS_BLOCK_MAX_BYTES as SKILL_LISTING_CEILING_BYTES, SKILL_LISTING_DESC_MAX_CHARS_DEFAULT, } from "../skill-listing-budget.js";
|
|
4
5
|
import { toolFace } from "../tool-catalog-entries.js";
|
|
5
6
|
import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
|
|
6
7
|
export const OUTPUT_TOOL_NAME = "StructuredOutput";
|
|
@@ -85,8 +86,7 @@ export function createReportFindingsTool() {
|
|
|
85
86
|
}
|
|
86
87
|
export const SKILL_TOOL_NAME = "Skill";
|
|
87
88
|
export const SKILL_CONTENT_MAX_CHARS = 1024 * 1024;
|
|
88
|
-
|
|
89
|
-
export const SKILLS_BLOCK_MAX_BYTES = 8_000;
|
|
89
|
+
export { SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
|
|
90
90
|
export const SKILL_ATTACHMENTS_MAX_CHARS = 50_000;
|
|
91
91
|
export const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
|
|
92
92
|
const SKILL_FILES_LIST_MAX_CHARS = 200;
|
|
@@ -121,18 +121,18 @@ export function normalizeSkills(skills) {
|
|
|
121
121
|
return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
122
122
|
}
|
|
123
123
|
export const SKILLS_LISTING_PROBE_HEADER = "The following skills are available for this task.";
|
|
124
|
-
export function skillListingLine(s) {
|
|
125
|
-
const desc = s.description.length >
|
|
126
|
-
? `${s.description.slice(0, SKILL_DESC_MAX_CHARS - 1)}…`
|
|
127
|
-
: s.description;
|
|
124
|
+
export function skillListingLine(s, descMaxChars = SKILL_LISTING_DESC_MAX_CHARS_DEFAULT) {
|
|
125
|
+
const desc = s.description.length > descMaxChars ? `${s.description.slice(0, descMaxChars - 1)}…` : s.description;
|
|
128
126
|
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
129
127
|
return `- ${safe(s.name)}: ${safe(desc)}${safe(skillFilesSuffix(s.files))}`;
|
|
130
128
|
}
|
|
131
129
|
const SKILLS_BLOCK_CAP_MARKER = "(block size cap reached — lower-priority skills are listed name-only; descriptions via the tool)";
|
|
132
|
-
export function buildSkillsBlock(skills) {
|
|
130
|
+
export function buildSkillsBlock(skills, budget) {
|
|
131
|
+
return renderSkillsBlock(skills, budget)?.text;
|
|
132
|
+
}
|
|
133
|
+
export function renderSkillsBlock(skills, budget) {
|
|
133
134
|
if (skills.length === 0)
|
|
134
135
|
return undefined;
|
|
135
|
-
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
136
136
|
const headerLines = [
|
|
137
137
|
"<skills>",
|
|
138
138
|
`${SKILLS_LISTING_PROBE_HEADER} A skill is a named package of instructions;`,
|
|
@@ -140,18 +140,34 @@ export function buildSkillsBlock(skills) {
|
|
|
140
140
|
`instructions by calling the \`${SKILL_TOOL_NAME}\` tool with \`skill: "<name>"\` (plus optional \`args\`).`,
|
|
141
141
|
"",
|
|
142
142
|
];
|
|
143
|
-
|
|
143
|
+
return renderSkillLinesWithin(skills, {
|
|
144
|
+
blockMaxBytes: budget?.blockMaxBytes ?? SKILL_LISTING_CEILING_BYTES,
|
|
145
|
+
descMaxChars: budget?.descMaxChars ?? SKILL_LISTING_DESC_MAX_CHARS_DEFAULT,
|
|
146
|
+
frame: (lines) => [...headerLines, ...lines, "</skills>"].join("\n"),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export function renderSkillLinesWithin(skills, opts) {
|
|
150
|
+
if (skills.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
const blockMaxBytes = Math.min(opts.blockMaxBytes, SKILL_LISTING_CEILING_BYTES);
|
|
153
|
+
const descMaxChars = opts.descMaxChars;
|
|
154
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
155
|
+
const render = opts.frame;
|
|
144
156
|
const lineCost = (l) => bytes(l) + 1;
|
|
145
|
-
const
|
|
146
|
-
const fullLines = skills.map((s) => skillListingLine(s));
|
|
147
|
-
const nameLines = skills.map((s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`);
|
|
157
|
+
const bodyBudget = blockMaxBytes - bytes(render([]));
|
|
158
|
+
const fullLines = skills.map((s) => skillListingLine(s, descMaxChars));
|
|
148
159
|
const fullTotal = fullLines.reduce((acc, l) => acc + lineCost(l), 0);
|
|
149
|
-
if (fullTotal <=
|
|
150
|
-
return render(fullLines);
|
|
160
|
+
if (fullTotal <= bodyBudget)
|
|
161
|
+
return { text: render(fullLines), nameOnly: [], dropped: [] };
|
|
162
|
+
const nameLines = skills.map((s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`);
|
|
163
|
+
const namePrefix = new Array(nameLines.length + 1);
|
|
164
|
+
namePrefix[0] = 0;
|
|
165
|
+
for (let i = 0; i < nameLines.length; i++)
|
|
166
|
+
namePrefix[i + 1] = namePrefix[i] + lineCost(nameLines[i]);
|
|
151
167
|
const markerCost = lineCost(SKILLS_BLOCK_CAP_MARKER);
|
|
152
|
-
const floorCost = markerCost + nameLines.
|
|
153
|
-
if (floorCost <=
|
|
154
|
-
let slack =
|
|
168
|
+
const floorCost = markerCost + namePrefix[nameLines.length];
|
|
169
|
+
if (floorCost <= bodyBudget) {
|
|
170
|
+
let slack = bodyBudget - floorCost;
|
|
155
171
|
const byPriority = skills
|
|
156
172
|
.map((s, i) => ({ i, rank: s.declaredRank ?? i }))
|
|
157
173
|
.toSorted((a, b) => a.rank - b.rank || a.i - b.i);
|
|
@@ -163,15 +179,28 @@ export function buildSkillsBlock(skills) {
|
|
|
163
179
|
slack -= delta;
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
|
-
return
|
|
182
|
+
return {
|
|
183
|
+
text: render([SKILLS_BLOCK_CAP_MARKER, ...skills.map((_s, i) => (withDescription.has(i) ? fullLines[i] : nameLines[i]))]),
|
|
184
|
+
nameOnly: skills.filter((_s, i) => !withDescription.has(i)).map((s) => s.name),
|
|
185
|
+
dropped: [],
|
|
186
|
+
};
|
|
167
187
|
}
|
|
168
188
|
for (let m = skills.length - 1; m >= 0; m--) {
|
|
169
189
|
const overflowLine = `(… +${skills.length - m} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`;
|
|
170
|
-
const cost = markerCost +
|
|
171
|
-
if (cost <=
|
|
172
|
-
return
|
|
190
|
+
const cost = markerCost + namePrefix[m] + lineCost(overflowLine);
|
|
191
|
+
if (cost <= bodyBudget) {
|
|
192
|
+
return {
|
|
193
|
+
text: render([SKILLS_BLOCK_CAP_MARKER, ...nameLines.slice(0, m), overflowLine]),
|
|
194
|
+
nameOnly: skills.slice(0, m).map((s) => s.name),
|
|
195
|
+
dropped: skills.slice(m).map((s) => s.name),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
173
198
|
}
|
|
174
|
-
return
|
|
199
|
+
return {
|
|
200
|
+
text: render([SKILLS_BLOCK_CAP_MARKER, `(… +${skills.length} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`]),
|
|
201
|
+
nameOnly: [],
|
|
202
|
+
dropped: skills.map((s) => s.name),
|
|
203
|
+
};
|
|
175
204
|
}
|
|
176
205
|
export function createSkillTool(skills, scope) {
|
|
177
206
|
const byName = new Map(skills.map((s) => [s.name, s]));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpDroppedTool } from "../mcp.js";
|
|
2
2
|
import type { TaskSpec } from "../types.js";
|
|
3
|
-
import { type SkillListingEntry } from "./synthetic-tools.js";
|
|
3
|
+
import { type SkillListingEntry, type SkillsBlockRender } from "./synthetic-tools.js";
|
|
4
|
+
import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
|
|
4
5
|
/**
|
|
5
6
|
* Turn-boundary attachment seam: the PURE producer layer (CC `getContextAttachments`
|
|
6
7
|
* parity). This module is the single home of the CC-parity cadence judgment + reminder copy (提示词主权批:
|
|
@@ -545,6 +546,9 @@ export interface AttachmentInputs {
|
|
|
545
546
|
* the delta path serves dynamic-installation deployments and tests, same posture as
|
|
546
547
|
* `agentListing`). Only passed when `config.skillsListing` is on. */
|
|
547
548
|
skillsListing?: ReadonlyArray<SkillListingEntry>;
|
|
549
|
+
/** The lowered skills-listing budget seats, resolved once at the config door. Absent ⇒ the module
|
|
550
|
+
* defaults, i.e. byte-identical to the pre-seat render. */
|
|
551
|
+
skillsListingBudget?: ResolvedSkillListingBudget;
|
|
548
552
|
/** G1 续批 mcp_instructions: pending deltas snapshotted from `MaterializedMcp.instructionsDelta`.
|
|
549
553
|
* CALLER-owned consumption on intact cap survival (same `newTools` discipline). */
|
|
550
554
|
mcpInstructions?: {
|
|
@@ -849,21 +853,48 @@ export declare function commitAgentListing(state: AttachmentState, entries: Read
|
|
|
849
853
|
export declare const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
|
|
850
854
|
/** [c209-C] skills removal-arm header — mirrors the agent-listing removal arm's phrasing. */
|
|
851
855
|
export declare const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
|
|
856
|
+
/** What one {@link renderSkillsListingFrame} call produced: the render's own report, plus the
|
|
857
|
+
* denominator it is against. */
|
|
858
|
+
export interface SkillsListingFrameRender extends SkillsBlockRender {
|
|
859
|
+
/** How many skills this frame set out to list — the whole roster on an initial frame, the ADDED set
|
|
860
|
+
* on a drift frame. The disclosure's denominator, read off the frame that was actually delivered
|
|
861
|
+
* rather than re-derived from the roster. */
|
|
862
|
+
readonly listed: number;
|
|
863
|
+
}
|
|
852
864
|
/**
|
|
853
|
-
* [c209-C] skills_listing producer — the skills counterpart of {@link renderAgentListingDelta}
|
|
854
|
-
* First call (no announced map) = the full
|
|
855
|
-
* the exact bytes that used to sit in the stable system prompt
|
|
856
|
-
* classic ships its skills listing the same way, as a
|
|
857
|
-
* Later calls announce only drift, name-keyed like the
|
|
858
|
-
* {@link SKILLS_LISTING_DELTA_HEADER} with the SAME `- name: description` line
|
|
859
|
-
* removals under {@link SKILLS_LISTING_REMOVED_HEADER} + `- <name>` lines,
|
|
860
|
-
* CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (agent-removal-arm parity). A description
|
|
861
|
-
* existing name is not re-announced (name-keyed, agent-arm parity).
|
|
865
|
+
* [c209-C] skills_listing producer — the skills counterpart of {@link renderAgentListingDelta}, with
|
|
866
|
+
* the degradation report the disclosure lane announces. First call (no announced map) = the full
|
|
867
|
+
* `<skills>` disclosure block VERBATIM (the exact bytes that used to sit in the stable system prompt
|
|
868
|
+
* now ride the reminder lane; CC 209 classic ships its skills listing the same way, as a
|
|
869
|
+
* first-user-turn reminder ~5.9KB, report §5.1). Later calls announce only drift, name-keyed like the
|
|
870
|
+
* agent arm: adds under {@link SKILLS_LISTING_DELTA_HEADER} with the SAME `- name: description` line
|
|
871
|
+
* shape the block uses, removals under {@link SKILLS_LISTING_REMOVED_HEADER} + `- <name>` lines,
|
|
872
|
+
* followed by the CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (agent-removal-arm parity). A description
|
|
873
|
+
* change on an existing name is not re-announced (name-keyed, agent-arm parity).
|
|
874
|
+
*
|
|
875
|
+
* ONE RULER FOR EVERY FRAME: the drift frame is measured against the SAME resolved budget as the
|
|
876
|
+
* initial block, arms and joiners included, and its ADD arm degrades through the same three tiers
|
|
877
|
+
* ({@link renderSkillLinesWithin}). An unmeasured drift frame is the block's own failure mode one lane
|
|
878
|
+
* over: a roster's worth of new skills renders past `ATTACHMENT_BYTE_CAP`, `capAttachments` clips the
|
|
879
|
+
* body, a clipped body never equals the intact-survival re-render, and the frame is re-rendered and
|
|
880
|
+
* re-clipped at every boundary for the life of the task. The split: the ambient tail and the removal
|
|
881
|
+
* arm's one-line floor are reserved first (both constants), the ADD arm degrades into what is left,
|
|
882
|
+
* and the removal arm spends whatever the add arm did not — so the frame's size is the resolved budget
|
|
883
|
+
* or, when even the two floors do not fit it, a small constant, never a function of the roster.
|
|
884
|
+
*
|
|
885
|
+
* The report covers the ADD arm only: `nameOnly` / `dropped` name skills this frame listed without a
|
|
886
|
+
* description or could not list at all. A departed name that did not fit is not a listed skill.
|
|
862
887
|
*
|
|
863
888
|
* MED-3① discipline: PURE — the announced-set advance is CALLER-owned via {@link commitSkillsListing},
|
|
864
|
-
* committed only on first-frame delivery or intact cap survival at a boundary.
|
|
889
|
+
* committed only on first-frame delivery or intact cap survival at a boundary. The commit records
|
|
890
|
+
* EVERY entry, including one this frame dropped: the alternative is a frame re-rendering the same
|
|
891
|
+
* un-listable tail at every boundary, which is the replay this budget exists to end. The `Skill` tool
|
|
892
|
+
* serves a dropped skill, and `skills.listing_truncated` tells the deployment it happened.
|
|
865
893
|
*/
|
|
866
|
-
export declare function
|
|
894
|
+
export declare function renderSkillsListingFrame(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsListingFrameRender | undefined;
|
|
895
|
+
/** The text-only face of {@link renderSkillsListingFrame} — what the delivery lanes render, and what
|
|
896
|
+
* the intact-survival predicate re-renders to compare. */
|
|
897
|
+
export declare function renderSkillsListingDelta(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
|
|
867
898
|
/** [c209-C] commit half of {@link renderSkillsListingDelta} — same MED-3① contract as
|
|
868
899
|
* {@link commitAgentListing}: called by the run loop only on first-frame delivery or when the
|
|
869
900
|
* `skills_listing` block survived the capped bundle INTACT; a clipped/dropped block leaves the
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
2
2
|
import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
3
3
|
import { protocolOf } from "../protocol-table.js";
|
|
4
|
-
import {
|
|
4
|
+
import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
|
|
5
|
+
import { SKILL_LISTING_DESC_MAX_CHARS_DEFAULT, SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
|
|
5
6
|
import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
|
|
6
7
|
export const TODO_REMINDER_CONFIG = {
|
|
7
8
|
TURNS_SINCE_WRITE: 10,
|
|
@@ -209,7 +210,9 @@ export function collectDueAttachments(state, inp) {
|
|
|
209
210
|
const agentListingBody = inp.config.agentListing && inp.agentListing !== undefined
|
|
210
211
|
? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels)
|
|
211
212
|
: undefined;
|
|
212
|
-
const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined
|
|
213
|
+
const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined
|
|
214
|
+
? renderSkillsListingDelta(state, inp.skillsListing, inp.skillsListingBudget)
|
|
215
|
+
: undefined;
|
|
213
216
|
const listingPair = [];
|
|
214
217
|
if (agentListingBody !== undefined)
|
|
215
218
|
listingPair.push({ source: "agent_listing", body: agentListingBody });
|
|
@@ -545,10 +548,9 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
545
548
|
return blocks.join("\n\n");
|
|
546
549
|
}
|
|
547
550
|
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
551
|
+
const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
|
|
548
552
|
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name))).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
549
|
-
const removed = [...announced.keys()]
|
|
550
|
-
.filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)))
|
|
551
|
-
.sort();
|
|
553
|
+
const removed = [...announced.keys()].filter((n) => !entryIds.has(listingIdentity(n))).sort();
|
|
552
554
|
const modelsDrifted = state.announcedModels !== undefined &&
|
|
553
555
|
models !== undefined &&
|
|
554
556
|
(state.announcedModels.length !== models.length ||
|
|
@@ -594,26 +596,68 @@ export function commitAgentListing(state, entries, models) {
|
|
|
594
596
|
}
|
|
595
597
|
export const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
|
|
596
598
|
export const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
|
|
597
|
-
|
|
599
|
+
const SKILLS_DELTA_BLOCK_JOINER = "\n\n";
|
|
600
|
+
function skillsRemovalArmFloor(count) {
|
|
601
|
+
return [SKILLS_LISTING_REMOVED_HEADER, `(… +${count} more are no longer available)`].join("\n");
|
|
602
|
+
}
|
|
603
|
+
function renderSkillsRemovalArm(removed, maxBytes) {
|
|
604
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
605
|
+
const lines = removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`);
|
|
606
|
+
const head = bytes(SKILLS_LISTING_REMOVED_HEADER);
|
|
607
|
+
const lineCost = (l) => bytes(l) + 1;
|
|
608
|
+
const prefix = new Array(lines.length + 1);
|
|
609
|
+
prefix[0] = 0;
|
|
610
|
+
for (let i = 0; i < lines.length; i++)
|
|
611
|
+
prefix[i + 1] = prefix[i] + lineCost(lines[i]);
|
|
612
|
+
if (head + prefix[lines.length] <= maxBytes)
|
|
613
|
+
return [SKILLS_LISTING_REMOVED_HEADER, ...lines].join("\n");
|
|
614
|
+
for (let m = lines.length - 1; m >= 0; m--) {
|
|
615
|
+
const tail = `(… +${lines.length - m} more are no longer available)`;
|
|
616
|
+
if (head + prefix[m] + lineCost(tail) <= maxBytes)
|
|
617
|
+
return [SKILLS_LISTING_REMOVED_HEADER, ...lines.slice(0, m), tail].join("\n");
|
|
618
|
+
}
|
|
619
|
+
return skillsRemovalArmFloor(lines.length);
|
|
620
|
+
}
|
|
621
|
+
export function renderSkillsListingFrame(state, entries, budget) {
|
|
598
622
|
const announced = state.announcedSkills;
|
|
599
623
|
if (announced === undefined) {
|
|
600
624
|
if (entries.length === 0)
|
|
601
625
|
return undefined;
|
|
602
|
-
|
|
626
|
+
const block = renderSkillsBlock(entries, budget);
|
|
627
|
+
return block === undefined ? undefined : { ...block, listed: entries.length };
|
|
603
628
|
}
|
|
604
629
|
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
630
|
+
const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
|
|
605
631
|
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name)));
|
|
606
|
-
const removed = [...announced.keys()].filter((n) => !
|
|
632
|
+
const removed = [...announced.keys()].filter((n) => !entryIds.has(listingIdentity(n)));
|
|
607
633
|
if (added.length === 0 && removed.length === 0)
|
|
608
634
|
return undefined;
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
635
|
+
const bytes = (s) => Buffer.byteLength(s, "utf8");
|
|
636
|
+
const joiner = bytes(SKILLS_DELTA_BLOCK_JOINER);
|
|
637
|
+
const frameMaxBytes = Math.min(budget?.blockMaxBytes ?? SKILLS_BLOCK_MAX_BYTES, SKILLS_BLOCK_MAX_BYTES);
|
|
638
|
+
const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
|
|
639
|
+
const removalReserve = removed.length > 0 ? joiner + bytes(skillsRemovalArmFloor(removed.length)) : 0;
|
|
640
|
+
const addArm = added.length > 0
|
|
641
|
+
? renderSkillLinesWithin(added, {
|
|
642
|
+
blockMaxBytes: frameMaxBytes - ambientCost - removalReserve,
|
|
643
|
+
descMaxChars: budget?.descMaxChars ?? SKILL_LISTING_DESC_MAX_CHARS_DEFAULT,
|
|
644
|
+
frame: (lines) => [SKILLS_LISTING_DELTA_HEADER, ...lines].join("\n"),
|
|
645
|
+
})
|
|
646
|
+
: undefined;
|
|
647
|
+
const addCost = addArm === undefined ? 0 : bytes(addArm.text) + (removed.length > 0 ? joiner : 0);
|
|
648
|
+
const blocks = addArm === undefined ? [] : [addArm.text];
|
|
612
649
|
if (removed.length > 0) {
|
|
613
|
-
blocks.push(
|
|
614
|
-
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
650
|
+
blocks.push(renderSkillsRemovalArm(removed, frameMaxBytes - ambientCost - addCost), AMBIENT_CONTEXT_NOTE);
|
|
615
651
|
}
|
|
616
|
-
return
|
|
652
|
+
return {
|
|
653
|
+
text: blocks.join(SKILLS_DELTA_BLOCK_JOINER),
|
|
654
|
+
nameOnly: addArm?.nameOnly ?? [],
|
|
655
|
+
dropped: addArm?.dropped ?? [],
|
|
656
|
+
listed: added.length,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
export function renderSkillsListingDelta(state, entries, budget) {
|
|
660
|
+
return renderSkillsListingFrame(state, entries, budget)?.text;
|
|
617
661
|
}
|
|
618
662
|
export function commitSkillsListing(state, entries) {
|
|
619
663
|
state.announcedSkills = new Map(entries.map((e) => [e.name, e.description]));
|