@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.
- package/CHANGELOG.md +23 -2
- package/README.md +9 -2
- package/dist/agent-loops.d.ts +4 -0
- package/dist/agent-loops.js +16 -3
- package/dist/artifacts.d.ts +78 -0
- package/dist/artifacts.js +24 -0
- package/dist/contracts.d.ts +86 -0
- package/dist/contracts.js +8 -0
- package/dist/conversations.d.ts +50 -0
- package/dist/conversations.js +97 -0
- package/dist/credentials.d.ts +14 -0
- package/dist/credentials.js +9 -0
- package/dist/devices.d.ts +94 -0
- package/dist/devices.js +138 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -4
- package/dist/provider-events.d.ts +1 -0
- package/dist/provider-events.js +3 -0
- package/dist/providers/openai-primitives.js +5 -2
- package/docs/ag-ui.md +5 -0
- package/docs/browser-automation.md +3 -0
- package/docs/conversations.md +135 -0
- package/docs/credential-storage.md +28 -1
- package/docs/credentials-and-redaction.md +2 -0
- package/docs/database-persistence.md +5 -1
- package/docs/device-adapters.md +97 -0
- package/docs/host-security.md +7 -2
- package/docs/index.md +24 -19
- package/docs/migration.md +50 -1
- package/docs/multimodal-content.md +8 -5
- package/docs/performance.md +36 -0
- package/docs/policy-and-audit.md +1 -0
- package/docs/provider-caching.md +12 -0
- package/docs/provider-conformance.md +29 -5
- package/docs/provider-packages.md +26 -2
- package/docs/providers/ai-sdk.md +23 -7
- package/docs/providers/alibaba.md +179 -0
- package/docs/providers/ollama.md +166 -0
- package/docs/providers/openai.md +22 -3
- package/docs/rag.md +41 -12
- package/docs/release-and-install.md +135 -17
- package/docs/resource-loading.md +3 -0
- package/docs/review-coverage-2026-07-25-phase-9.md +256 -0
- package/docs/review-coverage-2026-07-26-phase-10.md +132 -0
- package/docs/server.md +4 -0
- package/docs/work-artifacts-and-review.md +100 -0
- package/docs/work-connectors.md +5 -1
- package/docs/work-tools.md +3 -0
- package/docs/workflows.md +4 -0
- package/docs/working-and-semantic-memory.md +40 -7
- package/package.json +1 -1
- 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**:
|
|
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
|
|
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 |
|
package/dist/agent-loops.d.ts
CHANGED
|
@@ -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: {
|
package/dist/agent-loops.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
package/dist/contracts.d.ts
CHANGED
|
@@ -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
|
package/dist/credentials.d.ts
CHANGED
|
@@ -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>;
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
}
|