@trylucy/agent-memory 0.0.0-stage → 0.1.2

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 (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +205 -2
  3. package/contracts/agent-manifest.json +27 -0
  4. package/contracts/agent-memory-api.contract.json +199 -0
  5. package/contracts/hashes.json +23 -0
  6. package/package.json +84 -4
  7. package/schemas/canonical-recall/response.schema.json +56 -0
  8. package/schemas/v1/common.schema.json +190 -0
  9. package/schemas/v1/contract-document.schema.json +277 -0
  10. package/schemas/v1/create-proposal-request.schema.json +42 -0
  11. package/schemas/v1/error.schema.json +90 -0
  12. package/schemas/v1/memory-version-state.schema.json +39 -0
  13. package/schemas/v1/memory.schema.json +47 -0
  14. package/schemas/v1/proposal-status.schema.json +277 -0
  15. package/schemas/v1/public-surface.schema.json +26 -0
  16. package/schemas/v1/recall-request.schema.json +45 -0
  17. package/schemas/v1/recall-response.schema.json +60 -0
  18. package/schemas/v1/receipt.schema.json +67 -0
  19. package/schemas/v1/replace-proposal-request.schema.json +46 -0
  20. package/schemas/v1/replace-transition.schema.json +19 -0
  21. package/schemas/v1/scope.schema.json +10 -0
  22. package/scripts/lib/am01.mjs +159 -0
  23. package/src/client/canonical-validation.mjs +37 -0
  24. package/src/client/client.mjs +151 -0
  25. package/src/client/errors.mjs +101 -0
  26. package/src/client/http-transport.d.ts +12 -0
  27. package/src/client/http-transport.mjs +212 -0
  28. package/src/client/index.d.ts +243 -0
  29. package/src/client/index.mjs +38 -0
  30. package/src/client/serialization.mjs +53 -0
  31. package/src/client/validation.mjs +174 -0
  32. package/src/clients/claude-desktop.json +13 -0
  33. package/src/clients/claude.json +13 -0
  34. package/src/clients/codex.toml +8 -0
  35. package/src/clients/cursor.json +12 -0
  36. package/src/clients/generic.mcp.json +12 -0
  37. package/src/clients/openclaw.json +14 -0
  38. package/src/clients/vscode.json +13 -0
  39. package/src/integrations/hermes-advisory-lock.mjs +140 -0
  40. package/src/integrations/hermes-artifact.mjs +551 -0
  41. package/src/integrations/hermes-filesystem.mjs +112 -0
  42. package/src/integrations/hermes.d.ts +106 -0
  43. package/src/integrations/hermes.mjs +425 -0
  44. package/src/mcp/bin.mjs +4 -0
  45. package/src/mcp/config.mjs +60 -0
  46. package/src/mcp/index.d.ts +27 -0
  47. package/src/mcp/lifecycle.mjs +50 -0
  48. package/src/mcp/schemas.mjs +98 -0
  49. package/src/mcp/server.mjs +163 -0
  50. package/src/mcp/stdio.d.ts +28 -0
  51. package/src/mcp/stdio.mjs +59 -0
  52. package/src/onboarding/usage.mjs +44 -0
  53. package/src/projection/memory-md.mjs +319 -0
  54. package/src/provider/index.d.ts +9 -0
  55. package/src/provider/index.mjs +1 -0
  56. package/src/provider/memory-first.mjs +95 -0
  57. package/src/routing/destination.mjs +42 -0
  58. package/src/safety/secrets.mjs +31 -0
@@ -0,0 +1,46 @@
1
+ {
2
+ "$id": "https://schemas.trylucy.ai/agent-memory/v1/replace-proposal-request.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "content": {
7
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/content"
8
+ },
9
+ "expected_version": {
10
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/expectedVersion"
11
+ },
12
+ "idempotency_key": {
13
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/idempotencyKey"
14
+ },
15
+ "justification": {
16
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/justification"
17
+ },
18
+ "key": {
19
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/key"
20
+ },
21
+ "memory_id": {
22
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/memoryId"
23
+ },
24
+ "operation": {
25
+ "const": "replace"
26
+ },
27
+ "scope": {
28
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/scope"
29
+ },
30
+ "source_ref": {
31
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/sourceRef"
32
+ }
33
+ },
34
+ "required": [
35
+ "operation",
36
+ "memory_id",
37
+ "expected_version",
38
+ "key",
39
+ "scope",
40
+ "content",
41
+ "justification",
42
+ "idempotency_key"
43
+ ],
44
+ "title": "Replace memory proposal request",
45
+ "type": "object"
46
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$id": "https://schemas.trylucy.ai/agent-memory/v1/replace-transition.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "current": {
7
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/memory.schema.json"
8
+ },
9
+ "proposal": {
10
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/replace-proposal-request.schema.json"
11
+ }
12
+ },
13
+ "required": [
14
+ "current",
15
+ "proposal"
16
+ ],
17
+ "title": "Replacement transition validation envelope",
18
+ "type": "object"
19
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$id": "https://schemas.trylucy.ai/agent-memory/v1/scope.schema.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ {
6
+ "$ref": "https://schemas.trylucy.ai/agent-memory/v1/common.schema.json#/$defs/scope"
7
+ }
8
+ ],
9
+ "title": "Memory scope"
10
+ }
@@ -0,0 +1,159 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHash } from "node:crypto";
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import Ajv2020 from "ajv/dist/2020.js";
7
+ import addFormats from "ajv-formats";
8
+
9
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
10
+ export const CONTRACT_PATH = "contracts/agent-memory-api.contract.json";
11
+ export const EXPECTED_CONTRACT_VERSION = "2026-08-09";
12
+ export const EXPECTED_SCHEMA_VERSION = "1";
13
+ export const SCHEMA_BASE = "https://schemas.trylucy.ai/agent-memory/v1/";
14
+
15
+ export async function loadJson(relativePath) {
16
+ return JSON.parse(await readFile(path.join(ROOT, relativePath), "utf8"));
17
+ }
18
+ export async function loadContract() { return loadJson(CONTRACT_PATH); }
19
+ export async function loadFixtureManifest() { return loadJson("fixtures/v1/manifest.json"); }
20
+ async function filesUnder(relativeDir, suffix) {
21
+ const dir = path.join(ROOT, relativeDir);
22
+ const names = await readdir(dir, { recursive: true });
23
+ return names
24
+ .filter((name) => name.endsWith(suffix))
25
+ .map((name) => name.split(path.sep).join("/"))
26
+ .sort()
27
+ .map((name) => path.posix.join(relativeDir, name));
28
+ }
29
+ export async function compilePublicSchemas() {
30
+ const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: true });
31
+ addFormats(ajv);
32
+ const paths = await filesUnder("schemas/v1", ".schema.json");
33
+ const schemas = await Promise.all(paths.map(loadJson));
34
+ for (const schema of schemas) ajv.addSchema(schema);
35
+ for (const schema of schemas) ajv.getSchema(schema.$id);
36
+ return { ajv, schemaCount: schemas.length, paths };
37
+ }
38
+ function scopesEqual(left, right) {
39
+ if (!left || !right || left.kind !== right.kind) return false;
40
+ return left.id === right.id;
41
+ }
42
+ function semanticError(keyword, instancePath) {
43
+ return { instancePath, keyword, schemaPath: `#/semantic/${keyword}` };
44
+ }
45
+ export function isExpectedVersionSuccessor(expectedVersion, memoryVersion) {
46
+ return Number.isSafeInteger(expectedVersion)
47
+ && expectedVersion >= 1
48
+ && expectedVersion < Number.MAX_SAFE_INTEGER
49
+ && Number.isSafeInteger(memoryVersion)
50
+ && memoryVersion === Number(BigInt(expectedVersion) + 1n);
51
+ }
52
+ function memoryReceiptErrors(value, basePath = "") {
53
+ if (!value?.receipt) return [];
54
+ const checks = [
55
+ ["receiptMemoryId", `${basePath}/receipt/memory_id`, value.id === value.receipt.memory_id],
56
+ ["receiptVersion", `${basePath}/receipt/memory_version`, value.version === value.receipt.memory_version],
57
+ ["receiptKey", `${basePath}/receipt/key`, value.key === value.receipt.key],
58
+ ["receiptKind", `${basePath}/receipt/kind`, value.kind === value.receipt.kind],
59
+ ["receiptScope", `${basePath}/receipt/scope`, scopesEqual(value.scope, value.receipt.scope)],
60
+ ["receiptLifecycle", `${basePath}/receipt/lifecycle`, value.status === value.receipt.lifecycle]
61
+ ];
62
+ return checks.filter(([, , valid]) => !valid).map(([keyword, instancePath]) => semanticError(keyword, instancePath));
63
+ }
64
+ function proposalStatusErrors(value) {
65
+ if (value.status !== "approved" || !value.result) return [];
66
+ const errors = [];
67
+ if (value.operation === "create" && value.result.memory_version !== 1) {
68
+ errors.push(semanticError("proposalResultVersion", "/result/memory_version"));
69
+ }
70
+ if (value.operation === "replace") {
71
+ if (value.result.memory_id !== value.requested.memory_id) {
72
+ errors.push(semanticError("proposalResultMemoryId", "/result/memory_id"));
73
+ }
74
+ if (!isExpectedVersionSuccessor(value.requested.expected_version, value.result.memory_version)) {
75
+ errors.push(semanticError("proposalResultVersion", "/result/memory_version"));
76
+ }
77
+ }
78
+ return errors;
79
+ }
80
+ function semanticErrors(schemaId, value) {
81
+ if (schemaId === `${SCHEMA_BASE}memory.schema.json`) return memoryReceiptErrors(value);
82
+ if (schemaId === `${SCHEMA_BASE}proposal-status.schema.json`) return proposalStatusErrors(value);
83
+ if (schemaId === `${SCHEMA_BASE}recall-response.schema.json`) {
84
+ return (value.results ?? []).flatMap((memory, index) => [
85
+ ...memoryReceiptErrors(memory, `/results/${index}`),
86
+ ...(scopesEqual(value.scope, memory.scope)
87
+ ? []
88
+ : [semanticError("recallScope", `/results/${index}/scope`)])
89
+ ]);
90
+ }
91
+ if (schemaId !== `${SCHEMA_BASE}replace-transition.schema.json`) return [];
92
+ const errors = [...memoryReceiptErrors(value.current, "/current")];
93
+ if (value.current.id !== value.proposal.memory_id) errors.push(semanticError("stableMemoryId", "/proposal/memory_id"));
94
+ if (value.current.version !== value.proposal.expected_version) errors.push(semanticError("expectedVersion", "/proposal/expected_version"));
95
+ if (value.current.key !== value.proposal.key) errors.push(semanticError("stableKey", "/proposal/key"));
96
+ if (!scopesEqual(value.current.scope, value.proposal.scope)) errors.push(semanticError("stableScope", "/proposal/scope"));
97
+ return errors;
98
+ }
99
+ export async function validateFixture(registry, fixture) {
100
+ const value = await loadJson(fixture.file);
101
+ const validate = registry.ajv.getSchema(fixture.schema);
102
+ assert.ok(validate, `unknown fixture schema: ${fixture.schema}`);
103
+ const structural = validate(value);
104
+ const errors = [
105
+ ...(validate.errors ?? []).map(({ instancePath, keyword, params, schemaPath }) => ({ instancePath, keyword, params, schemaPath })),
106
+ ...semanticErrors(fixture.schema, value)
107
+ ];
108
+ return {
109
+ valid: Boolean(structural) && errors.length === 0,
110
+ keywords: errors.map(({ keyword }) => keyword),
111
+ errors,
112
+ message: JSON.stringify(errors)
113
+ };
114
+ }
115
+ export async function verifyFrozenContract(contract) {
116
+ contract ??= await loadContract();
117
+ assert.equal(contract.contract_version, EXPECTED_CONTRACT_VERSION);
118
+ assert.equal(contract.public_schema_version, EXPECTED_SCHEMA_VERSION);
119
+ assert.deepEqual(contract.public_permissions, ["memory:read", "memory:propose"]);
120
+ assert.deepEqual(contract.mcp_tools, ["memory_recall", "memory_propose", "memory_proposal_status"]);
121
+ assert.equal(contract.identity.account_source, "server-derived");
122
+ assert.equal(contract.identity.tenant_source, "server-derived");
123
+ assert.equal(contract.identity.client_may_supply_account, false);
124
+ assert.equal(contract.identity.client_may_supply_tenant, false);
125
+ assert.equal(contract.identity.maximum_active_per_conflict_identity, 1);
126
+ assert.equal(contract.create.may_replace_active, false);
127
+ assert.equal(contract.create.approved_memory_version, 1);
128
+ assert.equal(contract.replace.retains_memory_id, true);
129
+ assert.equal(contract.replace.expected_version_maximum, Number.MAX_SAFE_INTEGER - 1);
130
+ assert.equal(contract.replace.maximum_memory_version, Number.MAX_SAFE_INTEGER);
131
+ assert.equal(contract.replace.new_version_rule, "expected_version + 1");
132
+ assert.equal(contract.proposal.payload_immutable_after_acceptance, true);
133
+ assert.equal(contract.proposal.proposal_supersession_distinct_from_memory_version_supersession, true);
134
+ assert.equal(contract.recall.canonical_active_state_rejoin_required, true);
135
+ assert.equal(contract.recall.asynchronous_index_is_not_authority, true);
136
+ assert.equal(contract.receipts.version_specific, true);
137
+ assert.equal(contract.receipts.authority_types.length, 1);
138
+ assert.equal(contract.receipts.authority_types[0], "human_approved");
139
+ assert.equal(contract.forgetting.physical_deletion_everywhere_promised, false);
140
+ assert.ok(contract.rule_non_authorities.includes("approval authority"));
141
+ assert.ok(contract.forbidden_public_permissions.includes("memory:write"));
142
+ assert.ok(contract.forbidden_mcp_capabilities.includes("memory_resolve"));
143
+ return true;
144
+ }
145
+ function digest(buffer) { return createHash("sha256").update(buffer).digest("hex"); }
146
+ export async function currentHashes() {
147
+ const schemaPaths = await filesUnder("schemas/v1", ".schema.json");
148
+ const contractBytes = await readFile(path.join(ROOT, CONTRACT_PATH));
149
+ const schemas = {};
150
+ for (const schemaPath of schemaPaths) schemas[schemaPath] = digest(await readFile(path.join(ROOT, schemaPath)));
151
+ return { algorithm: "sha256", contract: { path: CONTRACT_PATH, sha256: digest(contractBytes) }, schemas };
152
+ }
153
+ export async function verifyHashes() {
154
+ const expected = await loadJson("contracts/hashes.json");
155
+ const actual = await currentHashes();
156
+ const valid = JSON.stringify(expected) === JSON.stringify(actual);
157
+ return { valid, message: valid ? "hashes verified" : `hash mismatch\nexpected=${JSON.stringify(expected)}\nactual=${JSON.stringify(actual)}` };
158
+ }
159
+ export { ROOT };
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import Ajv2020 from 'ajv/dist/2020.js';
4
+ import addFormats from 'ajv-formats';
5
+ import { MalformedResponseError } from './errors.mjs';
6
+
7
+ const schemaPath = fileURLToPath(new URL('../../schemas/canonical-recall/response.schema.json', import.meta.url));
8
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
9
+ const ajv = new Ajv2020({ strict: true, allErrors: true });
10
+ addFormats(ajv);
11
+ const validate = ajv.compile(schema);
12
+
13
+ function matchesScope(left, right) { return left?.kind === right?.kind && left?.id === right?.id; }
14
+
15
+ export function canonicalRecallOutputSchema() { return structuredClone(schema); }
16
+
17
+ export function validateCanonicalRecallResponse(value, request) {
18
+ if (!validate(value)) {
19
+ const details = validate.errors?.map(({ instancePath, keyword, schemaPath: path }) => ({ instancePath, keyword, schemaPath: path }));
20
+ throw new MalformedResponseError('Canonical recall response violates the strict canonical schema.', { details });
21
+ }
22
+ if (!request || !matchesScope(value.scope, request.scope)) throw new MalformedResponseError('Canonical recall response scope does not match the request.');
23
+ if (value.results.length > (request.limit ?? 3)) throw new MalformedResponseError('Canonical recall response exceeds the requested limit.');
24
+ for (const result of value.results) {
25
+ if (!matchesScope(result.scope, value.scope) || !matchesScope(result.scope, request.scope)
26
+ || result.provenance.record_id !== result.record_id) {
27
+ throw new MalformedResponseError('Canonical recall response violates scope or provenance record binding.');
28
+ }
29
+ if (request.keys && (result.key === null || !request.keys.includes(result.key))) {
30
+ throw new MalformedResponseError('Canonical recall response violates the requested key filter.');
31
+ }
32
+ if (request.kinds && !request.kinds.includes(result.kind)) {
33
+ throw new MalformedResponseError('Canonical recall response violates the requested kind filter.');
34
+ }
35
+ }
36
+ return value;
37
+ }
@@ -0,0 +1,151 @@
1
+ import {
2
+ AgentMemoryError, AuthenticationFailureError, AuthorizationFailureError, InternalLucyError, InvalidScopeError,
3
+ LifecycleConflictError, MalformedResponseError, MemoryNotFoundError, ProposalNotFoundError, RateLimitError,
4
+ TimeoutError, UpstreamUnavailableError, ValidationFailureError, VersionConflictError,
5
+ InsufficientCreditsError, BillingUnavailableError,
6
+ } from './errors.mjs';
7
+ import { parseRequestJson, stableSerialize } from './serialization.mjs';
8
+ import {
9
+ toProposalRequestDto, toProposalStatusRequestDto, toRecallRequestDto, validateErrorResponse,
10
+ validateProposalResponse, validateRecallResponse,
11
+ } from './validation.mjs';
12
+ import { validateCanonicalRecallResponse } from './canonical-validation.mjs';
13
+
14
+ const REQUEST_LIMITS = Object.freeze({ recall: 8192, 'canonical-recall': 8192, propose: 12288 });
15
+ // Match the existing canonical recall request_id limit without changing the public error schema.
16
+ const MAX_PUBLIC_ERROR_REQUEST_ID_LENGTH = 256;
17
+ const PUBLIC_ERROR_REQUEST_ID = /^req_[A-Za-z0-9_-]+$/;
18
+ const MAPPED_ERROR_REQUEST_IDS = new WeakMap();
19
+
20
+ export function safePublicErrorRequestId(value) {
21
+ return typeof value === 'string'
22
+ && value.length <= MAX_PUBLIC_ERROR_REQUEST_ID_LENGTH
23
+ && PUBLIC_ERROR_REQUEST_ID.exec(value)?.[0] === value
24
+ ? value
25
+ : null;
26
+ }
27
+
28
+ export function mappedErrorRequestId(error) {
29
+ return safePublicErrorRequestId(MAPPED_ERROR_REQUEST_IDS.get(error));
30
+ }
31
+
32
+ function permissions(value) {
33
+ if (!Array.isArray(value) || value.some((item) => !['memory:read', 'memory:propose'].includes(item))) {
34
+ throw new ValidationFailureError('permissions may contain only memory:read and memory:propose.');
35
+ }
36
+ return new Set(value);
37
+ }
38
+ function responseMetadata(value) {
39
+ if (value === null || typeof value !== 'object' || Array.isArray(value)
40
+ || !Number.isInteger(value.status) || value.status < 100 || value.status > 599
41
+ || !Number.isSafeInteger(value.byteLength) || value.byteLength < 0 || !('body' in value)
42
+ || (value.retryAfterMs !== undefined && (!Number.isSafeInteger(value.retryAfterMs) || value.retryAfterMs < 0))) {
43
+ throw new MalformedResponseError('Transport returned invalid response metadata.');
44
+ }
45
+ let canonicalBytes;
46
+ try { canonicalBytes = Buffer.byteLength(stableSerialize(value.body)); }
47
+ catch { throw new MalformedResponseError('Transport returned a non-serializable response body.'); }
48
+ if (value.byteLength < canonicalBytes) throw new MalformedResponseError('Transport response byte length is inconsistent.');
49
+ return value;
50
+ }
51
+ function mapError(response, operation) {
52
+ validateErrorResponse(response.body);
53
+ const { code } = response.body.error;
54
+ const requestId = safePublicErrorRequestId(response.body.request_id);
55
+ const options = {
56
+ status: response.status,
57
+ details: requestId === null ? { lucyCode: code } : { lucyCode: code, request_id: requestId },
58
+ };
59
+ const correlate = (error) => {
60
+ if (requestId !== null) MAPPED_ERROR_REQUEST_IDS.set(error, requestId);
61
+ return error;
62
+ };
63
+ if (response.status === 401 || code === 'unauthenticated') return correlate(new AuthenticationFailureError(undefined, options));
64
+ if (response.status === 403 || code === 'forbidden') return correlate(new AuthorizationFailureError(undefined, options));
65
+ if (code === 'insufficient_credits' || response.status === 402) return correlate(new InsufficientCreditsError(undefined, options));
66
+ if (code === 'billing_unavailable') return correlate(new BillingUnavailableError(undefined, options));
67
+ if (code === 'invalid_scope') return correlate(new InvalidScopeError(undefined, options));
68
+ if (code === 'version_conflict') return correlate(new VersionConflictError(undefined, options));
69
+ if (code === 'conflict') return correlate(new LifecycleConflictError(undefined, options));
70
+ if (response.status === 404 || code === 'not_found_or_not_visible') {
71
+ return correlate(operation === 'proposal-status'
72
+ ? new ProposalNotFoundError(undefined, options)
73
+ : new MemoryNotFoundError(undefined, options));
74
+ }
75
+ if (response.status === 429 || code === 'rate_limited') return correlate(new RateLimitError(undefined, { ...options, retryAfterMs: response.retryAfterMs ?? null }));
76
+ if ([502, 503, 504].includes(response.status) || code === 'service_unavailable') return correlate(new UpstreamUnavailableError(undefined, options));
77
+ if (response.status >= 500) return correlate(new InternalLucyError(undefined, options));
78
+ return correlate(new ValidationFailureError(undefined, options));
79
+ }
80
+
81
+ export class AgentMemoryClient {
82
+ #permissions;
83
+ #timeoutMs;
84
+ #token;
85
+ #transport;
86
+
87
+ constructor({ transport, token, permissions: configuredPermissions, timeoutMs = 10_000 } = {}) {
88
+ if (typeof transport !== 'function') throw new ValidationFailureError('transport must be a function.');
89
+ if (typeof token !== 'string' || token.length === 0 || /[\r\n\0]/.test(token)) throw new AuthenticationFailureError('token must be a non-empty string without line breaks or NUL bytes.');
90
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) throw new ValidationFailureError('timeoutMs must be an integer between 1 and 300000.');
91
+ this.#transport = transport;
92
+ this.#token = token;
93
+ this.#permissions = permissions(configuredPermissions);
94
+ this.#timeoutMs = timeoutMs;
95
+ Object.freeze(this);
96
+ }
97
+
98
+ async recall(requestJson) {
99
+ this.#require('memory:read');
100
+ const request = toRecallRequestDto(parseRequestJson(requestJson, 'recall request', 8192));
101
+ return this.#invoke('recall', request, validateRecallResponse);
102
+ }
103
+ async recallCanonical(requestJson) {
104
+ this.#require('memory:read');
105
+ const request = toRecallRequestDto(parseRequestJson(requestJson, 'canonical recall request', 8192));
106
+ return this.#invoke('canonical-recall', request, validateCanonicalRecallResponse);
107
+ }
108
+ async propose(requestJson) {
109
+ this.#require('memory:propose');
110
+ const request = toProposalRequestDto(parseRequestJson(requestJson, 'proposal request', 12288));
111
+ return this.#invoke('propose', request, validateProposalResponse);
112
+ }
113
+ async proposalStatus(requestJson) {
114
+ this.#require('memory:read');
115
+ const request = toProposalStatusRequestDto(parseRequestJson(requestJson, 'proposal status request', 8192));
116
+ return this.#invoke('proposal-status', request, validateProposalResponse);
117
+ }
118
+ #require(permission) {
119
+ if (!this.#permissions.has(permission)) throw new AuthorizationFailureError(`The client configuration is missing ${permission}.`, { details: { permission } });
120
+ }
121
+ async #invoke(operation, request, validate) {
122
+ const payload = stableSerialize(request);
123
+ const limit = REQUEST_LIMITS[operation];
124
+ if (limit && Buffer.byteLength(payload) > limit) throw new ValidationFailureError(`${operation} request exceeds ${limit} UTF-8 bytes.`);
125
+ const controller = new AbortController();
126
+ let timer;
127
+ const timeout = new Promise((_, reject) => {
128
+ timer = setTimeout(() => { controller.abort(); reject(new TimeoutError(`Lucy request exceeded ${this.#timeoutMs}ms.`)); }, this.#timeoutMs);
129
+ });
130
+ let raw;
131
+ try {
132
+ raw = await Promise.race([
133
+ Promise.resolve().then(() => this.#transport(Object.freeze({ operation, payload, accessToken: this.#token, signal: controller.signal }))),
134
+ timeout,
135
+ ]);
136
+ } catch (error) {
137
+ if (error instanceof AgentMemoryError) throw error;
138
+ if (controller.signal.aborted || error?.name === 'TimeoutError') throw new TimeoutError(`Lucy request exceeded ${this.#timeoutMs}ms.`);
139
+ throw new UpstreamUnavailableError('Lucy transport failed.');
140
+ } finally { clearTimeout(timer); }
141
+ const response = responseMetadata(raw);
142
+ if ((operation === 'recall' || operation === 'canonical-recall') && response.byteLength > 65536) throw new MalformedResponseError('Recall response exceeds 65536 UTF-8 bytes.', { status: response.status });
143
+ if (response.status < 200 || response.status >= 300) throw mapError(response, operation);
144
+ const validated = validate(response.body, request);
145
+ const headerValue = response.memoryOperationId;
146
+ if (typeof headerValue === 'string' && /^memory-[a-f0-9]{32}$/i.test(headerValue)) {
147
+ return { ...validated, operation_id: headerValue };
148
+ }
149
+ return validated;
150
+ }
151
+ }
@@ -0,0 +1,101 @@
1
+ export class AgentMemoryError extends Error {
2
+ constructor(message, { code = 'agent_memory_error', status = null, retryable = false, details = null } = {}) {
3
+ super(message);
4
+ this.name = new.target.name;
5
+ this.code = code;
6
+ this.status = status;
7
+ this.retryable = retryable;
8
+ this.details = details;
9
+ }
10
+ }
11
+ export class AuthenticationFailureError extends AgentMemoryError {
12
+ constructor(message = 'Lucy authentication failed.', options = {}) {
13
+ super(message, { code: 'authentication_failure', ...options, retryable: false });
14
+ }
15
+ }
16
+ export class AuthorizationFailureError extends AgentMemoryError {
17
+ constructor(message = 'Lucy authorization failed.', options = {}) {
18
+ super(message, { code: 'authorization_failure', ...options, retryable: false });
19
+ }
20
+ }
21
+ export class InvalidScopeError extends AgentMemoryError {
22
+ constructor(message = 'The memory scope is invalid.', options = {}) {
23
+ super(message, { code: 'invalid_scope', ...options, retryable: false });
24
+ }
25
+ }
26
+ export class InvalidKeyError extends AgentMemoryError {
27
+ constructor(message = 'The memory key is invalid.', options = {}) {
28
+ super(message, { code: 'invalid_key', ...options, retryable: false });
29
+ }
30
+ }
31
+ export class InvalidKindError extends AgentMemoryError {
32
+ constructor(message = 'The memory kind is invalid.', options = {}) {
33
+ super(message, { code: 'invalid_kind', ...options, retryable: false });
34
+ }
35
+ }
36
+ export class ValidationFailureError extends AgentMemoryError {
37
+ constructor(message = 'The memory request is invalid.', options = {}) {
38
+ super(message, { code: 'validation_failure', ...options, retryable: false });
39
+ }
40
+ }
41
+ export class ProposalNotFoundError extends AgentMemoryError {
42
+ constructor(message = 'The proposal was not found or is not visible.', options = {}) {
43
+ super(message, { code: 'proposal_not_found', ...options, retryable: false });
44
+ }
45
+ }
46
+ export class MemoryNotFoundError extends AgentMemoryError {
47
+ constructor(message = 'The memory was not found or is not visible.', options = {}) {
48
+ super(message, { code: 'memory_not_found', ...options, retryable: false });
49
+ }
50
+ }
51
+ export class VersionConflictError extends AgentMemoryError {
52
+ constructor(message = 'The expected memory version does not match.', options = {}) {
53
+ super(message, { code: 'version_conflict', ...options, retryable: false });
54
+ }
55
+ }
56
+ export class LifecycleConflictError extends AgentMemoryError {
57
+ constructor(message = 'The memory lifecycle does not permit this operation.', options = {}) {
58
+ super(message, { code: 'lifecycle_conflict', ...options, retryable: false });
59
+ }
60
+ }
61
+ export class RateLimitError extends AgentMemoryError {
62
+ constructor(message = 'Lucy rate limited the request.', { retryAfterMs = null, ...options } = {}) {
63
+ super(message, { code: 'rate_limit', ...options, retryable: true });
64
+ this.retryAfterMs = retryAfterMs;
65
+ }
66
+ }
67
+ export class UpstreamUnavailableError extends AgentMemoryError {
68
+ constructor(message = 'Lucy is temporarily unavailable.', options = {}) {
69
+ super(message, { code: 'upstream_unavailable', ...options, retryable: true });
70
+ }
71
+ }
72
+ export class TimeoutError extends AgentMemoryError {
73
+ constructor(message = 'The Lucy request timed out.', options = {}) {
74
+ super(message, { code: 'timeout', ...options, retryable: true });
75
+ }
76
+ }
77
+ export class MalformedResponseError extends AgentMemoryError {
78
+ constructor(message = 'Lucy returned a malformed response.', options = {}) {
79
+ super(message, { code: 'malformed_response', ...options, retryable: false });
80
+ }
81
+ }
82
+ export class ProposalContractIncompatibilityError extends MalformedResponseError {
83
+ constructor(message = 'Lucy returned a proposal response incompatible with the frozen AM-01 schema.', options = {}) {
84
+ super(message, { code: 'proposal_contract_incompatible', ...options, retryable: false });
85
+ }
86
+ }
87
+ export class InternalLucyError extends AgentMemoryError {
88
+ constructor(message = 'Lucy returned an internal error.', options = {}) {
89
+ super(message, { code: 'internal_lucy_error', ...options, retryable: false });
90
+ }
91
+ }
92
+ export class InsufficientCreditsError extends AgentMemoryError {
93
+ constructor(message = 'The workspace does not have enough Lucy credits for this Agent Memory operation.', options = {}) {
94
+ super(message, { code: 'insufficient_credits', ...options, retryable: false });
95
+ }
96
+ }
97
+ export class BillingUnavailableError extends AgentMemoryError {
98
+ constructor(message = 'Lucy billing is temporarily unavailable.', options = {}) {
99
+ super(message, { code: 'billing_unavailable', ...options, retryable: true });
100
+ }
101
+ }
@@ -0,0 +1,12 @@
1
+ import type { AgentMemoryTransport } from './index.js';
2
+
3
+ export interface LucyHttpTransportOptions {
4
+ baseUrl: string;
5
+ /** Development-only opt-in for cleartext HTTP to literal loopback/localhost endpoints. */
6
+ allowInsecureLoopback?: boolean;
7
+ fetchImpl?: typeof fetch;
8
+ maxRetries?: number;
9
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
10
+ }
11
+
12
+ export function createLucyHttpTransport(options: LucyHttpTransportOptions): AgentMemoryTransport;