@arnilo/prism 0.0.23 → 0.0.24

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 (42) hide show
  1. package/CHANGELOG.md +19 -3
  2. package/dist/agent-event-source.d.ts +11 -0
  3. package/dist/agent-event-source.js +512 -0
  4. package/dist/agent-run-state.js +6 -1
  5. package/dist/agents.js +7 -2
  6. package/dist/contracts.d.ts +157 -0
  7. package/dist/index.d.ts +7 -2
  8. package/dist/index.js +4 -1
  9. package/dist/testing/agent-event-source-conformance.d.ts +4 -0
  10. package/dist/testing/agent-event-source-conformance.js +54 -0
  11. package/dist/testing/persistence-schema.d.ts +2 -2
  12. package/dist/testing/persistence-schema.js +58 -21
  13. package/dist/testing/tool-effect-store-conformance.d.ts +9 -0
  14. package/dist/testing/tool-effect-store-conformance.js +85 -0
  15. package/dist/tool-effects.d.ts +15 -0
  16. package/dist/tool-effects.js +338 -0
  17. package/dist/tools.d.ts +3 -1
  18. package/dist/tools.js +204 -9
  19. package/docs/0.1.0-readiness.md +9 -9
  20. package/docs/a2a.md +6 -2
  21. package/docs/ag-ui-adoption.md +77 -0
  22. package/docs/ag-ui.md +39 -42
  23. package/docs/agent-events.md +5 -1
  24. package/docs/browser-automation.md +2 -0
  25. package/docs/coding-agent-tools.md +2 -0
  26. package/docs/database-persistence.md +2 -0
  27. package/docs/enterprise-postgres-state.md +5 -1
  28. package/docs/host-security.md +8 -1
  29. package/docs/index.md +11 -9
  30. package/docs/mcp-tools.md +17 -2
  31. package/docs/migration.md +22 -0
  32. package/docs/performance.md +24 -0
  33. package/docs/postgres-persistence.md +5 -2
  34. package/docs/public-contracts.md +2 -0
  35. package/docs/release-and-install.md +52 -693
  36. package/docs/server.md +9 -6
  37. package/docs/sqlite-persistence.md +10 -2
  38. package/docs/supervisors.md +2 -0
  39. package/docs/tool-effects.md +95 -0
  40. package/docs/tools.md +4 -0
  41. package/docs/work-tools.md +4 -0
  42. package/package.json +10 -2
