@arnilo/prism 0.0.13 → 0.0.15

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 (52) hide show
  1. package/CHANGELOG.md +23 -2
  2. package/README.md +9 -2
  3. package/dist/agent-loops.d.ts +4 -0
  4. package/dist/agent-loops.js +16 -3
  5. package/dist/artifacts.d.ts +78 -0
  6. package/dist/artifacts.js +24 -0
  7. package/dist/contracts.d.ts +86 -0
  8. package/dist/contracts.js +8 -0
  9. package/dist/conversations.d.ts +50 -0
  10. package/dist/conversations.js +97 -0
  11. package/dist/credentials.d.ts +14 -0
  12. package/dist/credentials.js +9 -0
  13. package/dist/devices.d.ts +94 -0
  14. package/dist/devices.js +138 -0
  15. package/dist/index.d.ts +12 -6
  16. package/dist/index.js +7 -4
  17. package/dist/provider-events.d.ts +1 -0
  18. package/dist/provider-events.js +3 -0
  19. package/dist/providers/openai-primitives.js +5 -2
  20. package/docs/ag-ui.md +5 -0
  21. package/docs/browser-automation.md +3 -0
  22. package/docs/conversations.md +135 -0
  23. package/docs/credential-storage.md +28 -1
  24. package/docs/credentials-and-redaction.md +2 -0
  25. package/docs/database-persistence.md +5 -1
  26. package/docs/device-adapters.md +97 -0
  27. package/docs/host-security.md +7 -2
  28. package/docs/index.md +24 -19
  29. package/docs/migration.md +50 -1
  30. package/docs/multimodal-content.md +8 -5
  31. package/docs/performance.md +36 -0
  32. package/docs/policy-and-audit.md +1 -0
  33. package/docs/provider-caching.md +12 -0
  34. package/docs/provider-conformance.md +29 -5
  35. package/docs/provider-packages.md +26 -2
  36. package/docs/providers/ai-sdk.md +23 -7
  37. package/docs/providers/alibaba.md +179 -0
  38. package/docs/providers/ollama.md +166 -0
  39. package/docs/providers/openai.md +22 -3
  40. package/docs/rag.md +41 -12
  41. package/docs/release-and-install.md +135 -17
  42. package/docs/resource-loading.md +3 -0
  43. package/docs/review-coverage-2026-07-25-phase-9.md +256 -0
  44. package/docs/review-coverage-2026-07-26-phase-10.md +132 -0
  45. package/docs/server.md +4 -0
  46. package/docs/work-artifacts-and-review.md +100 -0
  47. package/docs/work-connectors.md +5 -1
  48. package/docs/work-tools.md +3 -0
  49. package/docs/workflows.md +4 -0
  50. package/docs/working-and-semantic-memory.md +40 -7
  51. package/package.json +1 -1
  52. package/templates/init/providers.json +22 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.15] - 2026-07-26
4
+
5
+ ### Added
6
+
7
+ - Phase 10 provider, memory, and RAG parity: OpenAI hosted-tool attribution, bounded Responses continuation and Realtime seam; exact AI SDK V4 mapping; bounded RAG source lifecycle, document adapters, reranking, citation provenance, content trust, and ingestion status; memory export/rebuild with production-store conformance.
8
+
9
+ ### Changed
10
+
11
+ - Versioned all **43** publishable manifests, exact internal ranges, and lockfile entries to `0.0.15`; no package was added.
12
+ - Added network-free Phase 10 evidence: `scripts/benchmark-0.0.15.mjs`.
13
+
14
+ ## [0.0.14] - 2026-07-26
15
+
16
+ ### Added
17
+
18
+ - Phase 9 personal/work-agent surfaces: durable conversation service (`createConversationService`), durable artifact service with review/approval/authorized delivery (`createArtifactService`), memory consent + lifecycle (`setConsent`/`correct`/`forget`/`applyRetention`), AG-UI co-work events (`mapCoWork` + ACP parity), scoped M365/GWS OAuth connectors (`revokeOAuthCredential`, `createOAuthWorkTokenProvider`), a browser verified-state checkpoint ledger, and a deny-by-default device adapter contract (`resolveDevicePolicy`/`assertDeviceAdmit`).
19
+ - New optional provider packages `@arnilo/prism-provider-alibaba` (Model Studio / DashScope + Coding Plan) and `@arnilo/prism-provider-ollama` (cloud/local), both with dynamic model discovery; enrolled via `@arnilo/prism-providers`.
20
+
21
+ ### Changed
22
+
23
+ - Versioned all **43** first-party manifests and exact internal ranges to `0.0.14` (41 → 43; only the two provider packages are new).
24
+ - Network-free Phase 9 evidence: `scripts/benchmark-0.0.14.mjs`.
25
+
3
26
  ## [0.0.13] - 2026-07-24
