@deepstrike/wasm 0.2.77 → 0.2.78

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.
Files changed (62) hide show
  1. package/dist/agent.d.ts +6 -2
  2. package/dist/agent.js +59 -19
  3. package/dist/index.d.ts +7 -9
  4. package/dist/index.js +3 -5
  5. package/dist/memory/attestation.d.ts +38 -0
  6. package/dist/memory/attestation.js +43 -0
  7. package/dist/memory/authority.d.ts +2 -0
  8. package/dist/memory/authority.js +3 -1
  9. package/dist/memory/candidate-sink.d.ts +66 -0
  10. package/dist/memory/candidate-sink.js +82 -0
  11. package/dist/memory/durable.d.ts +4 -2
  12. package/dist/memory/durable.js +2 -0
  13. package/dist/memory/index.d.ts +9 -1
  14. package/dist/memory/index.js +5 -0
  15. package/dist/memory/lifecycle.d.ts +82 -0
  16. package/dist/memory/lifecycle.js +46 -0
  17. package/dist/memory/recall-projection.d.ts +5 -0
  18. package/dist/memory/recall-projection.js +18 -0
  19. package/dist/memory/runtime-policy.d.ts +15 -0
  20. package/dist/memory/runtime-policy.js +26 -0
  21. package/dist/memory/writer.d.ts +22 -0
  22. package/dist/memory/writer.js +41 -0
  23. package/dist/providers/anthropic-compaction.d.ts +84 -0
  24. package/dist/providers/anthropic-compaction.js +222 -0
  25. package/dist/providers/anthropic.d.ts +41 -1
  26. package/dist/providers/anthropic.js +187 -18
  27. package/dist/providers/base.js +11 -1
  28. package/dist/runtime/canonical-kernel-step.d.ts +1 -2
  29. package/dist/runtime/canonical-kernel-step.js +74 -8
  30. package/dist/runtime/execution-plane.js +1 -1
  31. package/dist/runtime/index.d.ts +0 -2
  32. package/dist/runtime/index.js +0 -1
  33. package/dist/runtime/kernel-event-log.d.ts +8 -0
  34. package/dist/runtime/kernel-event-log.js +61 -1
  35. package/dist/runtime/kernel-step.d.ts +13 -2
  36. package/dist/runtime/os-snapshot.js +1 -0
  37. package/dist/runtime/runner.d.ts +40 -25
  38. package/dist/runtime/runner.js +196 -240
  39. package/dist/runtime/session-event-registry.generated.d.ts +212 -0
  40. package/dist/runtime/session-event-registry.generated.js +112 -0
  41. package/dist/runtime/session-log.d.ts +69 -16
  42. package/dist/runtime/session-log.js +10 -58
  43. package/dist/runtime/session-repair.d.ts +0 -3
  44. package/dist/runtime/session-repair.js +0 -3
  45. package/dist/runtime/sub-agent-orchestrator.d.ts +2 -0
  46. package/dist/runtime/sub-agent-orchestrator.js +2 -0
  47. package/dist/runtime/types/agent.d.ts +9 -52
  48. package/dist/runtime/types/agent.js +8 -45
  49. package/dist/types.d.ts +10 -5
  50. package/package.json +3 -3
  51. package/dist/memory/extraction.d.ts +0 -4
  52. package/dist/memory/extraction.js +0 -48
  53. package/dist/runtime/context.d.ts +0 -28
  54. package/dist/runtime/context.js +0 -16
  55. package/dist/runtime/evolution.d.ts +0 -189
  56. package/dist/runtime/evolution.js +0 -25
  57. package/dist/runtime/reducers.d.ts +0 -15
  58. package/dist/runtime/reducers.js +0 -57
  59. package/dist/runtime/verifiable-report.d.ts +0 -59
  60. package/dist/runtime/verifiable-report.js +0 -64
  61. package/dist/runtime/workflow-control-flow.d.ts +0 -21
  62. package/dist/runtime/workflow-control-flow.js +0 -83
