@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
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Device adapter contract (realtime voice / desktop OS control) — 0.0.14.
3
+ *
4
+ * Contracts + deny-by-default policy ONLY. No vendor voice or desktop-control
5
+ * implementation ships in 0.0.14 (demand-gated to 0.1.x). This module composes
6
+ * over the existing `PermissionPolicy` / `RunLimits` / redactor seams; it adds
7
+ * no second approval runtime and no device framework. Hosts implement
8
+ * `DeviceAdapter`, resolve a policy, and admit sessions through the fail-closed
9
+ * gate below. Conformance fixtures (denial/approval/stream-bounds/redaction)
10
+ * live in `runDevicePolicyConformance` for future vendor adapters to run.
11
+ */
12
+ import type { RunLimits } from "./contracts.js";
13
+ import { type SecretRedactor } from "./redaction.js";
14
+ /** Audio / screenshot / stream chunk: 1 MiB default / 8 MiB hard. */
15
+ export declare const DEFAULT_DEVICE_MAX_CHUNK_BYTES: number;
16
+ export declare const HARD_DEVICE_MAX_CHUNK_BYTES: number;
17
+ /** Concurrent device sessions per identity: 1 default / 4 hard. */
18
+ export declare const DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS = 1;
19
+ export declare const HARD_DEVICE_MAX_CONCURRENT_SESSIONS = 4;
20
+ export type DeviceKind = "voice" | "desktop-control";
21
+ export type DevicePolicyErrorCode = "ERR_PRISM_DEVICE_INPUT" | "ERR_PRISM_DEVICE_DISABLED" | "ERR_PRISM_DEVICE_APPROVAL" | "ERR_PRISM_DEVICE_SESSIONS" | "ERR_PRISM_DEVICE_CHUNK" | "ERR_PRISM_DEVICE_RUN_LIMITS";
22
+ export declare class DevicePolicyError extends Error {
23
+ readonly code: DevicePolicyErrorCode;
24
+ constructor(code: DevicePolicyErrorCode, message: string);
25
+ }
26
+ export interface DeviceStreamLimits {
27
+ readonly maxChunkBytes?: number;
28
+ readonly maxConcurrentSessions?: number;
29
+ }
30
+ /**
31
+ * Host-declared device adapter. `enabled` is deny-by-default: a device is
32
+ * admitted only when the host explicitly sets it `true` AND supplies a sandbox
33
+ * AND (when `requireApproval`) explicit per-side-effect approval.
34
+ */
35
+ export interface DeviceAdapter {
36
+ readonly kind: DeviceKind;
37
+ readonly enabled: boolean;
38
+ readonly requireApproval: boolean;
39
+ readonly limits?: DeviceStreamLimits;
40
+ /** Host-owned sandbox identifier; admission fails closed without it. */
41
+ readonly sandbox?: string;
42
+ /** Host-owned network/egress policy identifier. */
43
+ readonly network?: string;
44
+ }
45
+ export interface ResolvedDevicePolicy {
46
+ readonly kind: DeviceKind;
47
+ readonly enabled: boolean;
48
+ readonly requireApproval: boolean;
49
+ readonly maxChunkBytes: number;
50
+ readonly maxConcurrentSessions: number;
51
+ readonly sandbox?: string;
52
+ readonly network?: string;
53
+ /** Shared run accounting the device session must consume. */
54
+ readonly runLimits?: RunLimits;
55
+ }
56
+ export interface DevicePolicyOptions {
57
+ readonly maxChunkBytes?: number;
58
+ readonly maxConcurrentSessions?: number;
59
+ readonly runLimits?: RunLimits;
60
+ }
61
+ export declare function resolveDevicePolicy(adapter: DeviceAdapter, options?: DevicePolicyOptions): ResolvedDevicePolicy;
62
+ export interface DeviceAdmitRequest {
63
+ /** Explicit host approval for this device side effect. */
64
+ readonly approved: boolean;
65
+ /** Currently active device sessions for this identity. */
66
+ readonly activeSessions: number;
67
+ }
68
+ /**
69
+ * Fail-closed admission gate. Denies unless the device is explicitly enabled,
70
+ * sandboxed, approved (when required), under the concurrent-session budget, and
71
+ * bound to shared run accounting. Side effects never replay after reconnect:
72
+ * hosts must re-admit on every resume.
73
+ */
74
+ export declare function assertDeviceAdmit(policy: ResolvedDevicePolicy, request: DeviceAdmitRequest): void;
75
+ export interface DeviceChunkResult {
76
+ readonly accepted: boolean;
77
+ readonly bytes: number;
78
+ /** Present when the chunk exceeded the stream bound and was dropped. */
79
+ readonly marker?: "dropped_oversize";
80
+ }
81
+ /** Stream bound: oversize audio/screenshot/stream chunks are dropped with a marker, never forwarded. */
82
+ export declare function acceptDeviceChunk(policy: ResolvedDevicePolicy, bytes: number): DeviceChunkResult;
83
+ /** Telemetry must be metadata-safe: apply the host redactor before any emit/persist. */
84
+ export declare function redactDeviceTelemetry<T>(redactor: SecretRedactor | undefined, telemetry: T): T;
85
+ export interface DeviceConformanceResult {
86
+ readonly passed: readonly string[];
87
+ }
88
+ /**
89
+ * Conformance pair for future voice / desktop-control adapters. Runs the
90
+ * deny-by-default fixtures (denial, approval, stream bounds, session budget,
91
+ * run accounting, redaction) against a resolved policy and throws on any
92
+ * regression. Tested in 0.0.14 via fixtures only.
93
+ */
94
+ export declare function runDevicePolicyConformance(adapter: DeviceAdapter, options?: DevicePolicyOptions): DeviceConformanceResult;
@@ -0,0 +1,138 @@
1
+ import { redactSecrets } from "./redaction.js";
2
+ /** Audio / screenshot / stream chunk: 1 MiB default / 8 MiB hard. */
3
+ export const DEFAULT_DEVICE_MAX_CHUNK_BYTES = 1 * 1024 * 1024;
4
+ export const HARD_DEVICE_MAX_CHUNK_BYTES = 8 * 1024 * 1024;
5
+ /** Concurrent device sessions per identity: 1 default / 4 hard. */
6
+ export const DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS = 1;
7
+ export const HARD_DEVICE_MAX_CONCURRENT_SESSIONS = 4;
8
+ export class DevicePolicyError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.code = code;
13
+ this.name = "DevicePolicyError";
14
+ }
15
+ }
16
+ function validateCap(name, value, def, hard) {
17
+ const resolved = value ?? def;
18
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > hard) {
19
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", `${name} must be a positive safe integer at most ${hard}`);
20
+ }
21
+ return resolved;
22
+ }
23
+ export function resolveDevicePolicy(adapter, options = {}) {
24
+ if (!adapter || (adapter.kind !== "voice" && adapter.kind !== "desktop-control")) {
25
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "device kind must be 'voice' or 'desktop-control'");
26
+ }
27
+ return {
28
+ kind: adapter.kind,
29
+ // Deny-by-default: anything but an explicit `true` resolves to disabled.
30
+ enabled: adapter.enabled === true,
31
+ // Approval is required unless the host explicitly opts out (it should not).
32
+ requireApproval: adapter.requireApproval !== false,
33
+ maxChunkBytes: validateCap("maxChunkBytes", options.maxChunkBytes ?? adapter.limits?.maxChunkBytes, DEFAULT_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CHUNK_BYTES),
34
+ maxConcurrentSessions: validateCap("maxConcurrentSessions", options.maxConcurrentSessions ?? adapter.limits?.maxConcurrentSessions, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, HARD_DEVICE_MAX_CONCURRENT_SESSIONS),
35
+ ...(adapter.sandbox ? { sandbox: adapter.sandbox } : {}),
36
+ ...(adapter.network ? { network: adapter.network } : {}),
37
+ ...(options.runLimits ? { runLimits: options.runLimits } : {}),
38
+ };
39
+ }
40
+ /**
41
+ * Fail-closed admission gate. Denies unless the device is explicitly enabled,
42
+ * sandboxed, approved (when required), under the concurrent-session budget, and
43
+ * bound to shared run accounting. Side effects never replay after reconnect:
44
+ * hosts must re-admit on every resume.
45
+ */
46
+ export function assertDeviceAdmit(policy, request) {
47
+ if (!policy.enabled) {
48
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_DISABLED", `${policy.kind} device is disabled by default`);
49
+ }
50
+ if (!policy.sandbox) {
51
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_DISABLED", `${policy.kind} device requires an explicit sandbox`);
52
+ }
53
+ if (policy.requireApproval && !request.approved) {
54
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_APPROVAL", `${policy.kind} device side effect requires approval`);
55
+ }
56
+ if (!Number.isSafeInteger(request.activeSessions) || request.activeSessions < 0) {
57
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "activeSessions must be a non-negative safe integer");
58
+ }
59
+ if (request.activeSessions >= policy.maxConcurrentSessions) {
60
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_SESSIONS", `concurrent ${policy.kind} sessions capped at ${policy.maxConcurrentSessions}`);
61
+ }
62
+ if (!policy.runLimits) {
63
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_RUN_LIMITS", `${policy.kind} device must consume shared RunLimits`);
64
+ }
65
+ }
66
+ /** Stream bound: oversize audio/screenshot/stream chunks are dropped with a marker, never forwarded. */
67
+ export function acceptDeviceChunk(policy, bytes) {
68
+ if (!Number.isSafeInteger(bytes) || bytes < 0) {
69
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_CHUNK", "chunk bytes must be a non-negative safe integer");
70
+ }
71
+ if (bytes > policy.maxChunkBytes) {
72
+ return { accepted: false, bytes, marker: "dropped_oversize" };
73
+ }
74
+ return { accepted: true, bytes };
75
+ }
76
+ /** Telemetry must be metadata-safe: apply the host redactor before any emit/persist. */
77
+ export function redactDeviceTelemetry(redactor, telemetry) {
78
+ return redactor ? redactor.redact(telemetry) : telemetry;
79
+ }
80
+ /**
81
+ * Conformance pair for future voice / desktop-control adapters. Runs the
82
+ * deny-by-default fixtures (denial, approval, stream bounds, session budget,
83
+ * run accounting, redaction) against a resolved policy and throws on any
84
+ * regression. Tested in 0.0.14 via fixtures only.
85
+ */
86
+ export function runDevicePolicyConformance(adapter, options = {}) {
87
+ const runLimits = options.runLimits ?? { maxTurns: 1 };
88
+ const expectThrow = (code, fn) => {
89
+ let threw = false;
90
+ try {
91
+ fn();
92
+ }
93
+ catch (error) {
94
+ threw = error instanceof DevicePolicyError && error.code === code;
95
+ }
96
+ if (!threw)
97
+ throw new DevicePolicyError(code, `conformance: expected ${code}`);
98
+ };
99
+ const passed = [];
100
+ // 1. Denial by default: a disabled adapter never admits.
101
+ const disabled = resolveDevicePolicy({ ...adapter, enabled: false }, { ...options, runLimits });
102
+ expectThrow("ERR_PRISM_DEVICE_DISABLED", () => assertDeviceAdmit(disabled, { approved: true, activeSessions: 0 }));
103
+ passed.push("denial-by-default");
104
+ // 2. Approval gate: enabled+sandboxed but unapproved denies; approved admits.
105
+ const enabled = resolveDevicePolicy({ ...adapter, enabled: true, requireApproval: true, sandbox: adapter.sandbox ?? "sandbox" }, { ...options, runLimits });
106
+ expectThrow("ERR_PRISM_DEVICE_APPROVAL", () => assertDeviceAdmit(enabled, { approved: false, activeSessions: 0 }));
107
+ assertDeviceAdmit(enabled, { approved: true, activeSessions: 0 });
108
+ passed.push("approval-gate");
109
+ // 3. Session budget: at/over the concurrent cap denies.
110
+ expectThrow("ERR_PRISM_DEVICE_SESSIONS", () => assertDeviceAdmit(enabled, { approved: true, activeSessions: enabled.maxConcurrentSessions }));
111
+ passed.push("session-budget");
112
+ // 4. Run accounting: no shared RunLimits denies.
113
+ const unaccounted = resolveDevicePolicy({ ...adapter, enabled: true, sandbox: "sandbox" }, { ...options, runLimits: undefined });
114
+ expectThrow("ERR_PRISM_DEVICE_RUN_LIMITS", () => assertDeviceAdmit(unaccounted, { approved: true, activeSessions: 0 }));
115
+ passed.push("run-accounting");
116
+ // 5. Stream bounds: oversize chunk dropped with marker; in-bound accepted.
117
+ const over = acceptDeviceChunk(enabled, enabled.maxChunkBytes + 1);
118
+ if (over.accepted || over.marker !== "dropped_oversize") {
119
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_CHUNK", "conformance: oversize chunk must be dropped");
120
+ }
121
+ if (!acceptDeviceChunk(enabled, enabled.maxChunkBytes).accepted) {
122
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_CHUNK", "conformance: in-bound chunk must accept");
123
+ }
124
+ passed.push("stream-bounds");
125
+ // 6. Redaction: telemetry passes through the host redactor (secrets stripped), passthrough when absent.
126
+ const secret = "super-secret-device-token";
127
+ const redactor = { redact: (value) => redactSecrets(value, [secret]) };
128
+ const redacted = redactDeviceTelemetry(redactor, { note: `leak ${secret}` });
129
+ if (redacted.note.includes(secret)) {
130
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "conformance: telemetry secret must be redacted");
131
+ }
132
+ if (redactDeviceTelemetry(undefined, { note: "ok" }).note !== "ok") {
133
+ throw new DevicePolicyError("ERR_PRISM_DEVICE_INPUT", "conformance: telemetry must pass through without a redactor");
134
+ }
135
+ passed.push("redaction");
136
+ return { passed };
137
+ }
138
+ //# sourceMappingURL=devices.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type * from "./contracts.js";
2
- export type { RunLimitCounters, RunLimitName, SecureAgentOptions } from "./contracts.js";
3
- export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
2
+ export type { RealtimeCaps, RealtimeEvent, RealtimeSession, RealtimeSessionFactory, RealtimeSessionOptions, RunLimitCounters, RunLimitName, SecureAgentOptions, ToolCallAuthority, } from "./contracts.js";
3
+ export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, assertSessionMetadataKey, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
4
4
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
5
5
  export { createBatchedRunLedger, isFlushableRunLedger, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_ENTRIES, DEFAULT_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_BYTES, DEFAULT_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_DELAY_MS, } from "./run-ledger.js";