4
27
 
5
28
  ### Added
@@ -16,8 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
39
 
17
40
  All notable changes to this project will be documented in this file.
18
41
 
19
- ## [Unreleased]
20
-
21
42
  ## [0.0.12] - 2026-07-22
22
43
 
23
44
  ### Added
package/README.md CHANGED
@@ -17,7 +17,7 @@ packages. Prism defines contracts, not apps.
17
17
  OpenAI/OpenRouter use best-effort explicit cache hints, NeuralWatt uses
18
18
  best-effort implicit prefix caching, and other providers have route/model-specific
19
19
  or no cache-control support; see [docs/provider-caching.md](docs/provider-caching.md).
20
- - **First-party packages**: six provider adapters, two compaction strategies,
20
+ - **First-party packages**: fourteen provider adapters, two compaction strategies,
21
21
  coding tools/security, JSON Schema validation, MCP, workflows, OpenTelemetry,
22
22
  encrypted credentials, SQLite/PostgreSQL persistence, and manifest-only install profiles.
23
23
  - **Tools, context, skills**: host-owned tool registry with allow/deny filtering
@@ -34,6 +34,11 @@ packages. Prism defines contracts, not apps.
34
34
  - **Config, settings, security**: layered config merge, settings providers,
35
35
  credential resolvers, trust/permission policies, and secret redaction.
36
36
  - **CLI/RPC/server**: `prism --mode print|json|rpc`, `prism init`, optional framework-free authorized Web agent/workflow routes, and explicit MCP server exposure.
37
+ - **Ecosystem parity (0.0.15)**: OpenAI hosted-tool attribution, bounded Responses
38
+ continuation/Realtime, exact AI SDK V4 mapping, bounded RAG lifecycle/reranking/trust,
39
+ and consent-bound memory export/rebuild; provider, RAG, and memory packages remain optional.
40
+ - **Co-work contracts (0.0.14)**: conversation/artifact review types, deny-by-default device
41
+ contracts, and OAuth refresh/revoke helpers; services stay in optional packages.
37
42
 
38
43
  ## Install
39
44
 
@@ -150,6 +155,8 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
150
155
  | `@arnilo/prism-provider-zai` | ZAI GLM provider |
151
156
  | `@arnilo/prism-provider-kimi` | Kimi For Coding provider |
152
157
  | `@arnilo/prism-provider-neuralwatt` | NeuralWatt provider with implicit vLLM prefix caching |
158
+ | `@arnilo/prism-provider-alibaba` | Alibaba Cloud (Model Studio / DashScope + Coding Plan) provider with dynamic discovery and explicit/implicit caching |
159
+ | `@arnilo/prism-provider-ollama` | Ollama Cloud / local provider with dynamic discovery and implicit-only caching |
153
160
  | `@arnilo/prism-compaction-llm` | provider-backed compaction strategy |
154
161
  | `@arnilo/prism-compaction-observational-memory` | source-backed memory + recall tool |
155
162
  | `@arnilo/prism-coding-agent` | bounded shell/read/write/edit tools |
@@ -163,7 +170,7 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
163
170
  | `@arnilo/prism-credentials-node` | encrypted-file and keychain credentials |
164
171
  | `@arnilo/prism-session-store-sqlite` | SQLite persistence/checkpoints/leases/owned run feedback |
165
172
  | `@arnilo/prism-session-store-postgres` | PostgreSQL persistence/checkpoints/leases/owned run feedback |
166
- | `@arnilo/prism-providers` | family: all 7 provider adapters, including AI SDK interoperability |
173
+ | `@arnilo/prism-providers` | family: all 11 provider adapters, including AI SDK interoperability |
167
174
  | `@arnilo/prism-compaction` | family: both compaction strategies |
168
175
  | `@arnilo/prism-base` | profile: core + compaction + JSON Schema validation |
169
176
  | `@arnilo/prism-code` | profile: base + coding tools/security + MCP |
@@ -14,6 +14,10 @@ export declare function resolveToolConcurrency(options: {
14
14
  }, config: {
15
15
  loop?: AgentLoopStrategy | AgentLoopOptions;
16
16
  }): number;
17
+ /** Calls the host must dispatch. Provider-hosted calls (`authority: "provider-hosted"`)
18
+ * were already executed server-side; the assistant response text carries their effect, so
19
+ * the host neither dispatches them nor appends a `tool_result`. */
20
+ export declare function dispatchableToolCalls(calls: readonly ToolCallContent[]): readonly ToolCallContent[];
17
21
  /** Dispatch tool calls with bounded concurrency; append transcript rows in call order. */