@@ -0,0 +1,46 @@
1
+ export function memoryRecordExpired(record, nowMs) {
2
+ return record.ttl_days !== undefined && nowMs > record.updated_at + record.ttl_days * 86_400_000;
3
+ }
4
+ export function verifyMemoryLease(lease, agentId, scope, nowMs, budget) {
5
+ if (!lease)
6
+ return { valid: false, reason_code: "lease_missing" };
7
+ if (lease.agent_id !== agentId || lease.scope.tenant_id !== scope.tenant_id || lease.scope.namespace !== scope.namespace)
8
+ return { valid: false, reason_code: "lease_scope_mismatch" };
9
+ if (lease.revoked_at !== undefined && nowMs >= lease.revoked_at)
10
+ return { valid: false, reason_code: "lease_revoked" };
11
+ if (nowMs >= lease.expires_at)
12
+ return { valid: false, reason_code: "lease_expired" };
13
+ if ((budget?.writes_used ?? lease.writes_used) >= (budget?.max_writes ?? lease.max_writes))
14
+ return { valid: false, reason_code: "memory_write_budget_exhausted" };
15
+ return { valid: true };
16
+ }
17
+ export function consumeMemoryLeaseWrite(lease) { return { ...lease, writes_used: lease.writes_used + 1 }; }
18
+ export function revokeMemoryLease(lease, revokedAt) { return { ...lease, revoked_at: revokedAt }; }
19
+ export function scopeMatchesMemoryDeletion(record, scope) {
20
+ return (scope.tenant_id === undefined || record.scope.tenant_id === scope.tenant_id) && (scope.namespace === undefined || record.scope.namespace === scope.namespace);
21
+ }
22
+ export class InMemoryMemoryLifecycleController {
23
+ leases = new Map();
24
+ consumed = new Map();
25
+ deletedRecords = new Map();
26
+ journal = [];
27
+ issueLease(lease) { const old = this.leases.get(lease.lease_id); if (old && JSON.stringify(old) !== JSON.stringify(lease))
28
+ throw new Error("memory lease conflict"); if (old)
29
+ return structuredClone(old); this.leases.set(lease.lease_id, structuredClone(lease)); this.append("lease_issued", lease, undefined, lease.issued_at, { max_writes: lease.max_writes, expires_at: lease.expires_at }); return structuredClone(lease); }
30
+ getLease(agentId, scope) { const lease = [...this.leases.values()].find(item => item.agent_id === agentId && item.scope.tenant_id === scope.tenant_id && item.scope.namespace === scope.namespace); return lease ? structuredClone(lease) : undefined; }
31
+ renewLease(agentId, leaseId, expiresAt, renewedAt) { const lease = this.require(agentId, leaseId); if (expiresAt <= renewedAt || expiresAt < lease.expires_at)
32
+ throw new Error("invalid memory lease renewal"); const updated = { ...lease, expires_at: expiresAt }; this.leases.set(leaseId, updated); this.append("lease_renewed", updated, undefined, renewedAt, { expires_at: expiresAt }); return structuredClone(updated); }
33
+ consumeWrite(agentId, scope, operationId, nowMs) { const key = `${agentId}:${operationId}`; const prior = this.consumed.get(key); if (prior)
34
+ return structuredClone(prior); const lease = this.getLease(agentId, scope); const verdict = verifyMemoryLease(lease, agentId, scope, nowMs); if (!verdict.valid) {
35
+ this.consumed.set(key, verdict);
36
+ return structuredClone(verdict);
37
+ } const updated = consumeMemoryLeaseWrite(lease); this.leases.set(updated.lease_id, updated); const event = this.append("lease_write_consumed", updated, operationId, nowMs, { writes_used: updated.writes_used }); const result = { valid: true, lease: updated, event_id: event.event_id }; this.consumed.set(key, result); return structuredClone(result); }
38
+ revokeLease(agentId, leaseId, revokedAt) { const lease = this.require(agentId, leaseId); if (lease.revoked_at !== undefined)
39
+ return structuredClone(lease); const updated = revokeMemoryLease(lease, revokedAt); this.leases.set(leaseId, updated); this.append("lease_revoked", updated, undefined, revokedAt, { revoked_at: revokedAt }); return structuredClone(updated); }
40
+ deleteRecord(agentId, scope, recordId, deletionId, priorDigest) { const key = `${agentId}:${deletionId}`; const old = this.deletedRecords.get(key); if (old)
41
+ return structuredClone(old); const payload = { deletion_id: deletionId, record_id: recordId, ...(priorDigest ? { prior_digest: priorDigest } : {}) }; const lease = { lease_id: `deletion:${deletionId}`, agent_id: agentId, scope: structuredClone(scope), issued_at: 0, expires_at: 0, max_writes: 0, writes_used: 0 }; this.append("deletion_requested", lease, deletionId, Date.now(), payload); const event = this.append("deletion_applied", lease, deletionId, Date.now(), payload); const receipt = { deletion_id: deletionId, agent_id: agentId, scope: structuredClone(scope), record_id: recordId, ...(priorDigest ? { prior_digest: priorDigest } : {}), status: "applied", event_sequence: event.sequence }; this.deletedRecords.set(key, receipt); return structuredClone(receipt); }
42
+ events() { return structuredClone(this.journal); }
43
+ require(agentId, leaseId) { const lease = this.leases.get(leaseId); if (!lease || lease.agent_id !== agentId)
44
+ throw new Error("memory lease not found"); return lease; }
45
+ append(type, lease, operationId, occurredAt, payload) { const event = { event_id: `${type}:${lease.lease_id}:${operationId ?? this.journal.length}`, sequence: this.journal.length, type, agent_id: lease.agent_id, lease_id: lease.lease_id, scope: structuredClone(lease.scope), ...(operationId ? { operation_id: operationId } : {}), occurred_at: occurredAt, payload }; this.journal.push(event); return event; }
46
+ }
@@ -0,0 +1,5 @@
1
+ import type { MemoryQuery, MemoryRecall, MemoryRecord } from "./index.js";
2
+ /** The answer to `memory(record_id)`: the record itself, or nothing when it is gone, expired,
3
+ * outside the query's scope or not one of the kinds asked for. A fetch is a point read, so a
4
+ * found record carries no ranking — the kernel refuses any record but the one it named. */
5
+ export declare function memoryFetchRecall(record: MemoryRecord | null, query: Pick<MemoryQuery, "scope" | "kinds">, nowMs: number): MemoryRecall[];
@@ -0,0 +1,18 @@
1
+ // M5 · the host half of reading one record in full. Recall itself is the kernel's: it queries for
2
+ // the goal at run start and after each renewal and lands an index line per hit as keyed knowledge
3
+ // `memory:<record_id>`. The model reads a record with `memory(record_id)`, answered here.
4
+ import { memoryRecordExpired } from "./lifecycle.js";
5
+ /** The answer to `memory(record_id)`: the record itself, or nothing when it is gone, expired,
6
+ * outside the query's scope or not one of the kinds asked for. A fetch is a point read, so a
7
+ * found record carries no ranking — the kernel refuses any record but the one it named. */
8
+ export function memoryFetchRecall(record, query, nowMs) {
9
+ if (!record)
10
+ return [];
11
+ if (record.scope.tenant_id !== query.scope.tenant_id || record.scope.namespace !== query.scope.namespace)
12
+ return [];
13
+ if (query.kinds.length > 0 && !query.kinds.includes(record.kind))
14
+ return [];
15
+ if (memoryRecordExpired(record, nowMs))
16
+ return [];
17
+ return [{ record, score: 1, why: "record_id" }];
18
+ }
@@ -0,0 +1,15 @@
1
+ export type MemoryRetrievalPolicy = "disabled" | "host";
2
+ export type MemoryWritePolicy = "disabled" | "candidate_sink";
3
+ export interface MemoryRuntimePolicy {
4
+ retrieval: MemoryRetrievalPolicy;
5
+ modelWrites: MemoryWritePolicy;
6
+ }
7
+ export interface ResolvedMemoryRuntimePolicy {
8
+ retrievalEnabled: boolean;
9
+ modelWritesEnabled: boolean;
10
+ /** The path is on and its writes go to the candidate sink, not the memory store. */
11
+ modelWritesToCandidateSink: boolean;
12
+ }
13
+ /** Validate a host-supplied policy and project it onto runner gates. `undefined` keeps the
14
+ * existing behavior: every memory path stays enabled. */
15
+ export declare function resolveMemoryRuntimePolicy(policy: MemoryRuntimePolicy | undefined): ResolvedMemoryRuntimePolicy | undefined;
@@ -0,0 +1,26 @@
1
+ // Host-facing memory runtime policy (contract M1). Each capability is an independent switch: a
2
+ // host can keep retrieval while turning off model-authored writes. `"disabled"` turns a path off;
3
+ // `"candidate_sink"` keeps writes on but hands them to the host's `MemoryCandidateSink` (M2).
4
+ const POLICY_FIELDS = ["retrieval", "modelWrites"];
5
+ /** Validate a host-supplied policy and project it onto runner gates. `undefined` keeps the
6
+ * existing behavior: every memory path stays enabled. */
7
+ export function resolveMemoryRuntimePolicy(policy) {
8
+ if (policy === undefined)
9
+ return undefined;
10
+ const unknown = Object.keys(policy).filter(key => !POLICY_FIELDS.includes(key));
11
+ if (unknown.length > 0) {
12
+ throw new TypeError(`unknown memory runtime policy field(s): ${unknown.join(", ")}`);
13
+ }
14
+ for (const field of POLICY_FIELDS) {
15
+ const value = policy[field];
16
+ const allowed = field === "retrieval" ? ["disabled", "host"] : ["disabled", "candidate_sink"];
17
+ if (value === undefined || !allowed.includes(value)) {
18
+ throw new TypeError(`memory runtime policy ${field} must be one of ${allowed.join(" | ")}, got ${String(value)}`);
19
+ }
20
+ }
21
+ return {
22
+ retrievalEnabled: policy.retrieval !== "disabled",
23
+ modelWritesEnabled: policy.modelWrites !== "disabled",
24
+ modelWritesToCandidateSink: policy.modelWrites === "candidate_sink",
25
+ };
26
+ }
@@ -0,0 +1,22 @@
1
+ import type { MemoryCandidate, MemoryCandidateReceipt, MemoryCandidateSink } from "./candidate-sink.js";
2
+ import type { MemoryRecord, MemoryStore } from "./index.js";
3
+ export interface MemoryWriterOptions {
4
+ store?: MemoryStore;
5
+ sink?: MemoryCandidateSink;
6
+ }
7
+ /** What a direct write settles with: the record the store now holds and its content digest. */
8
+ export interface MemoryPersistReceipt {
9
+ record_ref: string;
10
+ digest: string;
11
+ }
12
+ export declare class MemoryWriter {
13
+ private readonly opts;
14
+ constructor(opts: MemoryWriterOptions);
15
+ /** Write `record` to the store. Throws when there is no store. */
16
+ persist(agentId: string, record: MemoryRecord): Promise<MemoryPersistReceipt>;
17
+ /**
18
+ * Hand `candidate` to the sink and check its answer names this candidate (spec §2.3). A sink that
19
+ * throws has given no answer, and the same candidate may be redelivered.
20
+ */
21
+ propose(agentId: string, candidate: MemoryCandidate): Promise<MemoryCandidateReceipt>;
22
+ }
@@ -0,0 +1,41 @@
1
+ // The host's side of a memory write, in one place. The runner decides which path a write takes
2
+ // (the store, or the candidate sink for review); the writer decides what the write's answer
3
+ // names: the stored record and its digest, or the sink's receipt under its contract.
4
+ import { sha256Hex } from "../runtime/sha256.js";
5
+ export class MemoryWriter {
6
+ opts;
7
+ constructor(opts) {
8
+ this.opts = opts;
9
+ }
10
+ /** Write `record` to the store. Throws when there is no store. */
11
+ async persist(agentId, record) {
12
+ if (!this.opts.store)
13
+ throw new Error("memory persistence requires RuntimeOptions.memoryStore");
14
+ await this.opts.store.put(agentId, record);
15
+ return { record_ref: record.record_id, digest: sha256Hex(record.content) };
16
+ }
17
+ /**
18
+ * Hand `candidate` to the sink and check its answer names this candidate (spec §2.3). A sink that
19
+ * throws has given no answer, and the same candidate may be redelivered.
20
+ */
21
+ async propose(agentId, candidate) {
22
+ const sink = this.opts.sink;
23
+ if (!sink)
24
+ throw new Error("memory candidate writes require RuntimeOptions.memoryCandidateSink");
25
+ const receipt = await sink.submit(agentId, candidate);
26
+ const candidateId = candidate.candidate_id;
27
+ if (receipt.candidate_id !== candidateId || typeof receipt.receipt_id !== "string" || !receipt.receipt_id) {
28
+ throw new Error(`memory candidate sink answered for ${String(receipt.candidate_id)} without a receipt for ${candidateId}`);
29
+ }
30
+ if (receipt.status === "accepted" && (typeof receipt.record_ref !== "string" || !receipt.record_ref || typeof receipt.digest !== "string" || !receipt.digest)) {
31
+ throw new Error("memory candidate sink accepted a write without record_ref/digest");
32
+ }
33
+ if (!["pending", "accepted", "rejected"].includes(receipt.status)) {
34
+ throw new Error(`memory candidate sink answered with unknown status ${String(receipt.status)}`);
35
+ }
36
+ if (receipt.status === "rejected" && (typeof receipt.reason_code !== "string" || !receipt.reason_code)) {
37
+ throw new Error("memory candidate sink rejected a write without reason_code");
38
+ }
39
+ return receipt;
40
+ }
41
+ }
@@ -0,0 +1,84 @@
1
+ export declare const THRESHOLD_COMPACTION_BETA = "compact-2026-01-12";
2
+ export declare const ON_DEMAND_COMPACTION_BETA = "compact-2026-09-04";
3
+ /** `compact_20260112` settings. Unset fields take the API defaults. */
4
+ export interface AnthropicThresholdCompaction {
5
+ mode?: "threshold";
6
+ /** Input tokens at which the API summarizes earlier history (at least 50,000; API default 150,000). */
7
+ triggerInputTokens?: number;
8
+ /** A summarization prompt that replaces the API's default one. */
9
+ instructions?: string;
10
+ /** Stop after the summary is written, then continue with the newest history messages kept
11
+ * verbatim behind it. Their thinking blocks are dropped: they were produced with the full
12
+ * history in view and would fail the API's prefix check. */
13
+ pauseAfterCompaction?: {
14
+ keepRecentMessages: number;
15
+ };
16
+ }
17
+ /** On-demand compaction settings (`compact-2026-09-04`). */
18
+ export interface AnthropicOnDemandCompaction {
19
+ mode: "on_demand";
20
+ /** Once a turn's input plus output reaches this many tokens, the provider asks for a summary of
21
+ * that turn's history in the background and adopts it on the first request after it arrives. */
22
+ compactAtInputTokens: number;
23
+ /** A summarization prompt that replaces the API's default one (at most 16,384 characters). */
24
+ instructions?: string;
25
+ /** `max_tokens` for the summarization call; defaults to the turn's own. */
26
+ maxTokens?: number;
27
+ }
28
+ export type AnthropicCompaction = AnthropicThresholdCompaction | AnthropicOnDemandCompaction;
29
+ export type ResolvedCompaction = {
30
+ mode: "threshold";
31
+ edit: Record<string, unknown>;
32
+ keepRecentMessages?: number;
33
+ } | {
34
+ mode: "on_demand";
35
+ config: AnthropicOnDemandCompaction;
36
+ };
37
+ type WireMessage = Record<string, unknown>;
38
+ type WireBlock = Record<string, unknown>;
39
+ export declare function resolveCompaction(config: true | AnthropicCompaction): ResolvedCompaction;
40
+ /** The request that continues a paused threshold compaction: the same edits without the pause. */
41
+ export declare function withoutCompactionPause(params: Record<string, unknown>): Record<string, unknown>;
42
+ export declare function compactionBlock(content: unknown): WireBlock | undefined;
43
+ export interface CompactionResume {
44
+ /** The messages of the continuation request. */
45
+ messages: WireMessage[];
46
+ /** When recent turns were kept: the wire index of the assistant message the block now heads,
47
+ * and that message's new content (the block, then its own blocks without thinking). */
48
+ anchor?: {
49
+ wireIndex: number;
50
+ content: WireBlock[];
51
+ };
52
+ }
53
+ /** How a paused threshold compaction continues. With `keepRecentMessages` the block heads the
54
+ * nearest assistant message at or before the first kept history message, so the kept turns follow
55
+ * the summary verbatim; without it (or when no assistant message qualifies) the block is
56
+ * appended as an assistant turn of its own, as the API's pause flow does. `historyCount` is the
57
+ * number of leading wire messages that are history (the volatile state turn follows them). */
58
+ export declare function resumeAfterCompactionPause(messages: WireMessage[], historyCount: number, block: WireBlock, keepRecentMessages: number | undefined): CompactionResume;
59
+ /** The compaction request a turn's request becomes: the same model, system, tools and thinking,
60
+ * its history messages only, and none of the fields the API rejects on a summarization call. */
61
+ export declare function onDemandCompactionBody(sent: Record<string, unknown>, historyCount: number, config: AnthropicOnDemandCompaction): Record<string, unknown>;
62
+ /** Owns on-demand compaction for one provider: decides when to ask for a summary, runs the
63
+ * request in the background, and swaps the block in for the messages it summarized. Adoptions
64
+ * chain — a later summary covers the earlier block — and every request re-derives the swap from
65
+ * the full history the kernel rendered, so a history the summary no longer matches (a renewal, a
66
+ * changed system prompt or toolset) is sent whole and the stale adoptions are dropped. State is
67
+ * in memory: after a restart the first turn past the limit compacts again. */
68
+ export declare class OnDemandCompactor {
69
+ private readonly config;
70
+ private readonly send;
71
+ private adoptions;
72
+ private pending?;
73
+ constructor(config: AnthropicOnDemandCompaction, send: (body: Record<string, unknown>) => Promise<Record<string, unknown>>);
74
+ /** Swap the adopted summaries into a request's messages. Mutates `params`; returns whether a
75
+ * block was swapped in (the request then needs the on-demand beta). */
76
+ apply(params: Record<string, unknown>): boolean;
77
+ /** After a turn completes: once its input plus output reaches the limit, summarize the history
78
+ * that turn sent, unless a summary is already being written. Never throws; a summary that does
79
+ * not come back leaves the history as it is, and the next turn past the limit asks again. */
80
+ observe(sent: Record<string, unknown>, historyCount: number, totalTokens: number): void;
81
+ /** Resolves once any summary in flight has settled. For tests and orderly shutdown. */
82
+ idle(): Promise<void>;
83
+ }
84
+ export {};
@@ -0,0 +1,222 @@
1
+ // Server-side compaction for the Anthropic provider, in its two forms. Threshold compaction
2
+ // (`compact_20260112`) rides on every request and the API summarizes once input reaches the
3
+ // trigger; with `pauseAfterCompaction` the API stops after writing the summary so the provider can
4
+ // keep the newest turns verbatim behind it. On-demand compaction (`compact-2026-09-04`) is a
5
+ // separate request the provider sends in the background once a turn's input passes a limit; the
6
+ // signed block it returns replaces the messages it summarized on every later request. Either way
7
+ // the kernel sees an ordinary turn: the provider owns the block, and the kernel's renewal still
8
+ // decides on the full local history. Mirrors node/src/providers/anthropic-compaction.ts.
9
+ export const THRESHOLD_COMPACTION_BETA = "compact-2026-01-12";
10
+ export const ON_DEMAND_COMPACTION_BETA = "compact-2026-09-04";
11
+ function positiveInteger(value, field) {
12
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
13
+ throw new TypeError(`AnthropicProvider compaction.${field} must be a positive integer`);
14
+ }
15
+ return value;
16
+ }
17
+ export function resolveCompaction(config) {
18
+ const c = config === true ? {} : config;
19
+ if (c.mode === "on_demand") {
20
+ positiveInteger(c.compactAtInputTokens, "compactAtInputTokens");
21
+ if (c.maxTokens !== undefined)
22
+ positiveInteger(c.maxTokens, "maxTokens");
23
+ for (const field of ["triggerInputTokens", "pauseAfterCompaction"]) {
24
+ if (c[field] !== undefined) {
25
+ throw new TypeError(`AnthropicProvider compaction.${field} applies to threshold compaction, not on_demand`);
26
+ }
27
+ }
28
+ return { mode: "on_demand", config: c };
29
+ }
30
+ if (c.mode !== undefined && c.mode !== "threshold") {
31
+ throw new TypeError(`AnthropicProvider compaction.mode must be "threshold" or "on_demand", got ${String(c.mode)}`);
32
+ }
33
+ const keep = c.pauseAfterCompaction?.keepRecentMessages;
34
+ if (c.pauseAfterCompaction !== undefined && (typeof keep !== "number" || !Number.isSafeInteger(keep) || keep < 0)) {
35
+ throw new TypeError("AnthropicProvider compaction.pauseAfterCompaction.keepRecentMessages must be a non-negative integer");
36
+ }
37
+ return {
38
+ mode: "threshold",
39
+ edit: {
40
+ type: "compact_20260112",
41
+ ...(c.triggerInputTokens !== undefined ? { trigger: { type: "input_tokens", value: c.triggerInputTokens } } : {}),
42
+ ...(c.pauseAfterCompaction !== undefined ? { pause_after_compaction: true } : {}),
43
+ ...(c.instructions !== undefined ? { instructions: c.instructions } : {}),
44
+ },
45
+ ...(keep !== undefined ? { keepRecentMessages: keep } : {}),
46
+ };
47
+ }
48
+ /** The request that continues a paused threshold compaction: the same edits without the pause. */
49
+ export function withoutCompactionPause(params) {
50
+ const management = params.context_management;
51
+ if (!Array.isArray(management?.edits))
52
+ return params;
53
+ return {
54
+ ...params,
55
+ context_management: {
56
+ ...management,
57
+ edits: management.edits.map(edit => {
58
+ if (edit?.type !== "compact_20260112")
59
+ return edit;
60
+ const { pause_after_compaction: _pause, ...rest } = edit;
61
+ return rest;
62
+ }),
63
+ },
64
+ };
65
+ }
66
+ export function compactionBlock(content) {
67
+ return Array.isArray(content)
68
+ ? content.find(block => block?.type === "compaction")
69
+ : undefined;
70
+ }
71
+ function contentBlocks(message) {
72
+ const content = message.content;
73
+ if (typeof content === "string")
74
+ return content ? [{ type: "text", text: content }] : [];
75
+ return Array.isArray(content) ? content : [];
76
+ }
77
+ function withoutCacheControl(block) {
78
+ const { cache_control: _cache, ...rest } = block;
79
+ return rest;
80
+ }
81
+ /** How a paused threshold compaction continues. With `keepRecentMessages` the block heads the
82
+ * nearest assistant message at or before the first kept history message, so the kept turns follow
83
+ * the summary verbatim; without it (or when no assistant message qualifies) the block is
84
+ * appended as an assistant turn of its own, as the API's pause flow does. `historyCount` is the
85
+ * number of leading wire messages that are history (the volatile state turn follows them). */
86
+ export function resumeAfterCompactionPause(messages, historyCount, block, keepRecentMessages) {
87
+ const keep = keepRecentMessages ?? 0;
88
+ if (keep > 0) {
89
+ const firstKept = historyCount - keep;
90
+ for (let index = Math.min(firstKept, historyCount - 1); index > 0; index--) {
91
+ if (messages[index]?.role !== "assistant")
92
+ continue;
93
+ const own = contentBlocks(messages[index])
94
+ .filter(item => item.type !== "thinking" && item.type !== "redacted_thinking")
95
+ .map(withoutCacheControl);
96
+ const content = [block, ...own];
97
+ return {
98
+ messages: [{ role: "assistant", content }, ...messages.slice(index + 1)],
99
+ anchor: { wireIndex: index, content },
100
+ };
101
+ }
102
+ }
103
+ return { messages: [...messages, { role: "assistant", content: [block] }] };
104
+ }
105
+ /** Wire values as the API compares them: `cache_control` is a request annotation, not content,
106
+ * and a string message body is the same content as one text block (a cache marker turns the one
107
+ * into the other). */
108
+ function stableJson(value) {
109
+ return JSON.stringify(value, (key, item) => {
110
+ if (key === "cache_control")
111
+ return undefined;
112
+ if (item && typeof item === "object" && !Array.isArray(item)
113
+ && typeof item.role === "string" && typeof item.content === "string") {
114
+ return { ...item, content: contentBlocks(item) };
115
+ }
116
+ return item;
117
+ });
118
+ }
119
+ /** The compaction request a turn's request becomes: the same model, system, tools and thinking,
120
+ * its history messages only, and none of the fields the API rejects on a summarization call. */
121
+ export function onDemandCompactionBody(sent, historyCount, config) {
122
+ const { stream: _stream, context_management: _management, stop_sequences: _stop, ...body } = sent;
123
+ const choice = body.tool_choice;
124
+ if (choice?.type === "any" || choice?.type === "tool")
125
+ delete body.tool_choice;
126
+ const output = body.output_config;
127
+ if (output) {
128
+ const { format: _format, task_budget: budget, ...rest } = output;
129
+ const taskBudget = budget && typeof budget === "object"
130
+ ? (({ remaining: _remaining, ...kept }) => kept)(budget)
131
+ : undefined;
132
+ const next = { ...rest, ...(taskBudget ? { task_budget: taskBudget } : {}) };
133
+ if (Object.keys(next).length)
134
+ body.output_config = next;
135
+ else
136
+ delete body.output_config;
137
+ }
138
+ const betas = (Array.isArray(sent.betas) ? sent.betas : [])
139
+ .filter(beta => beta !== THRESHOLD_COMPACTION_BETA);
140
+ if (!betas.includes(ON_DEMAND_COMPACTION_BETA))
141
+ betas.push(ON_DEMAND_COMPACTION_BETA);
142
+ return {
143
+ ...body,
144
+ messages: sent.messages.slice(0, historyCount),
145
+ ...(config.maxTokens !== undefined ? { max_tokens: config.maxTokens } : {}),
146
+ compaction: {
147
+ type: "summarize",
148
+ ...(config.instructions?.trim() ? { instructions: config.instructions } : {}),
149
+ },
150
+ betas,
151
+ };
152
+ }
153
+ /** Owns on-demand compaction for one provider: decides when to ask for a summary, runs the
154
+ * request in the background, and swaps the block in for the messages it summarized. Adoptions
155
+ * chain — a later summary covers the earlier block — and every request re-derives the swap from
156
+ * the full history the kernel rendered, so a history the summary no longer matches (a renewal, a
157
+ * changed system prompt or toolset) is sent whole and the stale adoptions are dropped. State is
158
+ * in memory: after a restart the first turn past the limit compacts again. */
159
+ export class OnDemandCompactor {
160
+ config;
161
+ send;
162
+ adoptions = [];
163
+ pending;
164
+ constructor(config, send) {
165
+ this.config = config;
166
+ this.send = send;
167
+ }
168
+ /** Swap the adopted summaries into a request's messages. Mutates `params`; returns whether a
169
+ * block was swapped in (the request then needs the on-demand beta). */
170
+ apply(params) {
171
+ if (this.pending?.settled !== undefined) {
172
+ if (this.pending.settled)
173
+ this.adoptions.push(this.pending.settled);
174
+ this.pending = undefined;
175
+ }
176
+ if (!this.adoptions.length)
177
+ return false;
178
+ const frame = stableJson([params.system ?? null, params.tools ?? null]);
179
+ let messages = params.messages;
180
+ let applied = 0;
181
+ for (const adoption of this.adoptions) {
182
+ if (adoption.frame !== frame || adoption.summarized.length > messages.length)
183
+ break;
184
+ const matches = adoption.summarized.every((digest, index) => stableJson(messages[index]) === digest);
185
+ if (!matches)
186
+ break;
187
+ messages = [adoption.message, ...messages.slice(adoption.summarized.length)];
188
+ applied++;
189
+ }
190
+ this.adoptions.length = applied;
191
+ if (!applied)
192
+ return false;
193
+ params.messages = messages;
194
+ const betas = Array.isArray(params.betas) ? [...params.betas] : [];
195
+ if (!betas.includes(ON_DEMAND_COMPACTION_BETA))
196
+ betas.push(ON_DEMAND_COMPACTION_BETA);
197
+ params.betas = betas;
198
+ return true;
199
+ }
200
+ /** After a turn completes: once its input plus output reaches the limit, summarize the history
201
+ * that turn sent, unless a summary is already being written. Never throws; a summary that does
202
+ * not come back leaves the history as it is, and the next turn past the limit asks again. */
203
+ observe(sent, historyCount, totalTokens) {
204
+ if (this.pending || totalTokens < this.config.compactAtInputTokens || historyCount <= 0)
205
+ return;
206
+ const body = onDemandCompactionBody(sent, historyCount, this.config);
207
+ const summarized = body.messages.map(stableJson);
208
+ const frame = stableJson([body.system ?? null, body.tools ?? null]);
209
+ const pending = { done: Promise.resolve() };
210
+ pending.done = this.send(body).then(raw => {
211
+ const block = raw.stop_reason === "compaction" ? compactionBlock(raw.content) : undefined;
212
+ pending.settled = block ? { summarized, frame, message: { role: "assistant", content: [block] } } : null;
213
+ }, () => {
214
+ pending.settled = null;
215
+ });
216
+ this.pending = pending;
217
+ }
218
+ /** Resolves once any summary in flight has settled. For tests and orderly shutdown. */
219
+ async idle() {
220
+ await this.pending?.done;
221
+ }
222
+ }
@@ -1,11 +1,44 @@
1
1
  import type { PreparedProviderRequest, ProviderRunState } from "../types.js";
