@arnilo/prism 0.2.1 → 0.2.2
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 +5 -0
- package/dist/contracts-core.d.ts +32 -2
- package/dist/contracts-core.js +20 -0
- package/dist/conversations.d.ts +2 -0
- package/dist/conversations.js +1 -0
- package/dist/event-multiplexer.d.ts +13 -0
- package/dist/event-multiplexer.js +39 -16
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/testing/persistence-schema.d.ts +1 -1
- package/dist/testing/persistence-schema.js +32 -28
- package/dist/testing/state-concurrency-conformance.d.ts +145 -0
- package/dist/testing/state-concurrency-conformance.js +348 -0
- package/docs/agent-events.md +1 -1
- package/docs/conversations.md +5 -2
- package/docs/database-persistence.md +4 -0
- package/docs/enterprise-postgres-state.md +4 -3
- package/docs/index.md +8 -8
- package/docs/migration.md +52 -0
- package/docs/model-routing.md +13 -10
- package/docs/public-contracts.md +3 -3
- package/docs/release-and-install.md +20 -0
- package/docs/workflows.md +2 -1
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2] - 2026-08-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Release 0.2.2 (plan 022)** is the concurrent-state-and-durability-integrity cut on the 0.2.x review-remediation line. API surface **additive-only** vs 0.2.1 (plain reviewed compat gate at 0.2.2: deltas are the version literal plus `ModelRouterStateStore.reserveBudget`/`commitBudget`/`releaseBudget`, `ModelRouterReservation`, `ModelRouterBudgets.reservationTtlMs`, `ModelRouterLimits.maxRateKeys`/`maxBudgetKeys`, `SessionRecord.version` with `appendSession` `expectedVersion`, `EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER`, and the `@arnilo/prism/testing/state-concurrency-conformance` subpath; no removal; no `--allow-break`), four behavior tightenings documented in `docs/migration.md` `0.2.1 → 0.2.2`: (1) **atomic model-budget reservation** (`budget-reservation`, `@arnilo/prism-model-router` + `@arnilo/prism-enterprise-postgres`) — admission now runs `reserveBudget` (used + reserved + requested <= window max, `{reservationId, fencingToken, admitted, retryAfterMs?}`) and outcomes run `commitBudget` (actuals; a TTL-expired late commit charges the reserved amount with `unknownUsage: true`) or `releaseBudget`; reservations expire after `reservationTtlMs` (default 60 s, bounded to 31 days) so a crashed request never holds capacity forever; 0.2.1 `readBudget`/`addUsage` remain for cap-less requests and retrospective accounting, no longer admission authority; rate/budget key maps are capped (`maxRateKeys`/`maxBudgetKeys` 4,096 default / 65,536 hard; circuits unchanged) with LRU eviction that never drops a held-reservation row (capacity-exhausted fail-closed otherwise); durable store keeps reservations in a `reservations` JSONB column (enterprise migration 003, forward-only, checksummed). (2) **atomic conversation metadata** (`conversation-metadata-cas`, `@arnilo/prism-session-store-postgres` + `@arnilo/prism-session-store-sqlite`) — `SessionRecord.version` (fresh rows start at 1; migration 008 backfills legacy rows) and `appendSession` `expectedVersion` (`0` create-only, `N>0` exact-CAS update-only, omitted = 0.2.1 last-write-wins); stale writes throw `SessionMetadataConflictError` `metadata_conflict` carrying only `{id, expectedVersion, currentVersion}` (never metadata content), HTTP 409; concurrent create/branch/archive are single-statement with branch caps enforced inside the CAS write, archive wins over stale writers, retention-deleted rows are never resurrected. (3) **single-consumer EventMultiplexer** (`single-consumer`, core) — a second concurrent `subscribe()` throws `EventMultiplexerError` `ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER` instead of silently sharing one queue and losing events; slot frees on completion/return-at-yield/close; in-repo consumers (workflow bus, supervisor) already single-subscriber. (4) **restart-stable NATS durable identity + bounded non-durable active-run registries** (`nats-durable`, `@arnilo/prism-session-store-nats`; `active-runs`, `@arnilo/prism-workflows`) — the durable consumer name is exactly `prism_<hmac16 of tenantId|sessionId|runId>` (random suffix removed) so a crash-resumed subscribe continues from the last ack; clean stops delete the consumer (cursor resume), orphaned 0.2.1 `prism_<digest>_<random>` consumers are reclaimed by the existing cleanup; the workflow active-run registry sweeps aborted/leaked entries on register and fails closed at the 512 cap (`ERR_PRISM_WORKFLOW_RUN_REGISTRY_OVERFLOW`), documented non-durable. New regression surface: `scripts/phase22-security.test.mjs` (5 tests: 4 blockers + gate accounting, built public entrypoints, wired into `security:threat-suites`), the packed plain-JS `security22.mjs` consumer in install-smoke, the `@arnilo/prism/testing/state-concurrency-conformance` harness (7 probes — approval, cursor, checkpoint CAS, idempotency, router reservation, conversation metadata, unknown-outcome — memory leg in npm test, durable legs in `test:postgres`/NATS seam, zero timing-only sleeps), and the `scripts/phase22-conformance.test.mjs` gate in the `test:postgres` chain; phase-22 evidence in `scripts/phase22-baseline.json` + `scripts/phase22-freeze-manifest.json`. Release graph stays **50** publishable manifests at exact **0.2.2**; zero new runtime dependency names (core remains dependency-free). Store compatibility with 0.2.1: **forward-only migrations** (`prism_sessions.version` migration 008, `prism_model_router_budgets.reservations` enterprise migration 003; 0.2.1 databases migrate in place, 0.2.1 binaries read 0.2.2 databases but ignore the new columns — see `docs/migration.md` rollback-risk). Exit gate green (core + script gates incl. phase21-freeze + phase22 conformance, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain reviewed compat gate at 0.2.2, protected OIDC/OPA + durable state-concurrency evidence, evidence in `scripts/phase22-baseline.json`). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.2.2 publish handoff` — signed `v0.2.2` tag + npm OIDC).
|
|
7
|
+
|
|
3
8
|
## [0.2.1] - 2026-08-13
|
|
4
9
|
|
|
5
10
|
### Changed
|
package/dist/contracts-core.d.ts
CHANGED
|
@@ -823,6 +823,27 @@ export interface SessionBranchHandle {
|
|
|
823
823
|
}
|
|
824
824
|
/** Stable error code carried by `SessionAppendConflictError`. */
|
|
825
825
|
export declare const SESSION_APPEND_CONFLICT_CODE: "session_append_conflict";
|
|
826
|
+
/** CAS conflict code for `appendSession` metadata writes. Stable and message-independent. */
|
|
827
|
+
export declare const SESSION_METADATA_CONFLICT_CODE: "metadata_conflict";
|
|
828
|
+
/** Conflict details carried by `SessionMetadataConflictError`. Versions only; never metadata content. */
|
|
829
|
+
export interface SessionMetadataConflict {
|
|
830
|
+
readonly code: typeof SESSION_METADATA_CONFLICT_CODE;
|
|
831
|
+
readonly id: string;
|
|
832
|
+
readonly expectedVersion: number;
|
|
833
|
+
readonly currentVersion: number;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Thrown when `appendSession` is called with an `expectedVersion` CAS guard and the
|
|
837
|
+
* stored session's version no longer matches (concurrent create/branch/archive, or a
|
|
838
|
+
* delete raced the write). Recognize via the stable `code` or `isSessionMetadataConflict`.
|
|
839
|
+
*/
|
|
840
|
+
export declare class SessionMetadataConflictError extends Error {
|
|
841
|
+
readonly conflict: SessionMetadataConflict;
|
|
842
|
+
readonly code: "metadata_conflict";
|
|
843
|
+
constructor(conflict: SessionMetadataConflict);
|
|
844
|
+
}
|
|
845
|
+
/** Type guard keyed off the stable `code` (works across bundles; not message text). */
|
|
846
|
+
export declare function isSessionMetadataConflict(error: unknown): error is SessionMetadataConflictError;
|
|
826
847
|
/** Conflict details carried by `SessionAppendConflictError`. Carries no secrets. */
|
|
827
848
|
export interface SessionAppendConflict {
|
|
828
849
|
readonly code: typeof SESSION_APPEND_CONFLICT_CODE;
|
|
@@ -954,6 +975,8 @@ export interface SessionRecord extends OwnershipScope {
|
|
|
954
975
|
readonly expiresAt?: string;
|
|
955
976
|
readonly retentionPolicyId?: string;
|
|
956
977
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
978
|
+
/** Write version for optimistic metadata CAS; undefined on legacy/never-written rows. */
|
|
979
|
+
readonly version?: number;
|
|
957
980
|
}
|
|
958
981
|
/** Stored branch handle / leaf pointer. The leaf is the current entry id for the branch. */
|
|
959
982
|
export interface BranchRecord {
|
|
@@ -1182,8 +1205,15 @@ export interface ProductionPersistenceStore {
|
|
|
1182
1205
|
queryRetentionPolicies(query: RetentionPolicyQuery): Promise<PersistencePage<RetentionPolicy>>;
|
|
1183
1206
|
queryMigrations(query: MigrationQuery): Promise<PersistencePage<MigrationRecord>>;
|
|
1184
1207
|
/** Optional session-record write capability (conversation threads, host-managed sessions).
|
|
1185
|
-
* Upserts by id; ownership columns are set on create, `metadata`/`updatedAt` on update.
|
|
1186
|
-
|
|
1208
|
+
* Upserts by id; ownership columns are set on create, `metadata`/`updatedAt` on update.
|
|
1209
|
+
* Additive CAS: pass `expectedVersion` to require the stored version to match before the
|
|
1210
|
+
* write (0 = create-only, a positive number = exact current version); omit it for legacy
|
|
1211
|
+
* last-write-wins. Returns the new `version` when the underlying store supports it. */
|
|
1212
|
+
appendSession?(record: SessionRecord & {
|
|
1213
|
+
readonly expectedVersion?: number;
|
|
1214
|
+
}): Promise<{
|
|
1215
|
+
readonly version: number;
|
|
1216
|
+
} | void>;
|
|
1187
1217
|
/** DB-friendly branch read (mirrors `SessionStore.readBranchPath`): one ancestor-chain
|
|
1188
1218
|
* query instead of `queryEntries({ sessionId })` + in-memory walk. Optional. */
|
|
1189
1219
|
readBranchPath?(query: SessionBranchRead): Promise<PersistencePage<SessionEntry>>;
|
package/dist/contracts-core.js
CHANGED
|
@@ -84,6 +84,26 @@ export function isSessionSearchUnsupported(error) {
|
|
|
84
84
|
}
|
|
85
85
|
/** Stable error code carried by `SessionAppendConflictError`. */
|
|
86
86
|
export const SESSION_APPEND_CONFLICT_CODE = "session_append_conflict";
|
|
87
|
+
/** CAS conflict code for `appendSession` metadata writes. Stable and message-independent. */
|
|
88
|
+
export const SESSION_METADATA_CONFLICT_CODE = "metadata_conflict";
|
|
89
|
+
/**
|
|
90
|
+
* Thrown when `appendSession` is called with an `expectedVersion` CAS guard and the
|
|
91
|
+
* stored session's version no longer matches (concurrent create/branch/archive, or a
|
|
92
|
+
* delete raced the write). Recognize via the stable `code` or `isSessionMetadataConflict`.
|
|
93
|
+
*/
|
|
94
|
+
export class SessionMetadataConflictError extends Error {
|
|
95
|
+
conflict;
|
|
96
|
+
code = SESSION_METADATA_CONFLICT_CODE;
|
|
97
|
+
constructor(conflict) {
|
|
98
|
+
super(`session metadata conflict: expected version ${conflict.expectedVersion}, current ${conflict.currentVersion}`);
|
|
99
|
+
this.conflict = conflict;
|
|
100
|
+
this.name = "SessionMetadataConflictError";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Type guard keyed off the stable `code` (works across bundles; not message text). */
|
|
104
|
+
export function isSessionMetadataConflict(error) {
|
|
105
|
+
return error instanceof Error && error.code === SESSION_METADATA_CONFLICT_CODE;
|
|
106
|
+
}
|
|
87
107
|
/**
|
|
88
108
|
* Thrown when `SessionStore.append` rejects an entry under `SessionAppendOptions`
|
|
89
109
|
* (dangling/stale `expectedParentId`, stricter adapter CAS failure, or duplicate
|
package/dist/conversations.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ConversationThread extends OwnershipScope {
|
|
|
21
21
|
readonly updatedAt: string;
|
|
22
22
|
/** Branch leaves recorded by the conversation service; the entry tree remains the content source of truth. */
|
|
23
23
|
readonly branches: readonly ConversationBranchRef[];
|
|
24
|
+
/** CAS write version (stored `SessionRecord.version`); undefined on legacy rows. */
|
|
25
|
+
readonly version?: number;
|
|
24
26
|
/** Host-supplied create metadata; never credentials or raw transcripts. */
|
|
25
27
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
26
28
|
}
|
package/dist/conversations.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type EventOverflowPolicy = "close" | "drop_oldest" | "drop_newest";
|
|
2
|
+
export declare const EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE = "ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER";
|
|
3
|
+
/** Thrown when a second consumer subscribes while one is active (single-consumer contract). */
|
|
4
|
+
export declare class EventMultiplexerError extends Error {
|
|
5
|
+
readonly code = "ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER";
|
|
6
|
+
constructor(message?: string);
|
|
7
|
+
}
|
|
2
8
|
export interface EventOverflowInfo {
|
|
3
9
|
readonly droppedEvents: number;
|
|
4
10
|
readonly maxQueuedEvents: number;
|
|
@@ -14,6 +20,13 @@ export interface EventMultiplexerOptions<T> {
|
|
|
14
20
|
export interface EventMultiplexer<T> {
|
|
15
21
|
publish(event: T): void;
|
|
16
22
|
observe<S>(source: AsyncIterable<S>, map: (event: S) => T): () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Single-consumer subscription. A second concurrent subscriber is rejected with
|
|
25
|
+
* `EventMultiplexerError` (ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER); the slot
|
|
26
|
+
* frees when the active consumer's iterator completes or is `return()`ed at a
|
|
27
|
+
* yield, or when the multiplexer closes. A consumer parked awaiting an event is
|
|
28
|
+
* released by the next `publish`/`close` — `return()` while parked waits for it.
|
|
29
|
+
*/
|
|
17
30
|
subscribe(): AsyncIterable<T>;
|
|
18
31
|
close(): void;
|
|
19
32
|
readonly droppedEvents: number;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export const EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE = "ERR_PRISM_EVENT_MULTIPLEXER_SINGLE_CONSUMER";
|
|
2
|
+
/** Thrown when a second consumer subscribes while one is active (single-consumer contract). */
|
|
3
|
+
export class EventMultiplexerError extends Error {
|
|
4
|
+
code = EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE;
|
|
5
|
+
constructor(message = "Event multiplexer already has an active subscriber") {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "EventMultiplexerError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
1
10
|
/** Bounded single-consumer fan-in for arbitrary async event sources. */
|
|
2
11
|
export function createEventMultiplexer(options = {}) {
|
|
3
12
|
const maxQueuedEvents = Math.max(1, options.maxQueuedEvents ?? 1024);
|
|
@@ -8,6 +17,9 @@ export function createEventMultiplexer(options = {}) {
|
|
|
8
17
|
let isClosed = false;
|
|
9
18
|
let droppedEvents = 0;
|
|
10
19
|
let overflowNotified = false;
|
|
20
|
+
// Single-consumer contract: the parked-waiter design corrupts the queue when two
|
|
21
|
+
// consumers iterate concurrently, so a second active consumer is rejected instead.
|
|
22
|
+
let activeConsumer = false;
|
|
11
23
|
const abort = () => close();
|
|
12
24
|
if (options.signal?.aborted)
|
|
13
25
|
isClosed = true;
|
|
@@ -108,6 +120,9 @@ export function createEventMultiplexer(options = {}) {
|
|
|
108
120
|
options.signal?.removeEventListener("abort", abort);
|
|
109
121
|
finishSources();
|
|
110
122
|
queue.length = 0;
|
|
123
|
+
// Free the single-consumer slot: a consumer suspended at a yield may never pull
|
|
124
|
+
// again, and after close a fresh subscriber terminates immediately anyway.
|
|
125
|
+
activeConsumer = false;
|
|
111
126
|
if (waiter) {
|
|
112
127
|
const resolve = waiter;
|
|
113
128
|
waiter = undefined;
|
|
@@ -115,23 +130,31 @@ export function createEventMultiplexer(options = {}) {
|
|
|
115
130
|
}
|
|
116
131
|
}
|
|
117
132
|
async function* subscribe() {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
if (activeConsumer)
|
|
134
|
+
throw new EventMultiplexerError();
|
|
135
|
+
activeConsumer = true;
|
|
136
|
+
try {
|
|
137
|
+
while (true) {
|
|
138
|
+
if (queue.length > 0) {
|
|
139
|
+
if (options.compare)
|
|
140
|
+
queue.sort(options.compare);
|
|
141
|
+
yield queue.shift();
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (isClosed)
|
|
145
|
+
return;
|
|
146
|
+
const next = await new Promise((resolve) => {
|
|
147
|
+
waiter = resolve;
|
|
148
|
+
});
|
|
149
|
+
if (next.done)
|
|
150
|
+
return;
|
|
151
|
+
if (next.value === WAKE)
|
|
152
|
+
continue;
|
|
153
|
+
yield next.value;
|
|
124
154
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
waiter = resolve;
|
|
129
|
-
});
|
|
130
|
-
if (next.done)
|
|
131
|
-
return;
|
|
132
|
-
if (next.value === WAKE)
|
|
133
|
-
continue;
|
|
134
|
-
yield next.value;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
activeConsumer = false;
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type { ContextBudget, ContextBudgetMessageGroups, ContextBudgetOmission,
|
|
|
27
27
|
export { applyContextBudget, CONTEXT_BUDGET_ERROR_CODE, CONTEXT_BUDGET_REPORT_METADATA_KEY, ContextBudgetError, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS, estimateAssemblyTokens, estimateMessageBytes, estimateMessageTokens, estimateTextBytes, estimateTextTokens, getContextBudgetReport, HARD_MAX_CONTEXT_BUDGET_BYTES, HARD_MAX_CONTEXT_BUDGET_OMISSIONS, HARD_MAX_CONTEXT_BUDGET_TOKENS, isContextBudgetError, resolveContextBudget, } from "./context-budget.js";
|
|
28
28
|
export type * from "./contracts.js";
|
|
29
29
|
export type { ApprovalOutcome, DecisionScope, NestedRunApproval, NestedRunOutcome, NestedRunRef, PendingDecision, PendingDecisionKind, ProviderResolver, RealtimeCaps, RealtimeEvent, RealtimeSession, RealtimeSessionFactory, RealtimeSessionOptions, ResumeNestedRun, RunDecision, RunLimitCounters, RunLimitName, SecureAgentOptions, StickyDecision, ToolCallAuthority, ToolEffectClassifier, ToolEffectDeclaration, ToolEffectIdempotency, ToolEffectKey, ToolEffectKind, ToolEffectRecord, ToolEffectStatus, ToolEffectStore, ToolEffectTransition, ToolElicitationRequest, } from "./contracts.js";
|
|
30
|
-
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
30
|
+
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionMetadataConflict, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_METADATA_CONFLICT_CODE, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionMetadataConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
31
31
|
export { parseAgentFile, parseSkillFile } from "./contribution-parsing.js";
|
|
32
32
|
export type { ContributionRegistries, ContributionRegistriesOptions, ContributionRegistry, ContributionRegistryOptions, } from "./contributions.js";
|
|
33
33
|
export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
|
|
@@ -38,7 +38,7 @@ export { createChainedCredentialResolver, createEnvCredentialResolver, createExp
|
|
|
38
38
|
export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConformanceResult, DeviceKind, DevicePolicyErrorCode, DevicePolicyOptions, DeviceStreamLimits, ResolvedDevicePolicy, } from "./devices.js";
|
|
39
39
|
export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
|
|
40
40
|
export type { EventMultiplexer, EventMultiplexerOptions, EventOverflowInfo, EventOverflowPolicy } from "./event-multiplexer.js";
|
|
41
|
-
export { createEventMultiplexer } from "./event-multiplexer.js";
|
|
41
|
+
export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
|
|
42
42
|
export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
|
|
43
43
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
44
44
|
export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
|
|
@@ -109,5 +109,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
|
|
|
109
109
|
export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
|
|
110
110
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
111
111
|
export declare const name = "prism";
|
|
112
|
-
export declare const version = "0.2.
|
|
112
|
+
export declare const version = "0.2.2";
|
|
113
113
|
export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
package/dist/index.js
CHANGED
|
@@ -14,13 +14,13 @@ export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compac
|
|
|
14
14
|
export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } from "./config.js";
|
|
15
15
|
export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMessagesSupportModelCapabilities, assertModelSupportsContentBlocks, assertSsrfAllowedUrl, collectMessageContentBlocks, contentBlockInputModality, DEFAULT_MAX_AUDIO_DURATION_MS, DEFAULT_MAX_MEDIA_ITEM_BYTES, DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST, DEFAULT_MAX_MEDIA_REQUEST_BYTES, DEFAULT_MEDIA_FETCH_TIMEOUT_MS, loadBoundedBinaryResource, MediaContentError, MODEL_INPUT_CAPABILITIES, resolveMediaContentBlock, resolveMediaContentBlocks, sniffMediaMimeType, UnsupportedModalityError, } from "./content.js";
|
|
16
16
|
export { applyContextBudget, CONTEXT_BUDGET_ERROR_CODE, CONTEXT_BUDGET_REPORT_METADATA_KEY, ContextBudgetError, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS, estimateAssemblyTokens, estimateMessageBytes, estimateMessageTokens, estimateTextBytes, estimateTextTokens, getContextBudgetReport, HARD_MAX_CONTEXT_BUDGET_BYTES, HARD_MAX_CONTEXT_BUDGET_OMISSIONS, HARD_MAX_CONTEXT_BUDGET_TOKENS, isContextBudgetError, resolveContextBudget, } from "./context-budget.js";
|
|
17
|
-
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
17
|
+
export { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, assertSessionMetadataKey, DEFAULT_MAX_PENDING_DECISIONS, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_STICKY_DECISIONS, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_ACTION_CONSTRAINT_BYTES, HARD_MAX_ACTION_CONSTRAINTS, HARD_MAX_DECISION_REASON_BYTES, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, HARD_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEERS, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_STICKY_DECISIONS, isSessionAppendConflict, isSessionEntryKind, isSessionMetadataConflict, isSessionSearchUnsupported, MAX_ACTION_CONSTRAINT_BYTES, MAX_ACTION_CONSTRAINTS, MAX_ATTRIBUTION_DEPTH, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SESSION_METADATA_CONFLICT_CODE, SESSION_SEARCH_UNSUPPORTED_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionMetadataConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
|
|
18
18
|
export { parseAgentFile, parseSkillFile } from "./contribution-parsing.js";
|
|
19
19
|
export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
|
|
20
20
|
export { CONVERSATION_METADATA_KEY, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, decodeConversationReplayCursor, encodeConversationReplayCursor, HARD_MAX_CONVERSATION_CURSOR_BYTES, } from "./conversations.js";
|
|
21
21
|
export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue, revokeOAuthCredential, } from "./credentials.js";
|
|
22
22
|
export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
|
|
23
|
-
export { createEventMultiplexer } from "./event-multiplexer.js";
|
|
23
|
+
export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
|
|
24
24
|
export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
|
|
25
25
|
export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
|
|
26
26
|
export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
|
|
@@ -60,6 +60,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
|
|
|
60
60
|
export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
|
|
61
61
|
export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
|
|
62
62
|
export const name = "prism";
|
|
63
|
-
export const version = "0.2.
|
|
63
|
+
export const version = "0.2.2";
|
|
64
64
|
export const description = "Agent harness for AI providers, agents, sessions, and tools.";
|
|
65
65
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PersistencePage, SessionEntry, SessionEntryQuery } from "../contracts.js";
|
|
2
2
|
/** Current shared persistence schema version for production database adapters. */
|
|
3
|
-
export declare const PERSISTENCE_SCHEMA_VERSION =
|
|
3
|
+
export declare const PERSISTENCE_SCHEMA_VERSION = 8;
|
|
4
4
|
export type PersistenceTableName = "prism_tenants" | "prism_accounts" | "prism_users" | "prism_agent_definitions" | "prism_sessions" | "prism_branches" | "prism_session_entries" | "prism_session_append_idempotency" | "prism_runs" | "prism_agent_events" | "prism_agent_event_streams" | "prism_tool_calls" | "prism_usage" | "prism_run_feedback" | "prism_retention_policies" | "prism_legal_holds" | "prism_tenant_quotas" | "prism_migrations";
|
|
5
5
|
export type PersistenceColumnType = "text" | "integer" | "number" | "boolean" | "json" | "timestamp";
|
|
6
6
|
export interface PersistenceColumnDefinition {
|
|
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
// this module defines the shared table/index/pagination/migration expectations
|
|
5
5
|
// adapter authors implement and test against before shipping dialect-specific DDL.
|
|
6
6
|
/** Current shared persistence schema version for production database adapters. */
|
|
7
|
-
export const PERSISTENCE_SCHEMA_VERSION =
|
|
7
|
+
export const PERSISTENCE_SCHEMA_VERSION = 8;
|
|
8
8
|
/** Guidance adapters must follow: values are bound parameters, never interpolated. */
|
|
9
9
|
export const PARAMETERIZED_QUERY_GUIDANCE = "Bind every user-supplied value (session ids, idempotency keys, tenant ids, timestamps, JSON payloads) as a query parameter. Quote/validate schema and table identifiers only; never interpolate untrusted strings into SQL text.";
|
|
10
10
|
const TENANT_COLUMNS = [
|
|
@@ -81,6 +81,7 @@ export function createPersistenceSchemaModel() {
|
|
|
81
81
|
{ name: "expires_at", type: "timestamp", nullable: true },
|
|
82
82
|
{ name: "retention_policy_id", type: "text", nullable: true },
|
|
83
83
|
{ name: "metadata", type: "json", nullable: true },
|
|
84
|
+
{ name: "version", type: "integer", defaultValue: "0" },
|
|
84
85
|
],
|
|
85
86
|
},
|
|
86
87
|
{
|
|
@@ -499,33 +500,35 @@ function migrationStep(version, name, description) {
|
|
|
499
500
|
}
|
|
500
501
|
: version === 7
|
|
501
502
|
? { indexes: ["prism_agent_events_owner_timestamp_sequence_idx"] }
|
|
502
|
-
: version ===
|
|
503
|
-
? {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
: version ===
|
|
510
|
-
? {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
"
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
503
|
+
: version === 8
|
|
504
|
+
? { table: "prism_sessions", columns: ["version"] }
|
|
505
|
+
: version === 6
|
|
506
|
+
? {
|
|
507
|
+
tables: ["prism_agent_event_streams"],
|
|
508
|
+
indexes: ["prism_agent_events_run_sequence_idx"],
|
|
509
|
+
}
|
|
510
|
+
: version === 2
|
|
511
|
+
? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
|
|
512
|
+
: version === 3
|
|
513
|
+
? {
|
|
514
|
+
tables: ["prism_run_feedback"],
|
|
515
|
+
indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name),
|
|
516
|
+
}
|
|
517
|
+
: version === 4
|
|
518
|
+
? // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
|
|
519
|
+
{ search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
|
|
520
|
+
: version === 5
|
|
521
|
+
? {
|
|
522
|
+
tables: ["prism_legal_holds", "prism_tenant_quotas"],
|
|
523
|
+
indexes: [
|
|
524
|
+
"prism_legal_holds_owner_resource_idx",
|
|
525
|
+
"prism_legal_holds_created_id_idx",
|
|
526
|
+
"prism_tenant_quotas_owner_kind_idx",
|
|
527
|
+
],
|
|
528
|
+
}
|
|
529
|
+
: (() => {
|
|
530
|
+
throw new Error(`Unknown migration version ${version}`);
|
|
531
|
+
})();
|
|
529
532
|
return {
|
|
530
533
|
version,
|
|
531
534
|
name,
|
|
@@ -546,6 +549,7 @@ export function createPersistenceMigrationContract() {
|
|
|
546
549
|
migrationStep(5, "005_lifecycle_hold_quota", "Add legal-hold and tenant-quota tables for retention lifecycle."),
|
|
547
550
|
migrationStep(6, "006_agent_event_source", "Add transactional per-run event counters and unique durable event sequencing."),
|
|
548
551
|
migrationStep(7, "007_agent_event_retention_index", "Add an exact-owner durable-event retention cleanup index."),
|
|
552
|
+
migrationStep(8, "008_session_version", "Add a NOT NULL DEFAULT 0 version column to prism_sessions for appendSession metadata CAS."),
|
|
549
553
|
],
|
|
550
554
|
lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
|
|
551
555
|
leastPrivilegeGuidance: "Run migrations with a DDL-capable role; use a separate least-privilege runtime role limited to INSERT/SELECT/UPDATE on adapter tables. Never grant migration credentials to the agent runtime.",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { AgentEventSource, AgentIdentity, CheckpointStore, ProductionPersistenceStore } from "../contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Narrow router-state seam (structurally satisfied by
|
|
4
|
+
* `@arnilo/prism-model-router`'s `ModelRouterStateStore`). Core stays
|
|
5
|
+
* dependency-free; adapter packages pass their real stores.
|
|
6
|
+
*/
|
|
7
|
+
export interface StateConcurrencyRouterKey {
|
|
8
|
+
readonly tenantId: string;
|
|
9
|
+
readonly accountId?: string;
|
|
10
|
+
readonly userId?: string;
|
|
11
|
+
readonly principalId: string;
|
|
12
|
+
readonly provider: string;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
}
|
|
15
|
+
export interface StateConcurrencyRouterStore {
|
|
16
|
+
reserveBudget(input: {
|
|
17
|
+
readonly key: StateConcurrencyRouterKey;
|
|
18
|
+
readonly tokens?: number;
|
|
19
|
+
readonly costUsd?: number;
|
|
20
|
+
readonly maxTokens?: number;
|
|
21
|
+
readonly maxCostUsd?: number;
|
|
22
|
+
readonly windowMs: number;
|
|
23
|
+
readonly reservationTtlMs: number;
|
|
24
|
+
readonly now: number;
|
|
25
|
+
readonly maxBudgetKeys?: number;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
readonly admitted: boolean;
|
|
28
|
+
readonly reservationId?: string;
|
|
29
|
+
readonly fencingToken?: string;
|
|
30
|
+
readonly retryAfterMs?: number;
|
|
31
|
+
}>;
|
|
32
|
+
commitBudget(input: {
|
|
33
|
+
readonly key: StateConcurrencyRouterKey;
|
|
34
|
+
readonly reservationId: string;
|
|
35
|
+
readonly fencingToken: string;
|
|
36
|
+
readonly tokens?: number;
|
|
37
|
+
readonly costUsd?: number;
|
|
38
|
+
readonly windowMs: number;
|
|
39
|
+
readonly now: number;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
readonly unknownUsage: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
releaseBudget(input: {
|
|
44
|
+
readonly key: StateConcurrencyRouterKey;
|
|
45
|
+
readonly reservationId: string;
|
|
46
|
+
readonly fencingToken: string;
|
|
47
|
+
readonly windowMs: number;
|
|
48
|
+
readonly now: number;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
readBudget(input: {
|
|
51
|
+
readonly key: StateConcurrencyRouterKey;
|
|
52
|
+
readonly windowMs: number;
|
|
53
|
+
readonly now: number;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
readonly tokens: number;
|
|
56
|
+
readonly costUsd: number;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
export interface StateConcurrencyRouterFactory {
|
|
60
|
+
readonly create: () => StateConcurrencyRouterStore | Promise<StateConcurrencyRouterStore>;
|
|
61
|
+
/**
|
|
62
|
+
* True when the store derives reservation expiry from the caller-supplied
|
|
63
|
+
* `now` (in-memory stores). Durable stores compute expiry from their own
|
|
64
|
+
* clock (`clock_timestamp()`), so the deterministic unknown-outcome probe
|
|
65
|
+
* only runs when this is set; the durable leg is exercised by the Task 1
|
|
66
|
+
* enterprise-conformance integration probe (real 1ms TTL, `test:postgres`).
|
|
67
|
+
*/
|
|
68
|
+
readonly nowInjected?: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** Narrow idempotency seam (structurally satisfied by `@arnilo/prism-work-tools`'s `IdempotencyStore`). */
|
|
71
|
+
export interface StateConcurrencyIdempotencyStore {
|
|
72
|
+
get(input: StateConcurrencyIdempotencyKey): Promise<StateConcurrencyIdempotencyRecord | undefined>;
|
|
73
|
+
begin(input: StateConcurrencyIdempotencyKey): Promise<{
|
|
74
|
+
readonly outcome: "acquired" | "existing";
|
|
75
|
+
readonly record: StateConcurrencyIdempotencyRecord;
|
|
76
|
+
}>;
|
|
77
|
+
complete(input: StateConcurrencyIdempotencyKey & {
|
|
78
|
+
readonly claimToken: string;
|
|
79
|
+
readonly expectedVersion: number;
|
|
80
|
+
readonly result: {
|
|
81
|
+
readonly draftId: string;
|
|
82
|
+
readonly resourceId?: string;
|
|
83
|
+
};
|
|
84
|
+
}): Promise<StateConcurrencyIdempotencyRecord>;
|
|
85
|
+
}
|
|
86
|
+
export interface StateConcurrencyIdempotencyKey {
|
|
87
|
+
readonly identity: AgentIdentity;
|
|
88
|
+
readonly key: string;
|
|
89
|
+
readonly op: string;
|
|
90
|
+
readonly signal?: AbortSignal;
|
|
91
|
+
}
|
|
92
|
+
export interface StateConcurrencyIdempotencyRecord {
|
|
93
|
+
readonly tenantId?: string;
|
|
94
|
+
readonly accountId?: string;
|
|
95
|
+
readonly userId?: string;
|
|
96
|
+
readonly principalId: string;
|
|
97
|
+
readonly key: string;
|
|
98
|
+
readonly op: string;
|
|
99
|
+
readonly status: "in_progress" | "completed" | "failed_retryable" | "failed_terminal" | "unknown";
|
|
100
|
+
readonly attempt: number;
|
|
101
|
+
readonly version: number;
|
|
102
|
+
readonly claimToken?: string;
|
|
103
|
+
readonly result?: {
|
|
104
|
+
readonly draftId: string;
|
|
105
|
+
readonly resourceId?: string;
|
|
106
|
+
};
|
|
107
|
+
readonly failure?: {
|
|
108
|
+
readonly code: string;
|
|
109
|
+
readonly reference?: string;
|
|
110
|
+
};
|
|
111
|
+
readonly createdAt: string;
|
|
112
|
+
readonly updatedAt: string;
|
|
113
|
+
readonly expiresAt?: string;
|
|
114
|
+
}
|
|
115
|
+
export type StateConcurrencyEventSource = AgentEventSource & {
|
|
116
|
+
readonly close?: () => void | Promise<void>;
|
|
117
|
+
};
|
|
118
|
+
export interface StateConcurrencyEventSourceFactory {
|
|
119
|
+
readonly create: () => StateConcurrencyEventSource | Promise<StateConcurrencyEventSource>;
|
|
120
|
+
/**
|
|
121
|
+
* True when the factory can re-open against the same backend (durable
|
|
122
|
+
* stores). Memory stores are process-local; their probe covers cursor
|
|
123
|
+
* resume on the same instance and durable factories additionally re-open.
|
|
124
|
+
*/
|
|
125
|
+
readonly reopenable?: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface StateConcurrencyFactories {
|
|
128
|
+
/** Checkpoint seam: approval-determinism and checkpoint-CAS probes. */
|
|
129
|
+
readonly checkpoints?: () => CheckpointStore | Promise<CheckpointStore>;
|
|
130
|
+
/** Durable event seam: replay-cursor resume probe (memory and NATS/Postgres). */
|
|
131
|
+
readonly events?: StateConcurrencyEventSourceFactory;
|
|
132
|
+
/** Session-record seam: conversation metadata CAS probe (appendSession). */
|
|
133
|
+
readonly sessions?: () => ProductionPersistenceStore | Promise<ProductionPersistenceStore>;
|
|
134
|
+
/** Work idempotency seam: retry-same-key probe. */
|
|
135
|
+
readonly idempotency?: () => StateConcurrencyIdempotencyStore | Promise<StateConcurrencyIdempotencyStore>;
|
|
136
|
+
/** Router budget reservation seam: oversubscription + unknown-outcome probes. */
|
|
137
|
+
readonly routerState?: StateConcurrencyRouterFactory;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Run the state-concurrency probes for every provided store family. Returns
|
|
141
|
+
* the executed probe names so gates can assert coverage. Deterministic:
|
|
142
|
+
* concurrent ops are awaited through `Promise.allSettled` and state is
|
|
143
|
+
* asserted afterward; no timing-only sleeps anywhere in this file.
|
|
144
|
+
*/
|
|
145
|
+
export declare function assertStateConcurrencyConforms(factories: StateConcurrencyFactories): Promise<readonly string[]>;
|