18
22
  export declare function dispatchToolCallsInOrder(calls: readonly ToolCallContent[], ctx: LoopContext): Promise<void>;
19
23
  export declare function resolveLoop(options: {
@@ -39,7 +39,8 @@ export const singleShotLoop = {
39
39
  ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
40
40
  }
41
41
  ctx.emit({ type: "turn_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn });
42
- if (calls.length === 0 || toolRounds >= ctx.maxToolRounds) {
42
+ const dispatchable = dispatchableToolCalls(calls);
43
+ if (dispatchable.length === 0 || toolRounds >= ctx.maxToolRounds) {
43
44
  // Soft-interrupt / late steer: keep same run going when queue still has text.
44
45
  if (await ctx.applyPendingSteers?.()) {
45
46
  nextInput = [];
@@ -48,7 +49,7 @@ export const singleShotLoop = {
48
49
  break;
49
50
  }
50
51
  toolRounds += 1;
51
- await dispatchToolCallsInOrder(calls, ctx);
52
+ await dispatchToolCallsInOrder(dispatchable, ctx);
52
53
  nextInput = [];
53
54
  }
54
55
  return usage;
@@ -113,13 +114,19 @@ export function generateValidateReviseLoop(opts) {
113
114
  }
114
115
  ctx.emit({ type: "turn_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn });
115
116
  if (opts.toolCalls === "bounded" && calls.length > 0) {
117
+ const dispatchable = dispatchableToolCalls(calls);
118
+ if (dispatchable.length === 0) {
119
+ // Only provider-hosted calls; no host tool to run. Continue without charging a round.
120
+ nextInput = [];
121
+ continue;
122
+ }
116
123
  if (toolRounds >= ctx.maxToolRounds) {
117
124
  const result = { ok: false, errors: [{ message: "maximum tool rounds exceeded" }], metadata: { reason: "tool_round_limit" } };
118
125
  ctx.emit({ type: "artifact_failed", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt: attempts + 1, result });
119
126
  return usage;
120
127
  }
121
128
  toolRounds += 1;
122
- await dispatchToolCallsInOrder(calls, { ...ctx, toolConcurrency: 1 });
129
+ await dispatchToolCallsInOrder(dispatchable, { ...ctx, toolConcurrency: 1 });
123
130
  nextInput = [];
124
131
  continue;
125
132
  }
@@ -195,6 +202,12 @@ export function resolveToolConcurrency(options, config) {
195
202
  return 1;
196
203
  return Math.floor(value);
197
204
  }
205
+ /** Calls the host must dispatch. Provider-hosted calls (`authority: "provider-hosted"`)
206
+ * were already executed server-side; the assistant response text carries their effect, so
207
+ * the host neither dispatches them nor appends a `tool_result`. */
208
+ export function dispatchableToolCalls(calls) {
209
+ return calls.filter((call) => call.authority !== "provider-hosted");
210
+ }
198
211
  /** Dispatch tool calls with bounded concurrency; append transcript rows in call order. */
199
212
  export async function dispatchToolCallsInOrder(calls, ctx) {
200
213
  if (calls.length === 0)
@@ -0,0 +1,78 @@
1
+ import type { OwnershipScope } from "./contracts.js";
2
+ /**
3
+ * Durable artifact co-work review types (Phase 9 / 0.0.14). Core exports types only;
4
+ * the service + delivery-link signer live in `@arnilo/prism-server`. Prism persists bounded
5
+ * metadata, revisions, approvals, and delivery references — never file bodies (hosts own blobs).
6
+ */
7
+ /** Review state of an artifact's latest revision. */
8
+ export type ArtifactApprovalState = "pending" | "approved" | "rejected";
9
+ /** A resolved decision on one revision (pending is the absence of a decision). */
10
+ export type ArtifactDecisionState = Exclude<ArtifactApprovalState, "pending">;
11
+ /** Bounded citation / data-source reference. Host resolves the body; Prism stores the ref only. */
12
+ export interface ArtifactCitation {
13
+ readonly uri: string;
14
+ readonly title?: string;
15
+ /** Data-source kind (e.g. "web", "database", "upload"); host-defined, bounded. */
16
+ readonly kind?: string;
17
+ }
18
+ /** One immutable revision of an artifact. `uri`/`hash` reference host-owned content. */
19
+ export interface ArtifactRevision {
20
+ /** 1-based, monotonic within the artifact. */
21
+ readonly version: number;
22
+ /** Host-owned blob reference (redacted; never a local filesystem path). */
23
+ readonly uri: string;
24
+ readonly mime: string;
25
+ /** Host-computed content hash for integrity compare. */
26
+ readonly hash: string;
27
+ readonly changeNote?: string;
28
+ /** Run that produced this revision, if any. */
29
+ readonly producerRunId?: string;
30
+ readonly citations?: readonly ArtifactCitation[];
31
+ /** Preview metadata only; the host renders content. */
32
+ readonly preview?: Readonly<Record<string, unknown>>;
33
+ readonly createdAt: string;
34
+ }
35
+ /** A reviewer decision on a specific revision. */
36
+ export interface ArtifactApproval {
37
+ readonly version: number;
38
+ readonly state: ArtifactDecisionState;
39
+ /** Redacted reviewer actor reference. */
40
+ readonly reviewer: string;
41
+ /** Change-request / rejection note. */
42
+ readonly note?: string;
43
+ readonly decidedAt: string;
44
+ }
45
+ /**
46
+ * Durable artifact record. Stored as a versioned checkpoint value; the checkpoint version
47
+ * is the CAS counter for concurrent reviewers, distinct from revision numbers.
48
+ */
49
+ export interface ArtifactRecord extends OwnershipScope {
50
+ readonly id: string;
51
+ readonly threadId: string;
52
+ readonly title?: string;
53
+ readonly revisions: readonly ArtifactRevision[];
54
+ readonly approvals: readonly ArtifactApproval[];
55
+ /** Last approved revision; remains recoverable after a later rejection. */
56
+ readonly lastValidatedVersion?: number;
57
+ readonly createdAt: string;
58
+ readonly updatedAt: string;
59
+ }
60
+ /** Signed, expiring delivery authorization. Reauthorized per download; never a bearer secret. */
61
+ export interface ArtifactDeliveryToken extends OwnershipScope {
62
+ readonly artifactId: string;
63
+ readonly threadId: string;
64
+ readonly version: number;
65
+ readonly issuedAt: string;
66
+ readonly expiresAt: string;
67
+ }
68
+ /** Well-known checkpoint namespace for artifact records. */
69
+ export declare const ARTIFACT_CHECKPOINT_NAMESPACE = "prism.artifact";
70
+ export declare class ArtifactError extends Error {
71
+ readonly reason: string;
72
+ readonly code = "ERR_PRISM_ARTIFACT";
73
+ constructor(message: string, reason: string);
74
+ }
75
+ /** Checkpoint key for an artifact: thread-scoped so per-thread listing uses a key prefix. */
76
+ export declare function artifactCheckpointKey(threadId: string, artifactId: string): string;
77
+ /** Current review state: the decision on the latest revision, or pending when undecided. */
78
+ export declare function artifactApprovalState(record: ArtifactRecord): ArtifactApprovalState;
@@ -0,0 +1,24 @@
1
+ /** Well-known checkpoint namespace for artifact records. */
2
+ export const ARTIFACT_CHECKPOINT_NAMESPACE = "prism.artifact";
3
+ export class ArtifactError extends Error {
4
+ reason;
5
+ code = "ERR_PRISM_ARTIFACT";
6
+ constructor(message, reason) {
7
+ super(message);
8
+ this.reason = reason;
9
+ this.name = "ArtifactError";
10
+ }
11
+ }
12
+ /** Checkpoint key for an artifact: thread-scoped so per-thread listing uses a key prefix. */
13
+ export function artifactCheckpointKey(threadId, artifactId) {
14
+ return `${threadId}:${artifactId}`;
15
+ }
16
+ /** Current review state: the decision on the latest revision, or pending when undecided. */
17
+ export function artifactApprovalState(record) {
18
+ const latest = record.revisions[record.revisions.length - 1];
19
+ if (latest === undefined)
20
+ return "pending";
21
+ const decision = record.approvals.find((approval) => approval.version === latest.version);
22
+ return decision?.state ?? "pending";
23
+ }
24
+ //# sourceMappingURL=artifacts.js.map
@@ -43,7 +43,11 @@ export interface ToolCallDeltaContent {
43
43
  readonly id?: string;
44
44
  readonly name?: string;
45
45
  readonly argumentsText?: string;
46
+ /** Who executes the call. `"provider-hosted"` = the provider runs it server-side;
47
+ * the host must NOT dispatch it or send a `tool_result`. Defaults to `"host"`. */
48
+ readonly authority?: ToolCallAuthority;
46
49
  }
50
+ export type ToolCallAuthority = "host" | "provider-hosted";
47
51
  export interface ToolCallContent {
48
52
  readonly type: "tool_call";
49
53
  readonly id: string;
@@ -51,6 +55,10 @@ export interface ToolCallContent {
51
55
  readonly arguments: JsonObject;
52
56
  /** Set when streamed arguments failed JSON parse; dispatch blocks without execute(). */
53
57
  readonly argumentsError?: ErrorInfo;
58
+ /** Who executes the call. `"provider-hosted"` = the provider already ran it
59
+ * server-side; the host must NOT dispatch it or append a `tool_result`. The
60
+ * assistant response text already incorporates the call's effect. */
61
+ readonly authority?: ToolCallAuthority;
54
62
  }
55
63
  export interface ToolResultContent {
56
64
  readonly type: "tool_result";
@@ -229,6 +237,11 @@ export interface ProviderRequestOptions {
229
237
  readonly extra?: JsonObject;
230
238
  /** Provider-neutral JSON-schema structured output request. Requires model `capabilities.structuredOutput`. */
231
239
  readonly structuredOutput?: StructuredOutputOptions;
240
+ /** Opaque provider continuation cursor (e.g. OpenAI `previous_response_id`). When set,
241
+ * the provider resumes from this cursor instead of re-sending full history. */
242
+ readonly continuation?: {
243
+ readonly cursor: string;
244
+ };
232
245
  }
233
246
  export interface ProviderRequest {
234
247
  readonly model: ModelConfig;
@@ -251,12 +264,17 @@ export type ProviderEvent = {
251
264
  readonly id?: string;
252
265
  readonly name?: string;
253
266
  readonly argumentsText?: string;
267
+ readonly authority?: ToolCallAuthority;
254
268
  } | {
255
269
  readonly type: "tool_call";
256
270
  readonly call: ToolCallContent;
257
271
  } | {
258
272
  readonly type: "usage";
259
273
  readonly usage: Usage;
274
+ } | {
275
+ readonly type: "continuation_required";
276
+ readonly cursor: string;
277
+ readonly reason?: string;
260
278
  } | {
261
279
  readonly type: "done";
262
280
  readonly usage?: Usage;
@@ -269,6 +287,64 @@ export interface AIProvider {
269
287
  generate(request: ProviderRequest): AsyncIterable<ProviderEvent>;
270
288
  }
271
289
  export type ProviderResolver = (model: ModelConfig) => AIProvider | undefined;
290
+ /** Realtime audio/session event. Realtime is a bidirectional session, not a request/response
291
+ * stream, so it is a separate neutral seam from `AIProvider.generate()`. Credentials are
292
+ * bound to the session handshake only and never appear in events. */
293
+ export type RealtimeEvent = {
294
+ readonly type: "session_started";
295
+ readonly sessionId?: string;
296
+ } | {
297
+ readonly type: "audio_delta";
298
+ readonly audio: Uint8Array;
299
+ } | {
300
+ readonly type: "transcript_delta";
301
+ readonly text: string;
302
+ readonly role: "user" | "assistant";
303
+ } | {
304
+ readonly type: "tool_call";
305
+ readonly call: ToolCallContent;
306
+ } | {
307
+ readonly type: "interrupted";
308
+ } | {
309
+ readonly type: "session_closed";
310
+ readonly reason?: string;
311
+ } | {
312
+ readonly type: "error";
313
+ readonly error: ErrorInfo;
314
+ };
315
+ /** Neutral bidirectional realtime session seam. The provider owns the transport
316
+ * (e.g. WebSocket); the host owns audio capture/playback and session lifecycle. */
317
+ export interface RealtimeSession {
318
+ readonly id: string;
319
+ readonly provider: string;
320
+ /** Send an audio chunk (PCM/Opus; provider-specific format set at creation). */
321
+ sendAudio(chunk: Uint8Array, options?: {
322
+ readonly signal?: AbortSignal;
323
+ }): Promise<void>;
324
+ /** Inbound events (audio out, transcripts, hosted tool calls, interruption, close, error). */
325
+ events(): AsyncIterable<RealtimeEvent>;
326
+ /** Request the provider stop the current response mid-stream. */
327
+ interrupt(options?: {
328
+ readonly signal?: AbortSignal;
329
+ }): Promise<void>;
330
+ /** Close the session and release the transport. Idempotent. */
331
+ close(reason?: string, options?: {
332
+ readonly signal?: AbortSignal;
333
+ }): Promise<void>;
334
+ }
335
+ /** Factory a provider exposes for realtime sessions; not part of `AIProvider`. */
336
+ export type RealtimeSessionFactory = (options: RealtimeSessionOptions) => RealtimeSession;
337
+ export interface RealtimeSessionOptions {
338
+ readonly model: ModelConfig;
339
+ readonly signal?: AbortSignal;
340
+ /** Provider-specific caps override; providers enforce finite defaults. */
341
+ readonly caps?: RealtimeCaps;
342
+ }
343
+ export interface RealtimeCaps {
344
+ readonly maxAudioEventsPerSecond?: number;
345
+ readonly maxBytesPerSecond?: number;
346
+ readonly maxWallMs?: number;
347
+ }
272
348
  export type InputAssemblyLayout = "legacy" | "cache_aware";
273
349
  export interface RunOptions {
274
350
  readonly signal?: AbortSignal;
@@ -968,6 +1044,8 @@ export interface OAuthProvider {
968
1044
  readonly id: string;
969
1045
  login(callbacks?: OAuthLoginCallbacks): Promise<OAuthCredentials> | OAuthCredentials;
970
1046
  refresh?(credentials: OAuthCredentials): Promise<OAuthCredentials> | OAuthCredentials;
1047
+ /** Best-effort upstream revocation; the store delete is what fails closed locally. */
1048
+ revoke?(credentials: OAuthCredentials): Promise<void> | void;
971
1049
  getCredential?(credentials: OAuthCredentials): Promise<Credential | undefined> | Credential | undefined;
972
1050
  readonly metadata?: Readonly<Record<string, unknown>>;
973
1051
  }
@@ -1493,16 +1571,21 @@ export interface MigrationRecord {
1493
1571
  }
1494
1572
  /** Query for sessions. */
1495
1573
  export interface SessionQuery extends PersistenceQuery, OwnershipScope {
1574
+ readonly id?: string;
1496
1575
  readonly parentSessionId?: string;
1497
1576
  readonly agentDefinitionId?: string;
1498
1577
  readonly agentDefinitionVersion?: string;
1499
1578
  readonly retentionPolicyId?: string;
1579
+ /** Match sessions whose `metadata` object contains this top-level key (e.g. conversation marker). */
1580
+ readonly metadataKey?: string;
1500
1581
  readonly fromCreatedAt?: string;
1501
1582
  readonly toCreatedAt?: string;
1502
1583
  readonly fromUpdatedAt?: string;
1503
1584
  readonly toUpdatedAt?: string;
1504
1585
  readonly hasExpired?: boolean;
1505
1586
  }
1587
+ /** Validate a top-level `SessionRecord.metadata` key used by `SessionQuery.metadataKey` filters. */
1588
+ export declare function assertSessionMetadataKey(key: string): string;
1506
1589
  /** Query for session entries. */
1507
1590
  export interface SessionEntryQuery extends PersistenceQuery, OwnershipScope {
1508
1591
  readonly sessionId?: string;
@@ -1613,6 +1696,9 @@ export interface ProductionPersistenceStore {
1613
1696
  queryAgentDefinitions(query: AgentDefinitionQuery): Promise<PersistencePage<AgentDefinitionRecord>>;
1614
1697
  queryRetentionPolicies(query: RetentionPolicyQuery): Promise<PersistencePage<RetentionPolicy>>;
1615
1698
  queryMigrations(query: MigrationQuery): Promise<PersistencePage<MigrationRecord>>;
1699
+ /** Optional session-record write capability (conversation threads, host-managed sessions).
1700
+ * Upserts by id; ownership columns are set on create, `metadata`/`updatedAt` on update. */
1701
+ appendSession?(record: SessionRecord): Promise<void>;
1616
1702
  /** DB-friendly branch read (mirrors `SessionStore.readBranchPath`): one ancestor-chain
1617
1703
  * query instead of `queryEntries({ sessionId })` + in-memory walk. Optional. */
1618
1704
  readBranchPath?(query: SessionBranchRead): Promise<PersistencePage<SessionEntry>>;
package/dist/contracts.js CHANGED
@@ -128,4 +128,12 @@ export class SessionAppendConflictError extends Error {
128
128
  export function isSessionAppendConflict(error) {
129
129
  return error instanceof Error && error.code === SESSION_APPEND_CONFLICT_CODE;
130
130
  }
131
+ const SESSION_METADATA_KEY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
132
+ /** Validate a top-level `SessionRecord.metadata` key used by `SessionQuery.metadataKey` filters. */
133
+ export function assertSessionMetadataKey(key) {
134
+ if (typeof key !== "string" || !SESSION_METADATA_KEY_PATTERN.test(key)) {
135
+ throw new RangeError("metadataKey must match /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/");
136
+ }
137
+ return key;
138
+ }
131
139
  //# sourceMappingURL=contracts.js.map
@@ -0,0 +1,50 @@
1
+ import type { OwnershipScope, SessionRecord } from "./contracts.js";
2
+ /** Conversation thread lifecycle state. Archive is soft; deletion goes through persistence lifecycle. */
3
+ export type ConversationThreadState = "active" | "archived";
4
+ /** Well-known `SessionRecord.metadata` key marking conversation threads. */
5
+ export declare const CONVERSATION_METADATA_KEY = "prismConversation";
6
+ export declare const DEFAULT_MAX_CONVERSATION_CURSOR_BYTES: number;
7
+ export declare const HARD_MAX_CONVERSATION_CURSOR_BYTES: number;
8
+ export interface ConversationBranchRef {
9
+ readonly leafId: string;
10
+ readonly createdAt: string;
11
+ }
12
+ /**
13
+ * Durable user-scoped conversation thread. A thread is an ownership-scoped session
14
+ * branch plus metadata; content lives in session entries and the redacted event ledger.
15
+ */
16
+ export interface ConversationThread extends OwnershipScope {
17
+ readonly id: string;
18
+ readonly title?: string;
19
+ readonly state: ConversationThreadState;
20
+ readonly createdAt: string;
21
+ readonly updatedAt: string;
22
+ /** Branch leaves recorded by the conversation service; the entry tree remains the content source of truth. */
23
+ readonly branches: readonly ConversationBranchRef[];
24
+ /** Host-supplied create metadata; never credentials or raw transcripts. */
25
+ readonly metadata?: Readonly<Record<string, unknown>>;
26
+ }
27
+ /** Opaque thread-bound replay cursor; prevents replaying a cursor minted for another thread. */
28
+ export interface ConversationReplayCursor {
29
+ readonly v: 1;
30
+ readonly threadId: string;
31
+ /** Opaque store keyset cursor; undefined means the start of the thread. */
32
+ readonly cursor?: string;
33
+ }
34
+ export declare class ConversationError extends Error {
35
+ readonly reason: string;
36
+ readonly code = "ERR_PRISM_CONVERSATION";
37
+ constructor(message: string, reason: string);
38
+ }
39
+ export declare function encodeConversationReplayCursor(cursor: ConversationReplayCursor): string;
40
+ export declare function decodeConversationReplayCursor(encoded: string, expectedThreadId: string, maxBytes?: number): ConversationReplayCursor;
41
+ /** Project a persisted session record into a conversation thread. Undefined for non-conversation sessions. */
42
+ export declare function conversationThreadFromRecord(record: SessionRecord): ConversationThread | undefined;
43
+ /** Serialize conversation marker metadata for `SessionRecord.metadata`. */
44
+ export declare function conversationMarkerMetadata(marker: {
45
+ readonly title?: string;
46
+ readonly state: ConversationThreadState;
47
+ readonly branches?: readonly ConversationBranchRef[];
48
+ readonly requestId?: string;
49
+ readonly metadata?: Readonly<Record<string, unknown>>;
50
+ }): Readonly<Record<string, unknown>>;
@@ -0,0 +1,97 @@
1
+ /** Well-known `SessionRecord.metadata` key marking conversation threads. */
2
+ export const CONVERSATION_METADATA_KEY = "prismConversation";
3
+ export const DEFAULT_MAX_CONVERSATION_CURSOR_BYTES = 4 * 1024;
4
+ export const HARD_MAX_CONVERSATION_CURSOR_BYTES = 16 * 1024;
5
+ export class ConversationError extends Error {
6
+ reason;
7
+ code = "ERR_PRISM_CONVERSATION";
8
+ constructor(message, reason) {
9
+ super(message);
10
+ this.reason = reason;
11
+ this.name = "ConversationError";
12
+ }
13
+ }
14
+ export function encodeConversationReplayCursor(cursor) {
15
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
16
+ }
17
+ export function decodeConversationReplayCursor(encoded, expectedThreadId, maxBytes = DEFAULT_MAX_CONVERSATION_CURSOR_BYTES) {
18
+ if (typeof encoded !== "string" || encoded.length === 0) {
19
+ throw new ConversationError("Replay cursor is required", "invalid_cursor");
20
+ }
21
+ if (Buffer.byteLength(encoded, "utf8") > maxBytes) {
22
+ throw new ConversationError("Replay cursor exceeds byte limit", "cursor_too_large");
23
+ }
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
27
+ }
28
+ catch {
29
+ throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
30
+ }
31
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
32
+ throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
33
+ }
34
+ const cursor = parsed;
35
+ if (cursor.v !== 1 || typeof cursor.threadId !== "string" || cursor.threadId.length === 0) {
36
+ throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
37
+ }
38
+ if (cursor.threadId !== expectedThreadId) {
39
+ throw new ConversationError("Replay cursor belongs to another thread", "cursor_thread_mismatch");
40
+ }
41
+ if (cursor.cursor !== undefined && (typeof cursor.cursor !== "string" || cursor.cursor.length === 0)) {
42
+ throw new ConversationError("Replay cursor is invalid", "invalid_cursor");
43
+ }
44
+ return {
45
+ v: 1,
46
+ threadId: cursor.threadId,
47
+ ...(cursor.cursor === undefined ? {} : { cursor: cursor.cursor }),
48
+ };
49
+ }
50
+ /** Project a persisted session record into a conversation thread. Undefined for non-conversation sessions. */
51
+ export function conversationThreadFromRecord(record) {
52
+ const marker = record.metadata?.[CONVERSATION_METADATA_KEY];
53
+ if (!marker || typeof marker !== "object" || Array.isArray(marker))
54
+ return undefined;
55
+ const value = marker;
56
+ const branches = [];
57
+ if (Array.isArray(value.branches)) {
58
+ for (const item of value.branches) {
59
+ if (item && typeof item === "object" &&
60
+ typeof item.leafId === "string" &&
61
+ typeof item.createdAt === "string") {
62
+ branches.push({
63
+ leafId: item.leafId,
64
+ createdAt: item.createdAt,
65
+ });
66
+ }
67
+ }
68
+ }
69
+ const metadata = value.metadata && typeof value.metadata === "object" && !Array.isArray(value.metadata)
70
+ ? value.metadata
71
+ : undefined;
72
+ return {
73
+ id: record.id,
74
+ ...(record.tenantId !== undefined ? { tenantId: record.tenantId } : {}),
75
+ ...(record.accountId !== undefined ? { accountId: record.accountId } : {}),
76
+ ...(record.userId !== undefined ? { userId: record.userId } : {}),
77
+ ...(typeof value.title === "string" && value.title.length > 0 ? { title: value.title } : {}),
78
+ state: value.state === "archived" ? "archived" : "active",
79
+ createdAt: record.createdAt,
80
+ updatedAt: record.updatedAt,
81
+ branches: Object.freeze(branches),
82
+ ...(metadata === undefined ? {} : { metadata }),
83
+ };
84
+ }
85
+ /** Serialize conversation marker metadata for `SessionRecord.metadata`. */
86
+ export function conversationMarkerMetadata(marker) {
87
+ return {
88
+ [CONVERSATION_METADATA_KEY]: {
89
+ ...(marker.title === undefined ? {} : { title: marker.title }),
90
+ state: marker.state,
91
+ ...(marker.branches === undefined || marker.branches.length === 0 ? {} : { branches: marker.branches }),
92
+ ...(marker.requestId === undefined ? {} : { requestId: marker.requestId }),
93
+ ...(marker.metadata === undefined ? {} : { metadata: marker.metadata }),
94
+ },
95
+ };
96
+ }
97
+ //# sourceMappingURL=conversations.js.map
@@ -19,4 +19,18 @@ export declare function refreshOAuthCredential(options: {
19
19
  readonly credentials: OAuthCredentials;
20
20
  readonly store?: OAuthCredentialStore;
21
21
  }): Promise<OAuthCredentials>;
22
+ /** A credential store that can also remove entries (revocation fails closed on the delete). */
23
+ export interface RevocableOAuthCredentialStore extends OAuthCredentialStore {
24
+ delete(provider: string, accountId?: string): Promise<boolean> | boolean;
25
+ }
26
+ /**
27
+ * Revokes an OAuth credential: best-effort upstream revocation, then a mandatory local
28
+ * store delete so subsequent connector calls fail closed. The local delete is the trust
29
+ * boundary — an upstream revoke failure never leaves a stored token usable.
30
+ */
31
+ export declare function revokeOAuthCredential(options: {
32
+ readonly provider: OAuthProvider;
33
+ readonly credentials: OAuthCredentials;
34
+ readonly store?: RevocableOAuthCredentialStore;
35
+ }): Promise<void>;
22
36
  export declare function resolveCredentialValue(source: CredentialValueSource | undefined, request: CredentialRequest): Promise<string | undefined>;
@@ -48,6 +48,15 @@ export async function refreshOAuthCredential(options) {
48
48
  await options.store?.set(options.provider.id, refreshed);
49
49
  return refreshed;
50
50
  }
51
+ /**
52
+ * Revokes an OAuth credential: best-effort upstream revocation, then a mandatory local
53
+ * store delete so subsequent connector calls fail closed. The local delete is the trust
54
+ * boundary — an upstream revoke failure never leaves a stored token usable.
55
+ */
56
+ export async function revokeOAuthCredential(options) {
57
+ await options.provider.revoke?.(options.credentials);
58
+ await options.store?.delete(options.provider.id, options.credentials.accountId);
59
+ }
51
60
  function credentialMapKey(name, provider) {
52
61
  return provider ? `${provider}:${name}` : name;
53
62
  }