6
6
  export type { BatchedRunLedgerOptions } from "./run-ledger.js";
@@ -30,10 +30,10 @@ export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./
30
30
  export type { DefaultRetryPolicyOptions } from "./retry.js";
31
31
  export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
32
32
  export type { ContributionRegistries, ContributionRegistriesOptions, ContributionRegistry, ContributionRegistryOptions } from "./contributions.js";
33
- export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue } from "./credentials.js";
33
+ export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, revokeOAuthCredential, resolveCredentialValue } from "./credentials.js";
34
34
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
35
35
  export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy } from "./extensions.js";
36
- export type { CredentialRecord, CredentialValueSource, MemoryCredentialStore } from "./credentials.js";
36
+ export type { CredentialRecord, CredentialValueSource, MemoryCredentialStore, RevocableOAuthCredentialStore } from "./credentials.js";
37
37
  export { createModelRegistry } from "./models.js";
38
38
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
39
39
  export { assertStructuredOutputRequestSupported, artifactStructuredOutputRequest, DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH, DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES, modelSupportsStructuredOutput, resolveRunProviderOptions, StructuredOutputError, validateStructuredOutputOptions, withoutStructuredOutput, } from "./structured-output.js";
@@ -62,9 +62,15 @@ export type { ContextBudget, ContextBudgetMessageGroups, ContextBudgetOmission,
62
62
  export type { AgentInput, AssembleProviderInputOptions, DefaultInputBuilder, DefaultInputBuildContext, DefaultPromptBuilder, InputAttachment, PromptInstruction, PromptTemplateOptions, ResolveContextOptions } from "./input.js";
63
63
  export { createMockProvider } from "./mock-provider.js";
64
64
  export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, listSessionBranches, rebuildSessionContext } from "./session-stores.js";