2
2
  import type { RenderedContext, ToolSchema, StreamEvent, LLMProvider, ModelMessage, ProviderDescriptor, ProviderReplay } from "../types.js";
3
+ import { type AnthropicCompaction } from "./anthropic-compaction.js";
4
+ export type { AnthropicCompaction, AnthropicOnDemandCompaction, AnthropicThresholdCompaction, } from "./anthropic-compaction.js";
5
+ /** `clear_tool_uses_20250919` settings. Unset fields take the API defaults. */
6
+ export interface AnthropicContextEditing {
7
+ /** Input tokens above which clearing starts. */
8
+ triggerInputTokens?: number;
9
+ /** Most recent tool uses kept intact. */
10
+ keepToolUses?: number;
11
+ /** Skip a clearing that would remove fewer tokens than this — each clearing rewrites the cached prefix. */
12
+ clearAtLeastInputTokens?: number;
13
+ /** Tools whose uses and results are never cleared. */
14
+ excludeTools?: string[];
15
+ /** Also clear the tool-call inputs, not only the results. */
16
+ clearToolInputs?: boolean;
17
+ }
18
+ export interface AnthropicProviderOptions {
19
+ /** Clear old tool results server-side (context editing). The provider then declares the tier
20
+ * native and the kernel stops clearing tool results itself; `true` takes the API defaults. */
21
+ contextEditing?: true | AnthropicContextEditing;
22
+ /** Summarize earlier history server-side: at a threshold on every request (`compact_20260112`,
23
+ * the default mode), or on demand once a turn passes a limit (`compact-2026-09-04`, `mode:
24
+ * "on_demand"`). The provider then declares compaction native and the kernel stops compacting
25
+ * itself; its renewal stays the safety net, so set the trigger below the kernel's renewal
26
+ * threshold. */
27
+ compaction?: true | AnthropicCompaction;
28
+ }
3
29
  export declare class AnthropicProvider implements LLMProvider {
4
30
  private readonly apiKey;
5
31
  private readonly model;
6
32
  private readonly maxTokens;
7
33
  private nativeAssistantBlocks;
8
- constructor(apiKey: string, model?: string, maxTokens?: number);
34
+ /** Server-side context edits and the beta each needs. */
35
+ private readonly nativeContextEdits;
36
+ /** Threshold compaction with a pause: how many history messages stay verbatim behind the block. */
37
+ private readonly compactionPauseKeep?;
38
+ private readonly onDemandCompactor?;
39
+ constructor(apiKey: string, model?: string, maxTokens?: number, options?: AnthropicProviderOptions);
40
+ /** On-demand compaction in flight settles; for tests and orderly shutdown. */
41
+ compactionIdle(): Promise<void>;
9
42
  descriptor(): ProviderDescriptor;
10
43
  requestPlanIdentity(): {
11
44
  providerId: string;
@@ -20,6 +53,13 @@ export declare class AnthropicProvider implements LLMProvider {
20
53
  seedProviderReplay(message: Pick<ModelMessage, "content" | "toolCalls">, replay: ProviderReplay): void;
21
54
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<ModelMessage>;
22
55
  prepareRequest(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState): PreparedProviderRequest;
56
+ /** One turn: the response's events, or — when a compaction pause stops the response at the
57
+ * summary — the continuation's. With a pause configured, events are held until the response
58
+ * shows it is not a pause (any non-usage event) or ends. */
59
+ private streamTurn;
60
+ private headers;
61
+ /** A non-streaming request; the on-demand compaction request goes this way. */
62
+ private createMessage;
23
63
  stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState, signal?: AbortSignal): AsyncIterable<StreamEvent>;
24
64
  private streamBody;
25
65
  private rememberNativeBlocks;