@@ -365,6 +365,8 @@ export interface RunOptions {
365
365
  readonly metadata?: Readonly<Record<string, unknown>>;
366
366
  readonly redactor?: SecretRedactor;
367
367
  readonly runLedger?: RunLedger;
368
+ /** Optional durable recovery store. Per-run value overrides this agent default. */
369
+ readonly effectStore?: ToolEffectStore;
368
370
  readonly ownership?: OwnershipScope;
369
371
  /** Host-verified identity; when set, must project onto `ownership` without widening. */
370
372
  readonly identity?: import("./identity.js").AgentIdentity;
@@ -444,6 +446,8 @@ export interface AgentConfig {
444
446
  readonly systemPrompt?: SystemPromptConfig;
445
447
  readonly redactor?: SecretRedactor;
446
448
  readonly runLedger?: RunLedger;
449
+ /** Optional durable recovery store. */
450
+ readonly effectStore?: ToolEffectStore;
447
451
  readonly ownership?: OwnershipScope;
448
452
  /** Host-verified identity default for sessions created from this agent. */
449
453
  readonly identity?: import("./identity.js").AgentIdentity;
@@ -834,12 +838,23 @@ export type AgentEvent = {
834
838
  readonly attempt: number;
835
839
  readonly result: ArtifactValidation;
836
840
  };
841
+ export type ToolEffectKind = "none" | "local_mutation" | "external_mutation";
842
+ export type ToolEffectIdempotency = "none" | "optional" | "required" | "tool_managed" | "unsupported";
843
+ /** Static or validated-argument classification of one tool call's side-effect behavior. */
844
+ export interface ToolEffectDeclaration {
845
+ readonly kind: ToolEffectKind;
846
+ readonly idempotency: ToolEffectIdempotency;
847
+ }
848
+ /** Runs after argument validation. It must be synchronous, deterministic, bounded, and side-effect-free. */
849
+ export type ToolEffectClassifier = (args: JsonObject, context: ToolExecutionContext) => ToolEffectDeclaration;
837
850
  export interface ToolDefinition {
838
851
  readonly name: string;
839
852
  readonly description?: string;
840
853
  readonly parameters?: JsonObject;
841
854
  /** Force any provider turn containing this tool to dispatch sequentially. */
842
855
  readonly exclusive?: boolean;
856
+ /** Optional side-effect declaration. Omitted tools retain legacy unmanaged dispatch. */
857
+ readonly effect?: ToolEffectDeclaration | ToolEffectClassifier;
843
858
  execute(args: JsonObject, context: ToolExecutionContext): Promise<ToolResult> | ToolResult;
844
859
  }
845
860
  export interface ToolRegistry {
@@ -856,6 +871,8 @@ export interface ToolExecutionContext {
856
871
  readonly metadata?: Readonly<Record<string, unknown>>;
857
872
  /** Host-verified identity for this tool invocation, when enterprise identity is active. */
858
873
  readonly identity?: import("./identity.js").AgentIdentity;
874
+ /** Core-derived stable effect key. Never accept a model-supplied key as authority. */
875
+ readonly idempotencyKey?: string;
859
876
  progress?(progress?: unknown, metadata?: Readonly<Record<string, unknown>>): void | Promise<void>;
860
877
  }
861
878
  export interface ToolResult {
@@ -866,6 +883,90 @@ export interface ToolResult {
866
883
  readonly error?: ErrorInfo;
867
884
  readonly metadata?: Readonly<Record<string, unknown>>;
868
885
  }
886
+ export type ToolEffectStatus = "pending" | "dispatched" | "completed" | "failed_retryable" | "failed_terminal" | "unknown";
887
+ export interface ToolEffectRecord extends OwnershipScope {
888
+ readonly key: string;
889
+ readonly sessionId: string;
890
+ readonly runId: string;
891
+ readonly toolCallId: string;
892
+ readonly toolName: string;
893
+ readonly argumentsHash: string;
894
+ readonly status: ToolEffectStatus;
895
+ readonly attempt: number;
896
+ readonly version: number;
897
+ readonly claimToken?: string;
898
+ readonly result?: ToolResult;
899
+ readonly resultRef?: string;
900
+ readonly failure?: {
901
+ readonly code: string;
902
+ readonly reference?: string;
903
+ };
904
+ readonly createdAt: string;
905
+ readonly updatedAt: string;
906
+ readonly expiresAt?: string;
907
+ }
908
+ export interface ToolEffectKey {
909
+ readonly identity: import("./identity.js").AgentIdentity;
910
+ readonly ownership: OwnershipScope;
911
+ readonly key: string;
912
+ readonly sessionId: string;
913
+ readonly runId: string;
914
+ readonly toolCallId: string;
915
+ readonly toolName: string;
916
+ readonly argumentsHash: string;
917
+ readonly signal?: AbortSignal;
918
+ }
919
+ export interface ToolEffectTransition extends ToolEffectKey {
920
+ readonly claimToken: string;
921
+ readonly expectedVersion: number;
922
+ }
923
+ /** Durable claim/CAS store for recoverable tool effects. */
924
+ export interface ToolEffectStore {
925
+ get(input: ToolEffectKey): Promise<ToolEffectRecord | undefined>;
926
+ begin(input: ToolEffectKey & {
927
+ readonly claimTtlMs?: number;
928
+ readonly maxAttempts?: number;
929
+ }): Promise<{
930
+ readonly outcome: "acquired" | "existing";
931
+ readonly record: ToolEffectRecord;
932
+ }>;
933
+ markDispatched(input: ToolEffectTransition): Promise<ToolEffectRecord>;
934
+ complete(input: ToolEffectTransition & {
935
+ readonly result?: ToolResult;
936
+ readonly resultRef?: string;
937
+ }): Promise<ToolEffectRecord>;
938
+ fail(input: ToolEffectTransition & {
939
+ readonly status: "failed_retryable" | "failed_terminal";
940
+ readonly failure: {
941
+ readonly code: string;
942
+ readonly reference?: string;
943
+ };
944
+ }): Promise<ToolEffectRecord>;
945
+ markUnknown(input: ToolEffectTransition & {
946
+ readonly failure?: {
947
+ readonly code: string;
948
+ readonly reference?: string;
949
+ };
950
+ }): Promise<ToolEffectRecord>;
951
+ resolveUnknown(input: ToolEffectKey & {
952
+ readonly expectedVersion: number;
953
+ readonly status: "completed" | "failed_retryable" | "failed_terminal";
954
+ readonly result?: ToolResult;
955
+ readonly resultRef?: string;
956
+ readonly failure?: {
957
+ readonly code: string;
958
+ readonly reference?: string;
959
+ };
960
+ }): Promise<ToolEffectRecord>;
961
+ cleanup(input: {
962
+ readonly ownership: OwnershipScope;
963
+ readonly before: string;
964
+ readonly limit?: number;
965
+ readonly signal?: AbortSignal;
966
+ }): Promise<{
967
+ readonly deleted: number;
968
+ }>;
969
+ }
869
970
  export interface CommandDefinition {
870
971
  readonly name: string;
871
972
  readonly description?: string;
@@ -1446,6 +1547,8 @@ export interface AgentEventRecord extends OwnershipScope {
1446
1547
  readonly id: string;
1447
1548
  readonly sessionId: string;
1448
1549
  readonly runId?: string;
1550
+ /** Durable sources allocate positive, strictly increasing per-run positions. */
1551
+ readonly sequence?: number;
1449
1552
  readonly entryId?: string;
1450
1553
  readonly type: AgentEventType;
1451
1554
  readonly timestamp: string;
@@ -1453,6 +1556,58 @@ export interface AgentEventRecord extends OwnershipScope {
1453
1556
  readonly redacted: boolean;
1454
1557
  readonly metadata?: Readonly<Record<string, unknown>>;
1455
1558
  }
1559
+ /** An event record returned by an {@link AgentEventSource}. */
1560
+ export interface DurableAgentEventRecord extends AgentEventRecord {
1561
+ readonly runId: string;
1562
+ readonly sequence: number;
1563
+ }
1564
+ export interface AgentEventEnvelope {
1565
+ readonly record: DurableAgentEventRecord;
1566
+ /** Opaque cursor immediately after `record`. */
1567
+ readonly cursor: string;
1568
+ }
1569
+ export interface AgentEventSourcePage {
1570
+ readonly items: readonly AgentEventEnvelope[];
1571
+ readonly nextCursor?: string;
1572
+ /** True only after every event preceding a terminal event has been returned. */
1573
+ readonly terminal: boolean;
1574
+ }
1575
+ /** Exact-owned, per-run durable event read. `after` is exclusive. */
1576
+ export interface AgentEventSourceRead {
1577
+ readonly ownership: OwnershipScope;
1578
+ readonly sessionId: string;
1579
+ readonly runId: string;
1580
+ readonly after?: string;
1581
+ readonly limit?: number;
1582
+ readonly signal?: AbortSignal;
1583
+ }
1584
+ export interface AgentEventSourceCleanup {
1585
+ readonly ownership: OwnershipScope;
1586
+ readonly before: string;
1587
+ readonly limit?: number;
1588
+ readonly signal?: AbortSignal;
1589
+ }
1590
+ export interface AgentEventSourceOptions {
1591
+ readonly maxEventBytes?: number;
1592
+ readonly maxPageSize?: number;
1593
+ readonly maxCursorBytes?: number;
1594
+ readonly maxQueuedEvents?: number;
1595
+ readonly maxSubscribers?: number;
1596
+ readonly pollIntervalMs?: number;
1597
+ readonly reconnectInitialMs?: number;
1598
+ readonly reconnectMaxMs?: number;
1599
+ readonly maxRetainedEventsPerRun?: number;
1600
+ readonly maxRetentionAgeMs?: number;
1601
+ }
1602
+ /** Optional durable event capability. `RunLedger` remains a write-only contract. */
1603
+ export interface AgentEventSource {
1604
+ append(record: AgentEventRecord): Promise<DurableAgentEventRecord>;
1605
+ page(input: AgentEventSourceRead): Promise<AgentEventSourcePage>;
1606
+ subscribe(input: AgentEventSourceRead): AsyncIterable<AgentEventEnvelope>;
1607
+ cleanup(input: AgentEventSourceCleanup): Promise<{
1608
+ readonly deleted: number;
1609
+ }>;
1610
+ }
1456
1611
  export type ToolCallStatus = "started" | "finished" | "error" | "blocked";
1457
1612
  /** Stored tool-call row. The `result` payload should be redacted before storage when secrets are present. */
1458
1613
  export interface ToolCallRecord extends OwnershipScope {
@@ -1715,6 +1870,8 @@ export interface ProductionPersistenceStore {
1715
1870
  readonly leases?: LeaseStore;
1716
1871
  /** Optional immutable run/trace feedback storage capability. */
1717
1872
  readonly feedback?: RunFeedbackStore;
1873
+ /** Optional durable, cross-replica-capable event source. */
1874
+ readonly events?: AgentEventSource;
1718
1875
  querySessions(query: SessionQuery): Promise<PersistencePage<SessionRecord>>;
1719
1876
  queryBranches(query: BranchQuery): Promise<PersistencePage<BranchRecord>>;
1720
1877
  queryEntries(query: SessionEntryQuery): Promise<PersistencePage<SessionEntry>>;
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ export { assertDeclaredMediaTypeMatches, assertMediaBlocksWithinBounds, assertMe
20
20
  export type { ContextBudget, ContextBudgetMessageGroups, ContextBudgetOmission, ContextBudgetOmissionKind, ContextBudgetReport, } from "./context-budget.js";
21
21
  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";
22
22
  export type * from "./contracts.js";
23
- export type { ProviderResolver, RealtimeCaps, RealtimeEvent, RealtimeSession, RealtimeSessionFactory, RealtimeSessionOptions, RunLimitCounters, RunLimitName, SecureAgentOptions, ToolCallAuthority, } from "./contracts.js";
23
+ export type { ProviderResolver, RealtimeCaps, RealtimeEvent, RealtimeSession, RealtimeSessionFactory, RealtimeSessionOptions, RunLimitCounters, RunLimitName, SecureAgentOptions, ToolCallAuthority, ToolEffectClassifier, ToolEffectDeclaration, ToolEffectIdempotency, ToolEffectKey, ToolEffectKind, ToolEffectRecord, ToolEffectStatus, ToolEffectStore, ToolEffectTransition, } from "./contracts.js";
24
24
  export { AgentRunError, AgentRunStateError, assertSessionMetadataKey, 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_SESSION_SEARCH_LIMIT, 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, isSessionAppendConflict, isSessionEntryKind, isSessionSearchUnsupported, 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";
25
25
  export { parseAgentFile, parseSkillFile } from "./contribution-parsing.js";
26
26
  export type { ContributionRegistries, ContributionRegistriesOptions, ContributionRegistry, ContributionRegistryOptions, } from "./contributions.js";
@@ -31,6 +31,9 @@ export type { CredentialRecord, CredentialValueSource, MemoryCredentialStore, Re
31
31
  export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue, revokeOAuthCredential, } from "./credentials.js";
32
32
  export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConformanceResult, DeviceKind, DevicePolicyErrorCode, DevicePolicyOptions, DeviceStreamLimits, ResolvedDevicePolicy, } from "./devices.js";
33
33
  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";
34
+ export type { AgentEventSourceErrorCode } from "./agent-event-source.js";
35
+ export { AgentEventSourceError, createMemoryAgentEventSource } from "./agent-event-source.js";
36
+ export { assertAgentEventSourceConforms } from "./testing/agent-event-source-conformance.js";
34
37
  export type { EventMultiplexer, EventMultiplexerOptions, EventOverflowInfo, EventOverflowPolicy } from "./event-multiplexer.js";
35
38
  export { createEventMultiplexer } from "./event-multiplexer.js";
36
39
  export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
@@ -97,8 +100,10 @@ export type { ThinkingCompatFamily, ThinkingLevel } from "./thinking.js";
97
100
  export { applyThinkingLevel, isThinkingLevel, normalizeThinkingLevel, THINKING_LEVELS, thinkingCompatFor, thinkingFamilyForModel, } from "./thinking.js";
98
101
  export type { DispatchToolCallOptions, ToolArgumentValidationError, ToolArgumentValidationResult, ToolArgumentValidator, ToolFilter, ToolFilterInput, ToolParameterValidatorOptions, ToolRegistryOptions, ToolValidator, } from "./tools.js";
99
102
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
103
+ export { createMemoryToolEffectStore, ToolEffectError } from "./tool-effects.js";
104
+ export type { ToolEffectErrorCode } from "./tool-effects.js";
100
105
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
101
106
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
102
107
  export declare const name = "prism";
103
- export declare const version = "0.0.23";
108
+ export declare const version = "0.0.24";
104
109
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ export { createContributionRegistries, createContributionRegistry, registerDisco
16
16
  export { CONVERSATION_METADATA_KEY, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, decodeConversationReplayCursor, encodeConversationReplayCursor, HARD_MAX_CONVERSATION_CURSOR_BYTES, } from "./conversations.js";
17
17
  export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue, revokeOAuthCredential, } from "./credentials.js";
18
18
  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";
19
+ export { AgentEventSourceError, createMemoryAgentEventSource } from "./agent-event-source.js";
20
+ export { assertAgentEventSourceConforms } from "./testing/agent-event-source-conformance.js";
19
21
  export { createEventMultiplexer } from "./event-multiplexer.js";
20
22
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
21
23
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
@@ -52,8 +54,9 @@ export { artifactStructuredOutputRequest, assertStructuredOutputRequestSupported
52
54
  export { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
53
55
  export { applyThinkingLevel, isThinkingLevel, normalizeThinkingLevel, THINKING_LEVELS, thinkingCompatFor, thinkingFamilyForModel, } from "./thinking.js";
54
56
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
57
+ export { createMemoryToolEffectStore, ToolEffectError } from "./tool-effects.js";
55
58
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
56
59
  export const name = "prism";
57
- export const version = "0.0.23";
60
+ export const version = "0.0.24";
58
61
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
59
62
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import type { AgentEventSource } from "../contracts.js";
2
+ export type AgentEventSourceConformanceFactory = () => AgentEventSource | Promise<AgentEventSource>;
3
+ /** Assert durable append/page/replay ownership and cursor behavior without a database dependency. */
4
+ export declare function assertAgentEventSourceConforms(factory: AgentEventSourceConformanceFactory): Promise<void>;
@@ -0,0 +1,54 @@
1
+ // ponytail: runner-free durable-event contract probe for database adapters.
2
+ /** Assert durable append/page/replay ownership and cursor behavior without a database dependency. */
3
+ export async function assertAgentEventSourceConforms(factory) {
4
+ const source = await factory();
5
+ const ownership = { tenantId: "tenant-a", accountId: "account-a", userId: "user-a" };
6
+ const input = { ownership, sessionId: "session-a", runId: "run-a" };
7
+ const first = event("event-a", "agent_started", input);
8
+ const second = event("event-b", "turn_started", input, "2026-01-01T00:00:01.000Z");
9
+ const storedFirst = await source.append(first);
10
+ const storedSecond = await source.append(second);
11
+ equal(storedFirst.sequence, 1, "first durable event must receive sequence 1");
12
+ equal(storedSecond.sequence, 2, "durable sequence must increase per run");
13
+ equal((await source.append(first)).sequence, 1, "identical duplicate append must be idempotent");
14
+ await rejects(() => source.append({ ...first, timestamp: "2026-01-01T00:00:02.000Z" }), "changed duplicate append must fail");
15
+ const page = await source.page({ ...input, limit: 1 });
16
+ equal(page.items.length, 1, "page limit must be honored");
17
+ equal(page.items[0].record.id, first.id, "page order must follow sequence");
18
+ if (!page.nextCursor)
19
+ throw new Error("truncated page must include a cursor");
20
+ const secondPage = await source.page({ ...input, after: page.nextCursor, limit: 1 });
21
+ equal(secondPage.items[0]?.record.id, second.id, "cursor must be exclusive");
22
+ const iterator = source.subscribe({ ...input, after: secondPage.items[0].cursor })[Symbol.asyncIterator]();
23
+ const pending = iterator.next();
24
+ const third = await source.append(event("event-c", "turn_started", input, "2026-01-01T00:00:02.000Z"));
25
+ equal((await pending).value?.record.id, third.id, "replay/live handoff dropped an event");
26
+ await iterator.return?.();
27
+ const terminal = await source.append(event("event-d", "agent_finished", input, "2026-01-01T00:00:03.000Z"));
28
+ const final = await source.page({ ...input, after: secondPage.items[0].cursor, limit: 10 });
29
+ equal(final.items.at(-1)?.record.id, terminal.id, "terminal page must include its terminal event");
30
+ equal(final.terminal, true, "terminal event must close only after prior events are delivered");
31
+ await rejects(() => source.page({ ...input, ownership: { ...ownership, tenantId: "tenant-b" }, after: page.nextCursor }), "foreign cursor must fail closed");
32
+ await rejects(() => source.append({ ...event("event-unredacted", "turn_started", input), redacted: false }), "unredacted append must fail");
33
+ await rejects(() => source.page({ ...input, limit: 0 }), "invalid page limit must fail");
34
+ }
35
+ function event(id, type, input, timestamp = "2026-01-01T00:00:00.000Z") {
36
+ const event = type === "turn_started"
37
+ ? { type, sessionId: input.sessionId, runId: input.runId, turn: 1 }
38
+ : { type, sessionId: input.sessionId, runId: input.runId };
39
+ return { id, ...input.ownership, sessionId: input.sessionId, runId: input.runId, type, timestamp, event, redacted: true };
40
+ }
41
+ function equal(actual, expected, message) {
42
+ if (actual !== expected)
43
+ throw new Error(`${message}; expected ${String(expected)}, received ${String(actual)}`);
44
+ }
45
+ async function rejects(action, message) {
46
+ try {
47
+ await action();
48
+ }
49
+ catch {
50
+ return;
51
+ }
52
+ throw new Error(message);
53
+ }
54
+ //# sourceMappingURL=agent-event-source-conformance.js.map
@@ -1,7 +1,7 @@
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 = 5;
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_tool_calls" | "prism_usage" | "prism_run_feedback" | "prism_retention_policies" | "prism_legal_holds" | "prism_tenant_quotas" | "prism_migrations";
3
+ export declare const PERSISTENCE_SCHEMA_VERSION = 7;
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 {
7
7
  readonly name: string;
@@ -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 = 5;
7
+ export const PERSISTENCE_SCHEMA_VERSION = 7;
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 = [
@@ -177,6 +177,17 @@ export function createPersistenceSchemaModel() {
177
177
  ],
178
178
  foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
179
179
  },
180
+ {
181
+ name: "prism_agent_event_streams",
182
+ primaryKey: ["session_id", "run_id"],
183
+ columns: [
184
+ { name: "session_id", type: "text" },
185
+ { name: "run_id", type: "text" },
186
+ { name: "next_sequence", type: "integer" },
187
+ { name: "updated_at", type: "timestamp" },
188
+ ],
189
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
190
+ },
180
191
  {
181
192
  name: "prism_tool_calls",
182
193
  primaryKey: ["id"],
@@ -369,6 +380,7 @@ export function createPersistenceSchemaModel() {
369
380
  name: "prism_agent_events_run_sequence_idx",
370
381
  table: "prism_agent_events",
371
382
  columns: ["run_id", "sequence"],
383
+ unique: true,
372
384
  purpose: "stable per-run event timeline pagination",
373
385
  },
374
386
  {
@@ -377,6 +389,12 @@ export function createPersistenceSchemaModel() {
377
389
  columns: ["session_id", "timestamp", "id"],
378
390
  purpose: "event stream pagination",
379
391
  },
392
+ {
393
+ name: "prism_agent_events_owner_timestamp_sequence_idx",
394
+ table: "prism_agent_events",
395
+ columns: ["tenant_id", "account_id", "user_id", "timestamp", "sequence", "id"],
396
+ purpose: "owned durable event retention cleanup",
397
+ },
380
398
  {
381
399
  name: "prism_tool_calls_session_name_started_idx",
382
400
  table: "prism_tool_calls",
@@ -463,34 +481,51 @@ function migrationStep(version, name, description) {
463
481
  const content = version === 1
464
482
  ? {
465
483
  tables: model.tables
466
- .filter((table) => table.name !== "prism_run_feedback")
484
+ .filter((table) => table.name !== "prism_run_feedback" && table.name !== "prism_agent_event_streams")
467
485
  .map((table) => table.name === "prism_usage"
468
486
  ? { ...table, columns: table.columns.filter((column) => !["scope", "turn", "attempt"].includes(column.name)) }
469
487
  : table),
470
- indexes: model.indexes.filter((index) => !index.name.startsWith("prism_usage_session_scope_") && !index.name.startsWith("prism_run_feedback_")),
488
+ indexes: model.indexes
489
+ .filter((index) => !index.name.startsWith("prism_usage_session_scope_") &&
490
+ !index.name.startsWith("prism_run_feedback_") &&
491
+ !index.name.startsWith("prism_agent_event_streams_") &&
492
+ !index.name.startsWith("prism_agent_events_owner_timestamp_"))
493
+ .map((index) => index.name === "prism_agent_events_run_sequence_idx"
494
+ ? (() => {
495
+ const { unique: _unique, ...legacy } = index;
496
+ return legacy;
497
+ })()
498
+ : index),
471
499
  }
472
- : version === 2
473
- ? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
474
- : version === 3
500
+ : version === 7
501
+ ? { indexes: ["prism_agent_events_owner_timestamp_sequence_idx"] }
502
+ : version === 6
475
503
  ? {
476
- tables: ["prism_run_feedback"],
477
- indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name),
504
+ tables: ["prism_agent_event_streams"],
505
+ indexes: ["prism_agent_events_run_sequence_idx"],
478
506
  }
479
- : version === 4
480
- ? // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
481
- { search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
482
- : version === 5
507
+ : version === 2
508
+ ? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
509
+ : version === 3
483
510
  ? {
484
- tables: ["prism_legal_holds", "prism_tenant_quotas"],
485
- indexes: [
486
- "prism_legal_holds_owner_resource_idx",
487
- "prism_legal_holds_created_id_idx",
488
- "prism_tenant_quotas_owner_kind_idx",
489
- ],
511
+ tables: ["prism_run_feedback"],
512
+ indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name),
490
513
  }
491
- : (() => {
492
- throw new Error(`Unknown migration version ${version}`);
493
- })();
514
+ : version === 4
515
+ ? // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
516
+ { search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
517
+ : version === 5
518
+ ? {
519
+ tables: ["prism_legal_holds", "prism_tenant_quotas"],
520
+ indexes: [
521
+ "prism_legal_holds_owner_resource_idx",
522
+ "prism_legal_holds_created_id_idx",
523
+ "prism_tenant_quotas_owner_kind_idx",
524
+ ],
525
+ }
526
+ : (() => {
527
+ throw new Error(`Unknown migration version ${version}`);
528
+ })();
494
529
  return {
495
530
  version,
496
531
  name,
@@ -509,6 +544,8 @@ export function createPersistenceMigrationContract() {
509
544
  migrationStep(3, "003_run_feedback", "Add immutable ownership-scoped run/trace feedback and evaluation links."),
510
545
  migrationStep(4, "004_session_search", "Add bounded session search indexes and adapter-local FTS objects."),
511
546
  migrationStep(5, "005_lifecycle_hold_quota", "Add legal-hold and tenant-quota tables for retention lifecycle."),
547
+ migrationStep(6, "006_agent_event_source", "Add transactional per-run event counters and unique durable event sequencing."),
548
+ migrationStep(7, "007_agent_event_retention_index", "Add an exact-owner durable-event retention cleanup index."),
512
549
  ],
513
550
  lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
514
551
  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,9 @@
1
+ import type { AgentIdentity, ToolEffectKey, ToolEffectStore } from "../contracts.js";
2
+ export interface ToolEffectStoreConformanceOptions {
3
+ readonly identity?: AgentIdentity;
4
+ readonly ownership?: ToolEffectKey["ownership"];
5
+ readonly key?: string;
6
+ }
7
+ /** Assert core claim/CAS, duplicate, reconciliation, and cleanup semantics without a test framework. */
8
+ export declare function assertToolEffectStoreConforms(factory: () => ToolEffectStore | Promise<ToolEffectStore>, options?: ToolEffectStoreConformanceOptions): Promise<void>;
9
+ export declare function runToolEffectStoreConformance(factory: () => ToolEffectStore | Promise<ToolEffectStore>, options?: ToolEffectStoreConformanceOptions): Promise<void>;
@@ -0,0 +1,85 @@
1
+ /** Assert core claim/CAS, duplicate, reconciliation, and cleanup semantics without a test framework. */
2
+ export async function assertToolEffectStoreConforms(factory, options = {}) {
3
+ const store = await factory();
4
+ const identity = options.identity ?? testIdentity();
5
+ const ownership = options.ownership ?? { tenantId: identity.tenantId };
6
+ const base = key(identity, ownership, options.key ?? "prism:tool-effect:v1:conformance");
7
+ const first = await store.begin(base);
8
+ if (first.outcome !== "acquired" || first.record.status !== "pending" || !first.record.claimToken) {
9
+ throw new Error("store must acquire an absent tool effect as pending with a claim token");
10
+ }
11
+ const duplicate = await store.begin(base);
12
+ if (duplicate.outcome !== "existing" || duplicate.record.status !== "pending") {
13
+ throw new Error("store must return existing pending effects without a second claim");
14
+ }
15
+ const dispatched = await store.markDispatched(transition(base, first.record));
16
+ if (dispatched.status !== "dispatched" || dispatched.version !== first.record.version + 1) {
17
+ throw new Error("store must transition a claimed pending effect to dispatched");
18
+ }
19
+ const result = { toolCallId: base.toolCallId, name: base.toolName, value: { ok: true } };
20
+ const completed = await store.complete({ ...transition(base, dispatched), result });
21
+ if (completed.status !== "completed" || completed.result?.toolCallId !== base.toolCallId) {
22
+ throw new Error("store must retain a completed bounded result");
23
+ }
24
+ const replay = await store.begin(base);
25
+ if (replay.outcome !== "existing" || replay.record.status !== "completed") {
26
+ throw new Error("store must preserve completed duplicate state");
27
+ }
28
+ await expectReject(() => store.markDispatched(transition(base, dispatched)), "store must reject stale claim/version transitions");
29
+ const unknownKey = key(identity, ownership, `${base.key}:unknown`, "call-unknown");
30
+ const unknownPending = await store.begin(unknownKey);
31
+ const unknownDispatched = await store.markDispatched(transition(unknownKey, unknownPending.record));
32
+ const unknown = await store.markUnknown({ ...transition(unknownKey, unknownDispatched), failure: { code: "test" } });
33
+ if (unknown.status !== "unknown")
34
+ throw new Error("store must mark dispatched effects unknown");
35
+ const resolved = await store.resolveUnknown({
36
+ ...unknownKey,
37
+ expectedVersion: unknown.version,
38
+ status: "failed_terminal",
39
+ failure: { code: "test" },
40
+ });
41
+ if (resolved.status !== "failed_terminal")
42
+ throw new Error("store must CAS-resolve unknown effects");
43
+ const cleanup = await store.cleanup({ ownership, before: new Date(Date.now() + 60_000).toISOString(), limit: 100 });
44
+ if (cleanup.deleted < 1)
45
+ throw new Error("store cleanup must remove terminal effects");
46
+ }
47
+ export async function runToolEffectStoreConformance(factory, options = {}) {
48
+ await assertToolEffectStoreConforms(factory, options);
49
+ }
50
+ function key(identity, ownership, value, toolCallId = "call") {
51
+ return {
52
+ identity,
53
+ ownership,
54
+ key: value,
55
+ sessionId: "session",
56
+ runId: "run",
57
+ toolCallId,
58
+ toolName: "effect.tool",
59
+ argumentsHash: "a".repeat(64),
60
+ };
61
+ }
62
+ function transition(base, record) {
63
+ if (!record.claimToken)
64
+ throw new Error("store transition record lacks claim token");
65
+ return { ...base, claimToken: record.claimToken, expectedVersion: record.version };
66
+ }
67
+ async function expectReject(run, message) {
68
+ try {
69
+ await run();
70
+ }
71
+ catch {
72
+ return;
73
+ }
74
+ throw new Error(message);
75
+ }
76
+ function testIdentity() {
77
+ return {
78
+ tenantId: "tenant",
79
+ principal: { kind: "service", id: "conformance" },
80
+ scopes: ["tools:execute"],
81
+ issuedAt: "2026-01-01T00:00:00.000Z",
82
+ verified: true,
83
+ };
84
+ }
85
+ //# sourceMappingURL=tool-effect-store-conformance.js.map
@@ -0,0 +1,15 @@
1
+ import type { JsonObject, ToolEffectKey, ToolEffectStore } from "./contracts.js";
2
+ export type ToolEffectErrorCode = "ERR_PRISM_TOOL_EFFECT_REQUIRED" | "ERR_PRISM_TOOL_EFFECT_CONFLICT" | "ERR_PRISM_TOOL_EFFECT_UNKNOWN" | "ERR_PRISM_TOOL_EFFECT_COMPLETED" | "ERR_PRISM_TOOL_EFFECT_LIMIT";
3
+ export declare class ToolEffectError extends Error {
4
+ readonly code: ToolEffectErrorCode;
5
+ constructor(code: ToolEffectErrorCode, message: string);
6
+ }
7
+ /** Stable JSON representation for an already-validated tool arguments object. */
8
+ export declare function canonicalToolEffectJson(value: unknown): string;
9
+ export declare function toolEffectArgumentsHash(argumentsValue: JsonObject): string;
10
+ /** Derives the only core-authoritative key. Callers never supply this from model input. */
11
+ export declare function deriveToolEffectKey(input: Omit<ToolEffectKey, "key" | "signal">): string;
12
+ /** In-process reference. Use a durable adapter for cross-replica claims. */
13
+ export declare function createMemoryToolEffectStore(options?: {
14
+ readonly now?: () => number;
15
+ }): ToolEffectStore;