65
+ export { CONVERSATION_METADATA_KEY, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, HARD_MAX_CONVERSATION_CURSOR_BYTES, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, decodeConversationReplayCursor, encodeConversationReplayCursor, } from "./conversations.js";
66
+ export type { ConversationBranchRef, ConversationReplayCursor, ConversationThread, ConversationThreadState, } from "./conversations.js";
67
+ export { ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
68
+ export type { ArtifactApproval, ArtifactApprovalState, ArtifactCitation, ArtifactDecisionState, ArtifactDeliveryToken, ArtifactRecord, ArtifactRevision, } from "./artifacts.js";
69
+ export { DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, acceptDeviceChunk, assertDeviceAdmit, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
70
+ export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConformanceResult, DeviceKind, DevicePolicyErrorCode, DevicePolicyOptions, DeviceStreamLimits, ResolvedDevicePolicy, } from "./devices.js";
65
71
  export type { CreateMemorySessionStoreOptions, CreateSessionEntryOptions, MemorySessionSearchMode, SessionBranch, SessionBranchOptions, SessionContextSnapshot } from "./session-stores.js";
66
72
  export type { MockProviderOptions } from "./mock-provider.js";
67
- export { providerContentDelta, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
73
+ export { providerContentDelta, providerContinuationRequired, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
68
74
  export type { ProviderResolver } from "./contracts.js";
69
75
  export { createProviderRegistry, createProviderResolver } from "./providers.js";
70
76
  export type { ProviderRegistry, ProviderRegistryOptions } from "./providers.js";
@@ -89,5 +95,5 @@ export type { DispatchToolCallOptions, ToolArgumentValidationError, ToolArgument
89
95
  export type { DuplicateRegistrationOptions, DuplicateRegistrationPolicy } from "./registry-options.js";
90
96
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
91
97
  export declare const name = "prism";
92
- export declare const version = "0.0.13";
98
+ export declare const version = "0.0.15";
93
99
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
1
+ export { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_KINDS, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, isSessionAppendConflict, AgentRunError, AgentRunStateError, assertSessionMetadataKey, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SESSION_SEARCH_UNSUPPORTED_CODE, SessionSearchUnsupportedError, isSessionSearchUnsupported, DEFAULT_SESSION_SEARCH_LIMIT, HARD_MAX_SESSION_SEARCH_LIMIT, DEFAULT_MAX_SESSION_SEARCH_QUERY_BYTES, HARD_MAX_SESSION_SEARCH_QUERY_BYTES, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, HARD_MAX_SESSION_SEARCH_SNIPPET_BYTES, DEFAULT_MAX_SESSION_SEARCH_CURSOR_BYTES, HARD_MAX_SESSION_SEARCH_CURSOR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, HARD_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, HARD_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, HARD_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES, HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES, resolveSessionSearchQuery, DEFAULT_MAX_PENDING_STEERS, HARD_MAX_PENDING_STEERS, DEFAULT_MAX_PENDING_STEER_BYTES, HARD_MAX_PENDING_STEER_BYTES } from "./contracts.js";
2
2
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
3
3
  export { createBatchedRunLedger, isFlushableRunLedger, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_ENTRIES, DEFAULT_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_BYTES, DEFAULT_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_DELAY_MS, } from "./run-ledger.js";
4
4
  export { createSecureAgent } from "./secure-agent.js";
@@ -16,7 +16,7 @@ export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } f
16
16
  export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
17
17
  export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./retry.js";
18
18
  export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
19
- export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue } from "./credentials.js";
19
+ export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, revokeOAuthCredential, resolveCredentialValue } from "./credentials.js";
20
20
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
21
21
  export { createModelRegistry } from "./models.js";
22
22
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
@@ -35,7 +35,10 @@ export { assembleProviderInput, createDefaultInputBuilder, createDefaultPromptBu
35
35
  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";
36
36
  export { createMockProvider } from "./mock-provider.js";
37
37
  export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, listSessionBranches, rebuildSessionContext } from "./session-stores.js";
38
- export { providerContentDelta, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
38
+ export { CONVERSATION_METADATA_KEY, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, HARD_MAX_CONVERSATION_CURSOR_BYTES, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, decodeConversationReplayCursor, encodeConversationReplayCursor, } from "./conversations.js";
39
+ export { ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
40
+ export { DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, acceptDeviceChunk, assertDeviceAdmit, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
41
+ export { providerContentDelta, providerContinuationRequired, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
39
42
  export { createProviderRegistry, createProviderResolver } from "./providers.js";
40
43
  export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "./redaction.js";
41
44
  export { assertPermission, assertTrusted, checkPermission, createStaticPermissionPolicy, createStaticTrustPolicy, denialToErrorInfo, isTrusted, PermissionDeniedError, TrustDeniedError } from "./security.js";
@@ -48,6 +51,6 @@ export { assertGuardrailsAllowed, GuardrailError, MAX_GUARDRAIL_CONCURRENCY, run
48
51
  export { createRunLimitTracker, DEFAULT_RUN_LIMITS, HARD_MAX_RUN_COST, HARD_RUN_LIMITS, RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
49
52
  export { dispatchToolCallsInOrder, generateValidateReviseLoop, isAgentLoopOptions, resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
50
53
  export const name = "prism";
51
- export const version = "0.0.13";
54
+ export const version = "0.0.15";
52
55
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
53
56
  //# sourceMappingURL=index.js.map
@@ -10,6 +10,7 @@ export declare function providerToolCallDelta(delta: {
10
10
  readonly argumentsText?: string;
11
11
  }): ProviderEvent;
12
12
  export declare function providerToolCallDeltaContent(delta: Omit<ToolCallDeltaContent, "type">): ToolCallDeltaContent;
13
+ export declare function providerContinuationRequired(cursor: string, reason?: string): ProviderEvent;
13
14
  export declare function reconstructToolCallDeltas(events: readonly ProviderEvent[]): readonly ToolCallContent[];
14
15
  export declare function providerUsage(usage: Usage): ProviderEvent;
15
16
  export declare function providerDone(usage?: Usage): ProviderEvent;
@@ -18,6 +18,9 @@ export function providerToolCallDelta(delta) {
18
18
  export function providerToolCallDeltaContent(delta) {
19
19
  return { type: "tool_call_delta", ...delta };
20
20
  }
21
+ export function providerContinuationRequired(cursor, reason) {
22
+ return { type: "continuation_required", cursor, reason };
23
+ }
21
24
  export function reconstructToolCallDeltas(events) {
22
25
  const partials = new Map();
23
26
  for (const event of events) {
@@ -115,7 +115,8 @@ export function mapOpenAIChatUsage(usage) {
115
115
  && wire.total_tokens === undefined
116
116
  && wire.prompt_cache_hit_tokens === undefined
117
117
  && wire.prompt_tokens_details?.cached_tokens === undefined
118
- && wire.prompt_tokens_details?.cache_write_tokens === undefined) {
118
+ && wire.prompt_tokens_details?.cache_write_tokens === undefined
119
+ && wire.prompt_tokens_details?.cache_creation_input_tokens === undefined) {
119
120
  return undefined;
120
121
  }
121
122
  return {
@@ -123,7 +124,9 @@ export function mapOpenAIChatUsage(usage) {
123
124
  outputTokens: wire.completion_tokens,
124
125
  totalTokens: wire.total_tokens,
125
126
  cacheReadTokens: wire.prompt_tokens_details?.cached_tokens ?? wire.prompt_cache_hit_tokens,
126
- cacheWriteTokens: wire.prompt_tokens_details?.cache_write_tokens,
127
+ // Some OpenAI-compatible vendors report cache writes as `cache_write_tokens`,
128
+ // others as the `cache_creation_input_tokens` variant; accept both.
129
+ cacheWriteTokens: wire.prompt_tokens_details?.cache_write_tokens ?? wire.prompt_tokens_details?.cache_creation_input_tokens,
127
130
  };
128
131
  }
129
132
  //# sourceMappingURL=openai-primitives.js.map
package/docs/ag-ui.md CHANGED
@@ -47,6 +47,8 @@ A Prism durable `agent_suspended` returns `RUN_FINISHED` with interrupt id `${ru
47
47
 
48
48
  ACP maps assistant text to `agent_message_chunk`, safe tool lifecycle to `tool_call`/`tool_call_update`, provider usage to `usage_update`, and durable suspension to `session/request_permission`. Only `allow_once` approves; reject, cancellation, unknown outcomes, and request failure deny. It advertises only close-session capability—no terminal, filesystem, MCP, editor state, location, diff, or raw input/output capability.
49
49
 
50
+ Co-work review (0.0.14) projects durable collaboration state over the same stream as named `CUSTOM` events, one per kind: `prism.cowork.artifact.progress`, `prism.cowork.artifact.approval.requested`, `prism.cowork.draft.connector.pending`, `prism.cowork.browser.snapshot`, and `prism.cowork.artifact.download.link`. `AgUiEventMapper.mapCoWork()` (and ACP `mapCoWork()` parity) validate, host-project (`AgUiProjection.coWork`), redact, and byte-cap each event; malformed or oversized events fail closed to nothing rather than leak. The handler accepts optional `coWorkContext` (thread/artifact/identity) and a durable `coWork` source (`createCoWorkReplay()`); one bounded, redacted page is appended after the run stream. Because projection is a pure read + map, disconnect/resume from a cursor replays co-work state without duplicate side effects. Download-link events carry an authorized, expiring token only — hosts fetch the body and never receive local paths, raw credentials, injected browser secrets, or tool-argument dumps.
51
+
50
52
  ## Request/response example
51
53
 
52
54
  ```json
@@ -105,6 +107,8 @@ All identity, authorization, session/thread mapping, durable checkpoint lookup,
105
107
 
106
108
  `AgUiProjection` is an allow-list. Without a callback, raw tool arguments/results/progress, paths, arbitrary state, raw Prism events, ACP locations/diffs/terminals/raw I/O, and frontend-supplied tools remain absent. Use a projector that returns a redacted display value, not a host filesystem path or tool payload.
107
109
 
110
+ Co-work projection reuses the same allow-list: `AgUiProjection.coWork(event)` may return a curated, JSON-serializable payload for a co-work event; absent it, the redacted event fields are exposed. Wire `coWorkContext` to derive thread/artifact/identity from the authorized request (never client JSON) and `coWork` to a `createCoWorkReplay()` over your durable artifact/draft/snapshot stores. The handler projects one bounded page after the run; mount a dedicated cursor-paged co-work endpoint when full pagination is needed.
111
+
108
112
  ## Security and performance notes
109
113
 
110
114
  Authorize every start, replay, resume, ACP new/prompt/cancel/close request. Treat thread IDs, run IDs, cursors, client messages, resume payloads, and protocol output as untrusted. Persist run ↔ protocol correlation before exposing an interrupt. Keep `SecretRedactor` active for streaming and ledger writes.
@@ -121,3 +125,4 @@ Benchmark command/result placeholder: Task 8 adds `node scripts/benchmark-0.0.12
121
125
  - [Web-standard server handler](server.md): generic Prism HTTP API, separate from AG-UI.
122
126
  - [A2A interoperability](a2a.md): remote agent-to-agent tasks, not frontend protocol mapping.
123
127
  - [Host security guide](host-security.md): authorization, ownership, redaction, and credential boundaries.
128
+ - [Work artifacts and review](work-artifacts-and-review.md): durable artifact service that produces the co-work approval/progress/download-link events projected here.
@@ -106,6 +106,7 @@ await browser.close();
106
106
  - Observation (`snapshot`, `wait`, open-without-url, `close`) vs mutation/high-impact (`navigate`, click/form, dialog accept, upload, download release, popup select) is classified for `ExecutionPolicy` / `beforeSideEffect`.
107
107
  - `createSharedSandboxBrowserOptions()` aligns browser uploads/downloads with Task 1 sandbox `/workspace` and `/downloads`. `assertBrowserSandboxNetwork()` in `@arnilo/prism-coding-security` fails closed for custom Docker networks without browser egress attestation.
108
108
  - Raw CSS is absent from production defaults. Ref resolution uses Playwright’s built-in `aria-ref=` selector with a package-owned snapshot ref table for staleness checks.
109
+ - Verified-state checkpoints (0.0.14): `createBrowserCheckpointLedger()` records navigation state — URL, a domain-state hash, and host-owned data refs — never serialized browser internals (cookies/storage/contexts), which are fragile and secret-bearing. Frozen caps: URL 8 KiB/16 KiB, domain-state hash 256 B/1 KiB, host-data ref 2 KiB/8 KiB (refs only, never bodies), 16/64 checkpoints per run (oldest evicted). After any resume/interruption `markResumed(runId)` marks state stale; `assertVerifiedBeforeSideEffect(runId)` fails closed until the host reloads + `verify()`s, so side effects never replay on stale state. Checkpoints are run-scoped: a conversation thread composes through the run it owns, reusing the manager's sandbox/egress/approval/limit policy above.
109
110
 
110
111
  ## Security and performance notes
111
112
 
@@ -121,4 +122,6 @@ Default tests use fake Playwright APIs only. Protected live gate: `PRISM_LIVE_PL
121
122
  - [Host security](host-security.md): browser endpoint, approval, egress proxy, and artifact trust boundaries.
122
123
  - [Performance and resource limits](performance.md): browser ceilings and charging points.
123
124
  - [Coding execution approval and sandboxing](coding-security.md): optional shared disposable sandbox for coding+browser.
125
+ - [Conversations](conversations.md): durable threads that own the runs browser checkpoints scope to.
126
+ - [Device adapters](device-adapters.md): deny-by-default voice/desktop-control contracts (no vendor package in 0.0.14).
124
127
  - [Migration](migration.md): additive optional package activation.
@@ -0,0 +1,135 @@
1
+ # Conversations
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-server` ships a durable, user-scoped conversation service: create/list/get/continue/branch/archive/export/delete conversation threads on top of the existing session and event-ledger seams. A thread **is** an ownership-scoped session branch plus a `prismConversation` marker in `SessionRecord.metadata`; content stays in session entries and the redacted event ledger. Reconnectable replay pages durable redacted events without ever rerunning a provider or tool.
6
+
7
+ Core (`@arnilo/prism`) exports only conversation **types and pure helpers** (`ConversationThread`, `ConversationError`, `CONVERSATION_METADATA_KEY`, thread-bound replay cursor codec, `conversationThreadFromRecord`, `conversationMarkerMetadata`). The service and optional HTTP handler live in `@arnilo/prism-server`.
8
+
9
+ ## When to use it
10
+
11
+ Use it when a host needs persistent personal/work-agent conversations with reconnect, branch, archive, export, and deletion semantics without building a second session/event system. Hosts own authentication, agent selection, UI, transport chrome, and blob storage.
12
+
13
+ Do not use it as a chat UI, a push/always-on daemon, or a file store. Slack/Teams channels, realtime voice, and desktop-control vendors are deferred (0.1.x); device adapters are contract + deny-by-default conformance only in 0.0.14.
14
+
15
+ ## Inputs / request
16
+
17
+ ```ts
18
+ import { createConversationService, createConversationHandler } from "@arnilo/prism-server";
19
+ import { createSqlitePersistence } from "@arnilo/prism-session-store-sqlite";
20
+
21
+ const persistence = createSqlitePersistence({ filename }); // implements ConversationServiceStore
22
+ const service = createConversationService(persistence, {
23
+ redactor, // required: replay/export serve redacted rows only; continue runs with it
24
+ sessionFactory: ({ thread, leafId, ownership, signal }) =>
25
+ agent.createSession({ id: thread.id, ...(leafId ? { leafId } : {}) }), // host binds agent/store/leaf
26
+ runOptions?, // narrowable RunOptions minus ownership/identity/signal/redactor/idempotencyKey
27
+ limits?: ConversationLimits, // frozen defaults/caps below
28
+ });
29
+
30
+ await service.create({ ownership, title?, id?, requestId?, metadata? });
31
+ await service.list({ ownership, cursor?, limit? });
32
+ await service.get({ ownership, threadId });
33
+ await service.continue({ ownership, threadId, message, requestId?, leafId? });
34
+ await service.branch({ ownership, threadId, leafId });
35
+ await service.archive({ ownership, threadId });
36
+ await service.export({ ownership, threadId, cursor? });
37
+ await service.delete({ ownership, threadId });
38
+ await service.replay({ ownership, threadId, cursor?, limit? });
39
+
40
+ const handler = createConversationHandler({ service, authorize, basePath?: "/prism/conversations", redactor?, limits? });
41
+ ```
42
+
43
+ `ConversationServiceStore` is a narrow `Pick` of `ProductionPersistenceStore`: `querySessions`, `queryEvents`, optional `appendSession` (required at factory time; sqlite/postgres implement it), and optional `lifecycle.applyRetention` (required for delete). Stores without `appendSession` fail closed at construction.
44
+
45
+ ## Outputs / response / events
46
+
47
+ - `create`/`get`/`branch`/`archive` → `ConversationThread` (`id`, `title?`, `state: "active" | "archived"`, `branches`, timestamps, ownership projection, host metadata).
48
+ - `list` → ownership-scoped `PersistencePage<ConversationThread>` (newest first), marker-filtered; non-conversation sessions never appear.
49
+ - `continue` → `AgentRunResult` from one agent turn on the thread session; history rebuilds from durable entries, so the agent sees prior turns.
50
+ - `replay` → `{ records: AgentEventRecord[], nextCursor?, terminal }`; records are durable redacted ledger rows ordered by `(timestamp, id)`; `terminal` marks `agent_finished`/`agent_denied`/`error`.
51
+ - `export` → `{ thread, events, nextCursor?, truncated }`; redacted, byte/page-capped, cursor-resumable.
52
+ - `delete` → `{ deleted, held }` via persistence lifecycle; legal holds always win.
53
+ - HTTP handler routes: `POST {base}` create · `GET {base}` list · `GET {base}/{id}` · `DELETE {base}/{id}` · `POST {base}/{id}/continue|branch|archive|export` · `GET {base}/{id}/events?cursor=&limit=` replay.
54
+
55
+ ## Request/response example
56
+
57
+ ```http
58
+ POST /prism/conversations HTTP/1.1
59
+ content-type: application/json
60
+
61
+ { "title": "Q3 planning" }
62
+ ```
63
+
64
+ ```json
65
+ { "id": "conv_9b0f…", "title": "Q3 planning", "state": "active", "branches": [], "createdAt": "…", "updatedAt": "…", "tenantId": "t1", "userId": "u1" }
66
+ ```
67
+
68
+ Reconnect after a dropped connection: `GET {base}/{id}/events` (optionally with the last `nextCursor`) pages the same durable events; clients dedupe by stable record `id` (at-least-once across the page boundary). No provider or tool call is re-executed by replay/export.
69
+
70
+ ## Implementation example
71
+
72
+ ```ts
73
+ const thread = await service.create({ ownership, title: "draft review" });
74
+ await service.continue({ ownership, threadId: thread.id, message: "summarize the attached plan", requestId: "ui-req-1" });
75
+
76
+ // Branch from a known leaf (e.g. last entry id), then fork a continue from it.
77
+ const branched = await service.branch({ ownership, threadId: thread.id, leafId });
78
+ await service.continue({ ownership, threadId: thread.id, message: "try a shorter version", leafId });
79
+
80
+ // Reconnectable replay.
81
+ let cursor: string | undefined;
82
+ do {
83
+ const page = await service.replay({ ownership, threadId: thread.id, ...(cursor ? { cursor } : {}) });
84
+ render(page.records);
85
+ cursor = page.nextCursor;
86
+ } while (cursor);
87
+
88
+ await service.archive({ ownership, threadId: thread.id });
89
+ const result = await service.delete({ ownership, threadId: thread.id }); // { deleted: true, held: false }
90
+ ```
91
+
92
+ ## Extension and configuration notes
93
+
94
+ Frozen limits (default / hard cap; hosts may tighten, never raise past hard caps):
95
+
96
+ | Resource | Default / hard cap |
97
+ | --- | ---: |
98
+ | Thread list page | 50 / 200 |
99
+ | Replay/export page rows | 100 / 500 |
100
+ | Replay/export cursor | 4 KiB / 16 KiB |
101
+ | Thread title | 256 B / 2 KiB |
102
+ | Client request id | 256 B / 2 KiB |
103
+ | Active branches per thread | 16 / 64 |
104
+ | Export payload per request | 8 MiB / 32 MiB |
105
+ | Export pages per request | 100 / 500 |
106
+ | Handler request body | 64 KiB / 1 MiB |
107
+
108
+ Behavior notes:
109
+
110
+ - `create` with an explicit `id` is idempotent get-or-create; generated ids are `conv_<uuid>`.
111
+ - `continue` `requestId` flows into session-append idempotency (`RunRecord.idempotencyKey` + append dedup), so exact retries deduplicate.
112
+ - `continue` on an archived thread fails closed (`thread_archived`); `leafId` must be a branch ref recorded by `branch()`.
113
+ - Replay cursors are thread-bound: a cursor minted for one thread is rejected on another (`cursor_thread_mismatch`).
114
+ - Ledger rows from runs that had no redactor are never served by replay/export (fail-closed skip).
115
+ - Export truncates at page granularity when the next page would exceed `exportBytes`; a single page larger than `exportBytes` cannot be exported (raise the cap or page via `replay`).
116
+ - Branch refs live in thread metadata (read-modify-write); concurrent `branch()` calls can lose a ref, so the cap is approximate and the entry tree remains the content source of truth.
117
+ - Deletion purges the whole session ledger (entries, runs, events, tool calls, usage, branches, search rows) through `lifecycle.applyRetention`; legal holds block deletion and report `held: true`.
118
+
119
+ ## Security and performance notes
120
+
121
+ - Every operation starts from host-verified ownership (and optional `AgentIdentity`, which must project onto ownership without widening); wrong-user access returns not-found, never leaked existence.
122
+ - `appendSession` upserts set ownership columns only on create; metadata/`updatedAt` on update — ownership is immutable after create.
123
+ - Replay/export serve `redacted: true` ledger rows only and pass through the service redactor; no local paths, raw tool payloads, or secrets are emitted.
124
+ - All loops are bounded by the frozen caps above; review/agent turns consume shared `RunLimits` via the host's `runOptions`.
125
+ - No new permission surface: conversations reuse session/event/identity/redaction/lifecycle seams (roadmap gate 8).
126
+
127
+ ## Related APIs
128
+
129
+ - [Web-standard server handler](server.md): authorized agent/workflow routes; the conversation handler mounts beside it.
130
+ - [Session stores](session-stores.md): branch/append/checkout semantics a thread builds on.
131
+ - [Database persistence](database-persistence.md): `ProductionPersistenceStore`, `appendSession`, `SessionQuery` id/metadataKey filters, retention/legal-hold lifecycle.
132
+ - [Agents and sessions](agent-identity.md): verified identity and ownership projection.
133
+ - [Credentials and redaction](credentials-and-redaction.md): `SecretRedactor` used by replay/export/continue.
134
+ - [Browser automation](browser-automation.md): verified-state checkpoints scope to the runs a thread owns; reload/verify before side effect.
135
+ - [Device adapters](device-adapters.md): deny-by-default voice/desktop sessions bind to a thread's run and consume shared `RunLimits`.
@@ -146,6 +146,33 @@ await refreshOAuthCredential({
146
146
  });
147
147
  ```
148
148
 
149
+ Workload OAuth providers (0.0.14) — Microsoft 365 / Google Workspace over the shared OAuth2 seam (PKCE + device code + refresh + revoke), least-privilege scopes per read/mutation bundle:
150
+
151
+ ```ts
152
+ import { revokeOAuthCredential } from "@arnilo/prism";
153
+ import {
154
+ createMicrosoft365OAuthProvider,
155
+ createGoogleWorkspaceOAuthProvider,
156
+ createOAuthWorkTokenProvider,
157
+ createOAuthCredentialStoreAdapter,
158
+ } from "@arnilo/prism-credentials-node";
159
+
160
+ // Read-only mail/calendar (no mutation scopes requested).
161
+ const m365 = createMicrosoft365OAuthProvider({ clientId: "<app-id>", capabilities: ["mail", "calendar"], access: "read" });
162
+ const creds = await m365.login({ onDeviceCode: ({ userCode, verificationUri }) => host.showCode(userCode, verificationUri) });
163
+ await createOAuthCredentialStoreAdapter(store).set("microsoft365", creds);
164
+
165
+ // Late-bound, per-identity token for a work-tools connector (env var, never argv/model context).
166
+ const tokenProvider = createOAuthWorkTokenProvider({
167
+ provider: m365,
168
+ store: createOAuthCredentialStoreAdapter(store),
169
+ envVar: "M365_ACCESSTOKEN",
170
+ });
171
+
172
+ // Revocation: best-effort upstream (GWS supports RFC 7009; M365 does not) + mandatory local delete.
173
+ await revokeOAuthCredential({ provider: m365, credentials: creds, store: createOAuthCredentialStoreAdapter(store) });
174
+ ```
175
+
149
176
  Passphrase rotation:
150
177
 
151
178
  ```ts
@@ -205,7 +232,7 @@ const providers = createOpenAIProviderPackage({ apiKey });
205
232
  - Use distinct `namespace` or vault paths per tenant/environment.
206
233
  - Keychain `list()` / `listOAuth()` are intentionally unsupported — enumerate credentials through host configuration instead of scanning the OS store.
207
234
  - Combine with `createExplicitCredentialResolver()` so runtime overrides still win over stored values.
208
- - Wire `createOAuthCredentialStoreAdapter(store)` into `refreshOAuthCredential()` only for an OAuth flow explicitly selected by the host and authorized by that provider. In 0.0.12 that means OpenAI Codex; Anthropic and Google packages accept API keys only. Never import or migrate Claude Code/Gemini CLI credential files, setup tokens, browser sessions, or CLI OAuth rows into this store.
235
+ - Wire `createOAuthCredentialStoreAdapter(store)` into `refreshOAuthCredential()` only for an OAuth flow explicitly selected by the host and authorized by that provider. In 0.0.12 that means OpenAI Codex; in 0.0.14 the Microsoft 365 / Google Workspace workload providers (`createMicrosoft365OAuthProvider` / `createGoogleWorkspaceOAuthProvider`) are added over the same seam with least-privilege read/mutation scope bundles. Anthropic and Google *model* packages still accept API keys only. Never import or migrate Claude Code/Gemini CLI credential files, setup tokens, browser sessions, or CLI OAuth rows into this store.
209
236
  - Enterprise cloud providers (`azure` / `bedrock` / `vertex`) expect host workload-identity callbacks (Entra / IAM / ADC), not this local encrypted/keychain store as a cloud token minting service. Store may hold opaque refresh material only when the host already owns the cloud auth flow.
210
237
 
211
238
  ## Security and performance notes
@@ -8,6 +8,7 @@ Prism provides small helpers for host-owned credentials and known-secret redacti
8
8
  - `createExplicitCredentialResolver()`: tries named resolver sources in caller-provided order, such as runtime override → stored → env object → fallback.
9
9
  - `createEnvCredentialResolver()`: reads only a caller-supplied env-like object and map.
10
10
  - `refreshOAuthCredential()`: calls a provider OAuth refresh function and writes the result to a caller-owned store when supplied.
11
+ - `revokeOAuthCredential()`: best-effort upstream revocation (`OAuthProvider.revoke?`) followed by a mandatory caller-owned store delete, so a revoked token fails closed locally even if the provider has no revocation endpoint.
11
12
  - `CredentialValueSource`: the accepted source type for `resolveCredentialValue()`.
12
13
  - `redactSecrets()`: replaces known secret string values inside strings, arrays, and plain objects.
13
14
  - `errorToErrorInfo()`: converts unknown errors into `ErrorInfo` and redacts known secret values from error text.
@@ -41,6 +42,7 @@ resolveCredentialValue(
41
42
  createExplicitCredentialResolver(sources: readonly CredentialResolverSource[]): CredentialResolver
42
43
  createEnvCredentialResolver(env: Readonly<Record<string, string | undefined>>, map: Readonly<Record<string, string>>): CredentialResolver
43
44
  refreshOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: OAuthCredentialStore }): Promise<OAuthCredentials>
45
+ revokeOAuthCredential(options: { provider: OAuthProvider; credentials: OAuthCredentials; store?: RevocableOAuthCredentialStore }): Promise<void>
44
46
  redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T
45
47
  errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo
46
48
  ```