@fabric-harness/sdk 1.13.0 → 2.0.0
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/dist/action.d.ts +69 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +119 -0
- package/dist/action.js.map +1 -0
- package/dist/agent-definition.d.ts +6 -0
- package/dist/agent-definition.d.ts.map +1 -1
- package/dist/agent-definition.js.map +1 -1
- package/dist/agent.d.ts +2 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +35 -1
- package/dist/agent.js.map +1 -1
- package/dist/attachment-store-contract.d.ts +34 -0
- package/dist/attachment-store-contract.d.ts.map +1 -0
- package/dist/attachment-store-contract.js +0 -0
- package/dist/attachment-store-contract.js.map +1 -0
- package/dist/attachment-store.d.ts +107 -0
- package/dist/attachment-store.d.ts.map +1 -0
- package/dist/attachment-store.js +164 -0
- package/dist/attachment-store.js.map +1 -0
- package/dist/channel.d.ts +84 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +115 -0
- package/dist/channel.js.map +1 -0
- package/dist/context-budget.d.ts +16 -0
- package/dist/context-budget.d.ts.map +1 -1
- package/dist/context-budget.js +38 -3
- package/dist/context-budget.js.map +1 -1
- package/dist/conversation-projection.d.ts +52 -0
- package/dist/conversation-projection.d.ts.map +1 -0
- package/dist/conversation-projection.js +207 -0
- package/dist/conversation-projection.js.map +1 -0
- package/dist/conversation-stream-contract.d.ts +37 -0
- package/dist/conversation-stream-contract.d.ts.map +1 -0
- package/dist/conversation-stream-contract.js +453 -0
- package/dist/conversation-stream-contract.js.map +1 -0
- package/dist/conversation-stream.d.ts +141 -0
- package/dist/conversation-stream.d.ts.map +1 -0
- package/dist/conversation-stream.js +159 -0
- package/dist/conversation-stream.js.map +1 -0
- package/dist/cost-budget.d.ts +106 -1
- package/dist/cost-budget.d.ts.map +1 -1
- package/dist/cost-budget.js +48 -2
- package/dist/cost-budget.js.map +1 -1
- package/dist/delivered-message.d.ts +90 -0
- package/dist/delivered-message.d.ts.map +1 -0
- package/dist/delivered-message.js +154 -0
- package/dist/delivered-message.js.map +1 -0
- package/dist/dispatch.d.ts +108 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +179 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/error.d.ts +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/history.d.ts +49 -2
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +163 -0
- package/dist/history.js.map +1 -1
- package/dist/in-memory-submission-store.d.ts +53 -0
- package/dist/in-memory-submission-store.d.ts.map +1 -0
- package/dist/in-memory-submission-store.js +340 -0
- package/dist/in-memory-submission-store.js.map +1 -0
- package/dist/index.d.ts +51 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/dist/loop.d.ts +29 -3
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +167 -24
- package/dist/loop.js.map +1 -1
- package/dist/model.d.ts +37 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +250 -79
- package/dist/model.js.map +1 -1
- package/dist/otel-observer.d.ts +44 -0
- package/dist/otel-observer.d.ts.map +1 -0
- package/dist/otel-observer.js +165 -0
- package/dist/otel-observer.js.map +1 -0
- package/dist/persistence.d.ts +70 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +17 -0
- package/dist/persistence.js.map +1 -0
- package/dist/persistent-agent.d.ts +86 -0
- package/dist/persistent-agent.d.ts.map +1 -0
- package/dist/persistent-agent.js +75 -0
- package/dist/persistent-agent.js.map +1 -0
- package/dist/pi-loop-runtime.d.ts.map +1 -1
- package/dist/pi-loop-runtime.js +7 -1
- package/dist/pi-loop-runtime.js.map +1 -1
- package/dist/profile.d.ts +33 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +10 -0
- package/dist/profile.js.map +1 -0
- package/dist/providers.d.ts +14 -0
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +18 -1
- package/dist/providers.js.map +1 -1
- package/dist/result.d.ts +12 -0
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +26 -0
- package/dist/result.js.map +1 -1
- package/dist/retrieval.d.ts +61 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +26 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-mode.d.ts +1 -1
- package/dist/runtime-mode.d.ts.map +1 -1
- package/dist/runtime-mode.js +2 -1
- package/dist/runtime-mode.js.map +1 -1
- package/dist/scope-key.d.ts +25 -0
- package/dist/scope-key.d.ts.map +1 -0
- package/dist/scope-key.js +34 -0
- package/dist/scope-key.js.map +1 -0
- package/dist/session.d.ts +47 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +364 -35
- package/dist/session.js.map +1 -1
- package/dist/skills.d.ts +33 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +65 -0
- package/dist/skills.js.map +1 -0
- package/dist/store.d.ts +36 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +83 -0
- package/dist/store.js.map +1 -1
- package/dist/stream-chunks.d.ts +42 -0
- package/dist/stream-chunks.d.ts.map +1 -0
- package/dist/stream-chunks.js +138 -0
- package/dist/stream-chunks.js.map +1 -0
- package/dist/strict.d.ts +40 -11
- package/dist/strict.d.ts.map +1 -1
- package/dist/strict.js +22 -7
- package/dist/strict.js.map +1 -1
- package/dist/submission-context.d.ts +26 -0
- package/dist/submission-context.d.ts.map +1 -0
- package/dist/submission-context.js +20 -0
- package/dist/submission-context.js.map +1 -0
- package/dist/submission-runner.d.ts +150 -0
- package/dist/submission-runner.d.ts.map +1 -0
- package/dist/submission-runner.js +767 -0
- package/dist/submission-runner.js.map +1 -0
- package/dist/submission-state.d.ts +50 -0
- package/dist/submission-state.d.ts.map +1 -0
- package/dist/submission-state.js +82 -0
- package/dist/submission-state.js.map +1 -0
- package/dist/submission-store-contract.d.ts +31 -0
- package/dist/submission-store-contract.d.ts.map +1 -0
- package/dist/submission-store-contract.js +730 -0
- package/dist/submission-store-contract.js.map +1 -0
- package/dist/submission-store.d.ts +365 -0
- package/dist/submission-store.d.ts.map +1 -0
- package/dist/submission-store.js +191 -0
- package/dist/submission-store.js.map +1 -0
- package/dist/submission-telemetry.d.ts +56 -0
- package/dist/submission-telemetry.d.ts.map +1 -0
- package/dist/submission-telemetry.js +36 -0
- package/dist/submission-telemetry.js.map +1 -0
- package/dist/testing-contracts.d.ts +17 -0
- package/dist/testing-contracts.d.ts.map +1 -0
- package/dist/testing-contracts.js +14 -0
- package/dist/testing-contracts.js.map +1 -0
- package/dist/tools.d.ts +32 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +132 -6
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +132 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, content-addressed attachment storage (v2 migration, phase A7).
|
|
3
|
+
*
|
|
4
|
+
* Inline base64 attachments on a {@link DeliveredMessage} are materialized at
|
|
5
|
+
* admission into content-addressed refs: bytes live in an
|
|
6
|
+
* {@link AttachmentStore} keyed by `(scope, sha-256 digest)` and the persisted
|
|
7
|
+
* submission payload carries only the ref. `scope` is the storage partition —
|
|
8
|
+
* the harness passes the instance's store session id
|
|
9
|
+
* (`agent:<name>:<id>:<session>`), so attachments are isolated per instance
|
|
10
|
+
* session and content-deduplicated within it.
|
|
11
|
+
*
|
|
12
|
+
* Materialization is deterministic (ids derive from the submission/dispatch
|
|
13
|
+
* id + attachment index, digests from content), so an exact redelivery of the
|
|
14
|
+
* same message produces an identical payload and admission idempotency is
|
|
15
|
+
* preserved. Verify an implementation with
|
|
16
|
+
* `defineAttachmentStoreContractTests` from `attachment-store-contract.ts`.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Build an {@link AttachmentRef} for the given bytes, computing the SHA-256
|
|
20
|
+
* digest via WebCrypto (`crypto.subtle`) so the SDK stays runtime-agnostic.
|
|
21
|
+
*/
|
|
22
|
+
export async function createAttachmentRef(input) {
|
|
23
|
+
return {
|
|
24
|
+
id: input.id,
|
|
25
|
+
mimeType: input.mimeType,
|
|
26
|
+
sizeBytes: input.bytes.byteLength,
|
|
27
|
+
digest: await attachmentDigest(input.bytes),
|
|
28
|
+
...(input.filename !== undefined ? { filename: input.filename } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Lowercase hex SHA-256 of the bytes (WebCrypto). */
|
|
32
|
+
export async function attachmentDigest(bytes) {
|
|
33
|
+
const digest = await crypto.subtle.digest('SHA-256', viewToBuffer(bytes));
|
|
34
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Verify that `bytes` match the ref's digest (and declared size), throwing
|
|
38
|
+
* `AttachmentStoreError('DIGEST_MISMATCH')` otherwise. Every store's `put`
|
|
39
|
+
* MUST run this check before persisting.
|
|
40
|
+
*/
|
|
41
|
+
export async function verifyAttachmentBytes(ref, bytes) {
|
|
42
|
+
if (ref.sizeBytes !== bytes.byteLength || ref.digest !== (await attachmentDigest(bytes))) {
|
|
43
|
+
throw new AttachmentStoreError('DIGEST_MISMATCH', `Attachment ${ref.id} bytes do not match the declared digest/size.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class AttachmentStoreError extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'AttachmentStoreError';
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ─── In-memory store ────────────────────────────────────────────────────────
|
|
55
|
+
/** In-memory attachment store (dev / `runtime: 'stateless'` / tests). */
|
|
56
|
+
export class InMemoryAttachmentStore {
|
|
57
|
+
records = new Map();
|
|
58
|
+
async put(input) {
|
|
59
|
+
await verifyAttachmentBytes(input.ref, input.bytes);
|
|
60
|
+
const key = attachmentKey(input.scope, input.ref.digest);
|
|
61
|
+
// Idempotent by (scope, digest): the first stored copy wins.
|
|
62
|
+
if (this.records.has(key))
|
|
63
|
+
return;
|
|
64
|
+
this.records.set(key, { ref: { ...input.ref }, bytes: Uint8Array.from(input.bytes) });
|
|
65
|
+
}
|
|
66
|
+
async get(scope, digest) {
|
|
67
|
+
const record = this.records.get(attachmentKey(scope, digest));
|
|
68
|
+
if (!record)
|
|
69
|
+
return null;
|
|
70
|
+
return { ref: { ...record.ref }, bytes: Uint8Array.from(record.bytes) };
|
|
71
|
+
}
|
|
72
|
+
async getByAttachmentId(scope, attachmentId) {
|
|
73
|
+
for (const [key, record] of this.records) {
|
|
74
|
+
if (scopeOfKey(key) === scope && record.ref.id === attachmentId) {
|
|
75
|
+
return { ref: { ...record.ref }, bytes: Uint8Array.from(record.bytes) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
async list(scope) {
|
|
81
|
+
const refs = [];
|
|
82
|
+
for (const [key, record] of this.records) {
|
|
83
|
+
if (scopeOfKey(key) === scope)
|
|
84
|
+
refs.push({ ...record.ref });
|
|
85
|
+
}
|
|
86
|
+
return refs;
|
|
87
|
+
}
|
|
88
|
+
async delete(scope, digest) {
|
|
89
|
+
if (!this.records.delete(attachmentKey(scope, digest))) {
|
|
90
|
+
throw new AttachmentStoreError('NOT_FOUND', `Attachment not found: ${scope}/${digest}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function attachmentKey(scope, digest) {
|
|
95
|
+
return JSON.stringify([scope, digest]);
|
|
96
|
+
}
|
|
97
|
+
function scopeOfKey(key) {
|
|
98
|
+
const [scope] = JSON.parse(key);
|
|
99
|
+
return scope;
|
|
100
|
+
}
|
|
101
|
+
// ─── Message materialization ────────────────────────────────────────────────
|
|
102
|
+
/** True when the message carries at least one inline (base64) attachment. */
|
|
103
|
+
export function messageHasDataAttachments(message) {
|
|
104
|
+
return (message.kind === 'user' &&
|
|
105
|
+
(message.attachments ?? []).some((attachment) => attachment.data !== undefined));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Materialize a message's inline attachments into durable refs: decode the
|
|
109
|
+
* base64 data, store the bytes under `scope`, and return a NEW message whose
|
|
110
|
+
* attachments carry `{ type, mimeType, filename?, ref }` and no `data`.
|
|
111
|
+
*
|
|
112
|
+
* Deterministic by construction — attachment ids are `${idPrefix}_${index}`
|
|
113
|
+
* and digests derive from content — so an exact redelivery of the same
|
|
114
|
+
* message produces an identical materialized payload (admission idempotency).
|
|
115
|
+
* Messages without inline attachments are returned unchanged (same
|
|
116
|
+
* reference).
|
|
117
|
+
*/
|
|
118
|
+
export async function materializeMessageAttachments(message, store, options) {
|
|
119
|
+
if (!messageHasDataAttachments(message) || message.kind !== 'user')
|
|
120
|
+
return message;
|
|
121
|
+
const attachments = [];
|
|
122
|
+
const source = message.attachments ?? [];
|
|
123
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
124
|
+
const attachment = source[index];
|
|
125
|
+
if (!attachment || attachment.data === undefined) {
|
|
126
|
+
if (attachment)
|
|
127
|
+
attachments.push(attachment);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const bytes = decodeAttachmentBase64(attachment.data);
|
|
131
|
+
const ref = await createAttachmentRef({
|
|
132
|
+
id: `${options.idPrefix}_${index}`,
|
|
133
|
+
mimeType: attachment.mimeType,
|
|
134
|
+
bytes,
|
|
135
|
+
...(attachment.filename !== undefined ? { filename: attachment.filename } : {}),
|
|
136
|
+
});
|
|
137
|
+
await store.put({ scope: options.scope, ref, bytes });
|
|
138
|
+
attachments.push({
|
|
139
|
+
type: attachment.type,
|
|
140
|
+
mimeType: attachment.mimeType,
|
|
141
|
+
...(attachment.filename !== undefined ? { filename: attachment.filename } : {}),
|
|
142
|
+
ref: { id: ref.id, digest: ref.digest, sizeBytes: ref.sizeBytes },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { kind: 'user', body: message.body, attachments };
|
|
146
|
+
}
|
|
147
|
+
function decodeAttachmentBase64(value) {
|
|
148
|
+
try {
|
|
149
|
+
if (typeof Buffer !== 'undefined')
|
|
150
|
+
return new Uint8Array(Buffer.from(value, 'base64'));
|
|
151
|
+
const bin = atob(value);
|
|
152
|
+
const out = new Uint8Array(bin.length);
|
|
153
|
+
for (let i = 0; i < bin.length; i += 1)
|
|
154
|
+
out[i] = bin.charCodeAt(i);
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw new Error('[fabric-harness] Attachment "data" is not valid base64.');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function viewToBuffer(bytes) {
|
|
162
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=attachment-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-store.js","sourceRoot":"","sources":["../src/attachment-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { DeliveredMessage } from './delivered-message.js';
|
|
2
|
+
import type { DispatchReceipt } from './dispatch.js';
|
|
3
|
+
import type { FabricActor } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Channels turn platform webhooks (Slack, GitHub, …) into agent dispatches. Handlers are written
|
|
6
|
+
* against the Web `Request`/`Response` API and `crypto.subtle`, so the same channel runs on Node and
|
|
7
|
+
* Cloudflare. A channel is a stateless route container plus a conversation-id (de)serializer —
|
|
8
|
+
* session continuity falls out of the key (same thread → same key → same session).
|
|
9
|
+
*/
|
|
10
|
+
export interface ChannelRoute {
|
|
11
|
+
/** HTTP method, uppercase (e.g. "POST"). */
|
|
12
|
+
method: string;
|
|
13
|
+
/** Absolute path suffix mounted under `/channels/:name` (e.g. "/events"). */
|
|
14
|
+
path: string;
|
|
15
|
+
handler: (request: Request, context: ChannelContext) => Promise<Response> | Response;
|
|
16
|
+
}
|
|
17
|
+
export interface Channel {
|
|
18
|
+
/** Optional stable name; the mount name usually comes from the file/registration. */
|
|
19
|
+
readonly name?: string;
|
|
20
|
+
readonly routes: readonly ChannelRoute[];
|
|
21
|
+
/** Serialize a platform reference (e.g. a Slack thread) into a stable instance id. */
|
|
22
|
+
conversationKey(ref: unknown): string;
|
|
23
|
+
/** Parse an instance id back into the platform reference. */
|
|
24
|
+
parseConversationKey(id: string): unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface ChannelDispatchRequest {
|
|
27
|
+
/** Target persistent-agent instance id — typically `channel.conversationKey(ref)`. */
|
|
28
|
+
instanceId: string;
|
|
29
|
+
session?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The message delivered to the session. Channels should construct signals
|
|
32
|
+
* (`kind: 'signal'`) for platform activity; preferred over `input`.
|
|
33
|
+
*/
|
|
34
|
+
message?: DeliveredMessage;
|
|
35
|
+
/** Legacy JSON payload. Normalized into a user-kind message at admission. */
|
|
36
|
+
input?: unknown;
|
|
37
|
+
/** Stable key (e.g. the platform event id) → `dispatchId` for exactly-once delivery. */
|
|
38
|
+
dedupeKey?: string;
|
|
39
|
+
tenantId?: string;
|
|
40
|
+
actor?: FabricActor;
|
|
41
|
+
}
|
|
42
|
+
export type ChannelDispatch = (agent: string, request: ChannelDispatchRequest) => Promise<DispatchReceipt>;
|
|
43
|
+
export interface ChannelContext {
|
|
44
|
+
/** Dispatch to a persistent agent. Provided by the runtime mounting the channel. */
|
|
45
|
+
dispatch: ChannelDispatch;
|
|
46
|
+
env: Record<string, string | undefined>;
|
|
47
|
+
}
|
|
48
|
+
/** Validates and brands a channel's routes. */
|
|
49
|
+
export declare function defineChannel(channel: Channel): Channel;
|
|
50
|
+
export declare function hmacSha256(secret: string | Uint8Array, message: Uint8Array): Promise<Uint8Array>;
|
|
51
|
+
/** Constant-time HMAC-SHA256 verification (via `crypto.subtle.verify`). */
|
|
52
|
+
export declare function verifyHmacSha256(secret: string | Uint8Array, message: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
|
53
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
54
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
55
|
+
export declare function conversationKey(provider: string, version: string, ...segments: string[]): string;
|
|
56
|
+
export interface ParsedConversationKey {
|
|
57
|
+
provider: string;
|
|
58
|
+
version: string;
|
|
59
|
+
segments: string[];
|
|
60
|
+
}
|
|
61
|
+
export declare function parseConversationKey(key: string): ParsedConversationKey;
|
|
62
|
+
/**
|
|
63
|
+
* Reads the full request body as bytes, or returns undefined if it exceeds `limitBytes`.
|
|
64
|
+
*
|
|
65
|
+
* NOTE: this consumes the request stream (single read). Signature-verifying channels need the *exact*
|
|
66
|
+
* bytes for HMAC and the parsed JSON afterward — don't call `request.json()` as well. Use
|
|
67
|
+
* {@link readJsonBody} to get both from one read.
|
|
68
|
+
*/
|
|
69
|
+
export declare function readRequestBody(request: Request, limitBytes?: number): Promise<Uint8Array | undefined>;
|
|
70
|
+
export interface RequestBody {
|
|
71
|
+
/** Exact bytes — use these (or `text`) for signature verification. */
|
|
72
|
+
raw: Uint8Array;
|
|
73
|
+
/** UTF-8 decode of `raw`. */
|
|
74
|
+
text: string;
|
|
75
|
+
/** Parsed JSON, or `undefined` if the body isn't valid JSON. */
|
|
76
|
+
json: unknown;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads the body once and returns the raw bytes, the decoded text, and the parsed JSON together — so a
|
|
80
|
+
* channel can HMAC-verify the exact bytes and use the JSON without re-reading the (already consumed)
|
|
81
|
+
* stream. Returns undefined only when the body exceeds `limitBytes`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function readJsonBody(request: Request, limitBytes?: number): Promise<RequestBody | undefined>;
|
|
84
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const METHOD_RE = /^[A-Z]+$/;
|
|
2
|
+
/** Validates and brands a channel's routes. */
|
|
3
|
+
export function defineChannel(channel) {
|
|
4
|
+
if (!Array.isArray(channel.routes) || channel.routes.length === 0) {
|
|
5
|
+
throw new Error('defineChannel: a channel must declare at least one route.');
|
|
6
|
+
}
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
for (const route of channel.routes) {
|
|
9
|
+
if (!METHOD_RE.test(route.method))
|
|
10
|
+
throw new Error(`defineChannel: route method "${route.method}" must be uppercase letters.`);
|
|
11
|
+
if (!route.path.startsWith('/') || route.path.startsWith('//') || route.path.includes('?') || route.path.includes('#')) {
|
|
12
|
+
throw new Error(`defineChannel: route path "${route.path}" must be an absolute suffix without query or fragment.`);
|
|
13
|
+
}
|
|
14
|
+
const key = `${route.method} ${route.path}`;
|
|
15
|
+
if (seen.has(key))
|
|
16
|
+
throw new Error(`defineChannel: duplicate route "${key}".`);
|
|
17
|
+
seen.add(key);
|
|
18
|
+
}
|
|
19
|
+
return channel;
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Web-standard crypto helpers (HMAC-SHA256) — run identically on Node and edge.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
function toBytes(value) {
|
|
26
|
+
return typeof value === 'string' ? encoder.encode(value) : value;
|
|
27
|
+
}
|
|
28
|
+
function viewToBuffer(bytes) {
|
|
29
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
30
|
+
}
|
|
31
|
+
export async function hmacSha256(secret, message) {
|
|
32
|
+
const key = await crypto.subtle.importKey('raw', viewToBuffer(toBytes(secret)), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
33
|
+
const signature = await crypto.subtle.sign('HMAC', key, viewToBuffer(message));
|
|
34
|
+
return new Uint8Array(signature);
|
|
35
|
+
}
|
|
36
|
+
/** Constant-time HMAC-SHA256 verification (via `crypto.subtle.verify`). */
|
|
37
|
+
export async function verifyHmacSha256(secret, message, signature) {
|
|
38
|
+
try {
|
|
39
|
+
const key = await crypto.subtle.importKey('raw', viewToBuffer(toBytes(secret)), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
40
|
+
return await crypto.subtle.verify('HMAC', key, viewToBuffer(signature), viewToBuffer(message));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function bytesToHex(bytes) {
|
|
47
|
+
let hex = '';
|
|
48
|
+
for (const byte of bytes)
|
|
49
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
50
|
+
return hex;
|
|
51
|
+
}
|
|
52
|
+
export function hexToBytes(hex) {
|
|
53
|
+
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
54
|
+
if (clean.length % 2 !== 0)
|
|
55
|
+
throw new Error('hexToBytes: odd-length hex string.');
|
|
56
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
57
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
58
|
+
bytes[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Conversation key encoding — `provider:version:seg1:seg2:...` with url-safe segments.
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
export function conversationKey(provider, version, ...segments) {
|
|
65
|
+
if (provider.includes(':') || version.includes(':'))
|
|
66
|
+
throw new Error('conversationKey: provider/version must not contain ":".');
|
|
67
|
+
return [provider, version, ...segments.map(encodeURIComponent)].join(':');
|
|
68
|
+
}
|
|
69
|
+
export function parseConversationKey(key) {
|
|
70
|
+
const parts = key.split(':');
|
|
71
|
+
if (parts.length < 2)
|
|
72
|
+
throw new Error(`parseConversationKey: invalid key "${key}".`);
|
|
73
|
+
const [provider, version, ...rest] = parts;
|
|
74
|
+
return { provider, version, segments: rest.map(decodeURIComponent) };
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Request body — read with a size guard (signature verification needs exact bytes).
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
const DEFAULT_BODY_LIMIT = 1_000_000;
|
|
80
|
+
/**
|
|
81
|
+
* Reads the full request body as bytes, or returns undefined if it exceeds `limitBytes`.
|
|
82
|
+
*
|
|
83
|
+
* NOTE: this consumes the request stream (single read). Signature-verifying channels need the *exact*
|
|
84
|
+
* bytes for HMAC and the parsed JSON afterward — don't call `request.json()` as well. Use
|
|
85
|
+
* {@link readJsonBody} to get both from one read.
|
|
86
|
+
*/
|
|
87
|
+
export async function readRequestBody(request, limitBytes = DEFAULT_BODY_LIMIT) {
|
|
88
|
+
const contentLength = request.headers.get('content-length');
|
|
89
|
+
if (contentLength && Number(contentLength) > limitBytes)
|
|
90
|
+
return undefined;
|
|
91
|
+
const buffer = new Uint8Array(await request.arrayBuffer());
|
|
92
|
+
if (buffer.byteLength > limitBytes)
|
|
93
|
+
return undefined;
|
|
94
|
+
return buffer;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Reads the body once and returns the raw bytes, the decoded text, and the parsed JSON together — so a
|
|
98
|
+
* channel can HMAC-verify the exact bytes and use the JSON without re-reading the (already consumed)
|
|
99
|
+
* stream. Returns undefined only when the body exceeds `limitBytes`.
|
|
100
|
+
*/
|
|
101
|
+
export async function readJsonBody(request, limitBytes = DEFAULT_BODY_LIMIT) {
|
|
102
|
+
const raw = await readRequestBody(request, limitBytes);
|
|
103
|
+
if (!raw)
|
|
104
|
+
return undefined;
|
|
105
|
+
const text = new TextDecoder().decode(raw);
|
|
106
|
+
let json;
|
|
107
|
+
try {
|
|
108
|
+
json = JSON.parse(text);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
json = undefined;
|
|
112
|
+
}
|
|
113
|
+
return { raw, text, json };
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/context-budget.d.ts
CHANGED
|
@@ -6,15 +6,31 @@ export interface ContextBudget {
|
|
|
6
6
|
reserveTokens: number;
|
|
7
7
|
compactAtTokens?: number;
|
|
8
8
|
shouldCompact: boolean;
|
|
9
|
+
keepRecentTokens?: number;
|
|
9
10
|
}
|
|
10
11
|
export interface ContextBudgetOptions {
|
|
11
12
|
contextWindowTokens?: number;
|
|
12
13
|
compactAtTokens?: number;
|
|
13
14
|
reserveTokens?: number;
|
|
15
|
+
keepRecentTokens?: number;
|
|
14
16
|
}
|
|
15
17
|
export declare function estimateTextTokens(text: string): number;
|
|
16
18
|
export declare function estimateModelMessagesTokens(messages: ModelMessage[]): number;
|
|
19
|
+
export declare function estimateSessionEntryTokens(entry: SessionEntry): number;
|
|
17
20
|
export declare function estimateSessionEntriesTokens(entries: SessionEntry[]): number;
|
|
21
|
+
/**
|
|
22
|
+
* Compute model-aware compaction defaults. Reserve is capped at the model's
|
|
23
|
+
* max output because reserving more than the model can emit in one turn
|
|
24
|
+
* wastes context; the preserved tail stays flat because recent-context
|
|
25
|
+
* fidelity depends on the active work, not on the model's total window size.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deriveCompactionDefaults(input: {
|
|
28
|
+
contextWindowTokens: number;
|
|
29
|
+
maxOutputTokens?: number;
|
|
30
|
+
}): {
|
|
31
|
+
reserveTokens: number;
|
|
32
|
+
keepRecentTokens: number;
|
|
33
|
+
};
|
|
18
34
|
export declare function evaluateContextBudget(messages: ModelMessage[], options?: ContextBudgetOptions): ContextBudget;
|
|
19
35
|
export declare function isContextOverflowError(error: unknown): boolean;
|
|
20
36
|
//# sourceMappingURL=context-budget.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../src/context-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../src/context-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAKD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,CAE5E;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAEtE;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAE5E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAatD;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAuBjH;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG9D"}
|
package/dist/context-budget.js
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
|
-
const DEFAULT_RESERVE_TOKENS =
|
|
1
|
+
const DEFAULT_RESERVE_TOKENS = 20_000;
|
|
2
|
+
const DEFAULT_KEEP_RECENT_TOKENS = 8_000;
|
|
2
3
|
export function estimateTextTokens(text) {
|
|
3
4
|
return Math.ceil(text.length / 4);
|
|
4
5
|
}
|
|
5
6
|
export function estimateModelMessagesTokens(messages) {
|
|
6
7
|
return messages.reduce((sum, message) => sum + estimateTextTokens(message.content) + 8, 0);
|
|
7
8
|
}
|
|
9
|
+
export function estimateSessionEntryTokens(entry) {
|
|
10
|
+
return estimateTextTokens(JSON.stringify({ type: entry.type, data: entry.data ?? {} })) + 8;
|
|
11
|
+
}
|
|
8
12
|
export function estimateSessionEntriesTokens(entries) {
|
|
9
|
-
return entries.reduce((sum, entry) => sum +
|
|
13
|
+
return entries.reduce((sum, entry) => sum + estimateSessionEntryTokens(entry), 0);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Compute model-aware compaction defaults. Reserve is capped at the model's
|
|
17
|
+
* max output because reserving more than the model can emit in one turn
|
|
18
|
+
* wastes context; the preserved tail stays flat because recent-context
|
|
19
|
+
* fidelity depends on the active work, not on the model's total window size.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveCompactionDefaults(input) {
|
|
22
|
+
const reserveCap = input.maxOutputTokens !== undefined && input.maxOutputTokens > 0
|
|
23
|
+
? input.maxOutputTokens
|
|
24
|
+
: DEFAULT_RESERVE_TOKENS;
|
|
25
|
+
let reserveTokens = Math.min(DEFAULT_RESERVE_TOKENS, reserveCap);
|
|
26
|
+
if (input.contextWindowTokens > 0 && reserveTokens * 2 >= input.contextWindowTokens) {
|
|
27
|
+
reserveTokens = Math.max(1024, Math.floor(input.contextWindowTokens / 3));
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
reserveTokens,
|
|
31
|
+
keepRecentTokens: DEFAULT_KEEP_RECENT_TOKENS,
|
|
32
|
+
};
|
|
10
33
|
}
|
|
11
34
|
export function evaluateContextBudget(messages, options = {}) {
|
|
12
35
|
const estimatedTokens = estimateModelMessagesTokens(messages);
|
|
13
|
-
|
|
36
|
+
let reserveTokens;
|
|
37
|
+
if (options.reserveTokens !== undefined) {
|
|
38
|
+
reserveTokens = options.reserveTokens;
|
|
39
|
+
}
|
|
40
|
+
else if (options.contextWindowTokens !== undefined) {
|
|
41
|
+
const defaults = deriveCompactionDefaults({ contextWindowTokens: options.contextWindowTokens });
|
|
42
|
+
reserveTokens = defaults.reserveTokens;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
reserveTokens = DEFAULT_RESERVE_TOKENS;
|
|
46
|
+
}
|
|
47
|
+
const keepRecentTokens = options.keepRecentTokens ?? DEFAULT_KEEP_RECENT_TOKENS;
|
|
14
48
|
const compactAtTokens = options.compactAtTokens ?? (options.contextWindowTokens ? Math.max(0, options.contextWindowTokens - reserveTokens) : undefined);
|
|
15
49
|
return {
|
|
16
50
|
estimatedTokens,
|
|
@@ -18,6 +52,7 @@ export function evaluateContextBudget(messages, options = {}) {
|
|
|
18
52
|
...(options.contextWindowTokens !== undefined ? { contextWindowTokens: options.contextWindowTokens } : {}),
|
|
19
53
|
...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
|
|
20
54
|
shouldCompact: compactAtTokens !== undefined && estimatedTokens >= compactAtTokens,
|
|
55
|
+
keepRecentTokens,
|
|
21
56
|
};
|
|
22
57
|
}
|
|
23
58
|
export function isContextOverflowError(error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-budget.js","sourceRoot":"","sources":["../src/context-budget.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"context-budget.js","sourceRoot":"","sources":["../src/context-budget.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ConversationStreamStore } from './conversation-stream.js';
|
|
2
|
+
import type { SessionData, SessionEntry, SessionStore } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wrap a {@link SessionStore} so active-path appends are mirrored into an
|
|
5
|
+
* append-only {@link ConversationStreamStore} projection (v2 A4).
|
|
6
|
+
*
|
|
7
|
+
* The SessionEntry DAG stays the single source of truth; the stream gives
|
|
8
|
+
* clients offset-based catch-up + live tail. Two rules keep them coherent:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Idempotent by entryId** — a crash between the DAG write and the
|
|
11
|
+
* stream write is repaired on the next append: the projector diffs the
|
|
12
|
+
* stream tail against the active path and re-emits anything missing.
|
|
13
|
+
* 2. **Truncation is explicit** — the DAG can branch (fork/replay/
|
|
14
|
+
* checkpoint-restore rewrite `leafId`); the stream cannot. When `save()`
|
|
15
|
+
* rewinds the active path, the projector appends a `truncated` record
|
|
16
|
+
* and re-acquires the producer (epoch bump), so tailing clients see the
|
|
17
|
+
* branch switch instead of silently reading a stale path.
|
|
18
|
+
*
|
|
19
|
+
* Projection failures are logged and swallowed — the conversation stream is
|
|
20
|
+
* an observability surface and must never fail the session write itself;
|
|
21
|
+
* the diff-repair on the next append restores consistency.
|
|
22
|
+
*/
|
|
23
|
+
export declare function withConversationProjection(store: SessionStore, streams: ConversationStreamStore, options?: {
|
|
24
|
+
producerId?: string;
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
}): SessionStore;
|
|
27
|
+
export declare class ConversationProjector {
|
|
28
|
+
private readonly streams;
|
|
29
|
+
private readonly producerId;
|
|
30
|
+
private readonly onError;
|
|
31
|
+
private readonly producers;
|
|
32
|
+
constructor(streams: ConversationStreamStore, options?: {
|
|
33
|
+
producerId?: string;
|
|
34
|
+
onError?: (error: unknown) => void;
|
|
35
|
+
});
|
|
36
|
+
/** Mirror one appended entry, repairing any missed backlog first. */
|
|
37
|
+
projectAppend(store: SessionStore, sessionId: string, entry: SessionEntry): Promise<void>;
|
|
38
|
+
/** Handle whole-document saves (fork/replay/truncation rewrite the path). */
|
|
39
|
+
projectSave(data: SessionData): Promise<void>;
|
|
40
|
+
deleteStream(sessionId: string): Promise<void>;
|
|
41
|
+
private ensureProducer;
|
|
42
|
+
/** Fold the stream into the currently-projected entry ids (truncations rewind). */
|
|
43
|
+
private readProjectedEntryIds;
|
|
44
|
+
/**
|
|
45
|
+
* Diff the projected tail against the current active path. A pure
|
|
46
|
+
* extension appends the missing suffix; a divergence appends a
|
|
47
|
+
* `truncated` record (with a fresh producer epoch) then the new suffix.
|
|
48
|
+
*/
|
|
49
|
+
private reproject;
|
|
50
|
+
private appendRecords;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=conversation-projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-projection.d.ts","sourceRoot":"","sources":["../src/conversation-projection.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,uBAAuB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,uBAAuB,EAChC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CAAO,GACxE,YAAY,CAyBd;AAUD,qBAAa,qBAAqB;IAM9B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoC;gBAG3C,OAAO,EAAE,uBAAuB,EACjD,OAAO,GAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;KAAO;IAQ3E,qEAAqE;IAC/D,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB/F,6EAA6E;IACvE,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAS7C,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAStC,cAAc;IAmB5B,mFAAmF;YACrE,qBAAqB;IAoBnC;;;;OAIG;YACW,SAAS;YAgCT,aAAa;CA6B5B"}
|