@lensagent/core 0.1.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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/config/lens.example.jsonc +92 -0
  4. package/config/pair.example.json +3 -0
  5. package/dist/adapters/executor.d.ts +35 -0
  6. package/dist/adapters/executor.js +296 -0
  7. package/dist/adapters/lsp.d.ts +18 -0
  8. package/dist/adapters/lsp.js +218 -0
  9. package/dist/adapters/mcp.d.ts +32 -0
  10. package/dist/adapters/mcp.js +283 -0
  11. package/dist/adapters/revert.d.ts +12 -0
  12. package/dist/adapters/revert.js +155 -0
  13. package/dist/config/runtime.d.ts +55 -0
  14. package/dist/config/runtime.js +371 -0
  15. package/dist/config/schema.d.ts +292 -0
  16. package/dist/config/schema.js +177 -0
  17. package/dist/contracts/adapter.d.ts +28 -0
  18. package/dist/contracts/adapter.js +8 -0
  19. package/dist/contracts/capability.d.ts +41 -0
  20. package/dist/contracts/capability.js +1 -0
  21. package/dist/contracts/domain.d.ts +61 -0
  22. package/dist/contracts/domain.js +1 -0
  23. package/dist/contracts/index.d.ts +5 -0
  24. package/dist/contracts/index.js +5 -0
  25. package/dist/contracts/model.d.ts +77 -0
  26. package/dist/contracts/model.js +1 -0
  27. package/dist/contracts/ports.d.ts +14 -0
  28. package/dist/contracts/ports.js +14 -0
  29. package/dist/core/access.d.ts +24 -0
  30. package/dist/core/access.js +205 -0
  31. package/dist/core/codec.d.ts +33 -0
  32. package/dist/core/codec.js +105 -0
  33. package/dist/core/domain.d.ts +13 -0
  34. package/dist/core/domain.js +24 -0
  35. package/dist/core/host.d.ts +19 -0
  36. package/dist/core/host.js +249 -0
  37. package/dist/core/identity.d.ts +19 -0
  38. package/dist/core/identity.js +54 -0
  39. package/dist/entry/bun.d.ts +1 -0
  40. package/dist/entry/bun.js +2 -0
  41. package/dist/entry/node.d.ts +2 -0
  42. package/dist/entry/node.js +3 -0
  43. package/dist/entry/rebuild.d.ts +1 -0
  44. package/dist/entry/rebuild.js +47 -0
  45. package/dist/entry/run.d.ts +1 -0
  46. package/dist/entry/run.js +43 -0
  47. package/dist/execution/lsp-process.d.ts +14 -0
  48. package/dist/execution/lsp-process.js +234 -0
  49. package/dist/http/server.d.ts +34 -0
  50. package/dist/http/server.js +1332 -0
  51. package/dist/launch.d.ts +3 -0
  52. package/dist/launch.js +4 -0
  53. package/dist/runtime/agent.d.ts +35 -0
  54. package/dist/runtime/agent.js +853 -0
  55. package/dist/runtime/artifacts.d.ts +15 -0
  56. package/dist/runtime/artifacts.js +72 -0
  57. package/dist/runtime/cache.d.ts +20 -0
  58. package/dist/runtime/cache.js +44 -0
  59. package/dist/runtime/calls.d.ts +21 -0
  60. package/dist/runtime/calls.js +130 -0
  61. package/dist/runtime/capabilities.d.ts +58 -0
  62. package/dist/runtime/capabilities.js +196 -0
  63. package/dist/runtime/cleanup.d.ts +7 -0
  64. package/dist/runtime/cleanup.js +33 -0
  65. package/dist/runtime/context.d.ts +34 -0
  66. package/dist/runtime/context.js +169 -0
  67. package/dist/runtime/diagnostics.d.ts +8 -0
  68. package/dist/runtime/diagnostics.js +71 -0
  69. package/dist/runtime/events.d.ts +22 -0
  70. package/dist/runtime/events.js +131 -0
  71. package/dist/runtime/kernel-capabilities.d.ts +32 -0
  72. package/dist/runtime/kernel-capabilities.js +200 -0
  73. package/dist/runtime/manifests.d.ts +14 -0
  74. package/dist/runtime/manifests.js +117 -0
  75. package/dist/runtime/model.d.ts +20 -0
  76. package/dist/runtime/model.js +241 -0
  77. package/dist/runtime/observability.d.ts +22 -0
  78. package/dist/runtime/observability.js +104 -0
  79. package/dist/runtime/policy.d.ts +8 -0
  80. package/dist/runtime/policy.js +31 -0
  81. package/dist/runtime/profile.d.ts +8 -0
  82. package/dist/runtime/profile.js +52 -0
  83. package/dist/runtime/projects.d.ts +40 -0
  84. package/dist/runtime/projects.js +115 -0
  85. package/dist/runtime/results.d.ts +7 -0
  86. package/dist/runtime/results.js +55 -0
  87. package/dist/runtime/review.d.ts +9 -0
  88. package/dist/runtime/review.js +54 -0
  89. package/dist/runtime/scheduler.d.ts +15 -0
  90. package/dist/runtime/scheduler.js +85 -0
  91. package/dist/runtime/skills.d.ts +2 -0
  92. package/dist/runtime/skills.js +83 -0
  93. package/dist/runtime/tokenizer.d.ts +8 -0
  94. package/dist/runtime/tokenizer.js +30 -0
  95. package/dist/store/database.d.ts +3 -0
  96. package/dist/store/database.js +49 -0
  97. package/dist/store/event-log.d.ts +19 -0
  98. package/dist/store/event-log.js +61 -0
  99. package/dist/store/schema.d.ts +1 -0
  100. package/dist/store/schema.js +347 -0
  101. package/dist/store/store.d.ts +344 -0
  102. package/dist/store/store.js +2554 -0
  103. package/package.json +73 -0
  104. package/proto/executor.proto +314 -0
@@ -0,0 +1,77 @@
1
+ import type { Json } from './domain.js';
2
+ export interface ModelMessage {
3
+ readonly role: 'system' | 'user' | 'assistant' | 'tool';
4
+ readonly content: string;
5
+ readonly toolCallId?: string;
6
+ readonly toolName?: string;
7
+ readonly toolCalls?: readonly {
8
+ readonly id: string;
9
+ readonly name: string;
10
+ readonly arguments: Json;
11
+ }[];
12
+ }
13
+ export interface ModelTool {
14
+ readonly id: string;
15
+ readonly description: string;
16
+ readonly inputSchema: Readonly<Record<string, Json>>;
17
+ readonly outputSchema?: Readonly<Record<string, Json>>;
18
+ }
19
+ export interface ModelToolBridgeInput {
20
+ readonly capabilityId: string;
21
+ readonly arguments: Json;
22
+ readonly providerCallId: string;
23
+ readonly ordinal: number;
24
+ readonly parentProviderCallId?: string;
25
+ }
26
+ export interface ProgrammaticToolCalling {
27
+ readonly capabilityIds: readonly string[];
28
+ readonly maxToolCalls: number;
29
+ execute(input: ModelToolBridgeInput, signal: AbortSignal | undefined): Promise<Json>;
30
+ }
31
+ export interface ModelRequest {
32
+ readonly model: string;
33
+ readonly messages: readonly ModelMessage[];
34
+ readonly tools: readonly ModelTool[];
35
+ readonly reasoningEffort: string | null;
36
+ readonly maxOutputTokens?: number;
37
+ readonly programmaticToolCalling?: ProgrammaticToolCalling;
38
+ readonly signal: AbortSignal;
39
+ }
40
+ export type ModelStreamEvent = {
41
+ readonly type: 'text';
42
+ readonly text: string;
43
+ } | {
44
+ readonly type: 'thinking';
45
+ readonly text: string;
46
+ } | {
47
+ readonly type: 'toolStart';
48
+ readonly ordinal: number;
49
+ readonly id: string;
50
+ readonly name: string;
51
+ } | {
52
+ readonly type: 'tool';
53
+ readonly ordinal: number;
54
+ readonly id: string;
55
+ readonly name: string;
56
+ readonly arguments: Json;
57
+ readonly parentProviderCallId?: string;
58
+ readonly providerExecuted?: boolean;
59
+ readonly programmaticCaller?: boolean;
60
+ } | {
61
+ readonly type: 'toolResult';
62
+ readonly ordinal: number;
63
+ readonly id: string;
64
+ readonly name: string;
65
+ readonly output: Json;
66
+ readonly providerExecuted?: boolean;
67
+ } | {
68
+ readonly type: 'usage';
69
+ readonly inputTokens: number;
70
+ readonly outputTokens: number;
71
+ } | {
72
+ readonly type: 'end';
73
+ readonly reason: string;
74
+ };
75
+ export interface ModelGateway {
76
+ stream(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export declare const PORT_CONTRACT_VERSIONS: Readonly<{
2
+ access: "1.0.0";
3
+ config: "1.0.0";
4
+ store: "1.0.0";
5
+ event: "1.0.0";
6
+ capability: "1.0.0";
7
+ execution: "1.0.0";
8
+ result: "1.0.0";
9
+ model: "1.0.0";
10
+ tokenizer: "1.0.0";
11
+ observability: "1.0.0";
12
+ mcp: "1.0.0";
13
+ lsp: "1.0.0";
14
+ }>;
@@ -0,0 +1,14 @@
1
+ export const PORT_CONTRACT_VERSIONS = Object.freeze({
2
+ access: '1.0.0',
3
+ config: '1.0.0',
4
+ store: '1.0.0',
5
+ event: '1.0.0',
6
+ capability: '1.0.0',
7
+ execution: '1.0.0',
8
+ result: '1.0.0',
9
+ model: '1.0.0',
10
+ tokenizer: '1.0.0',
11
+ observability: '1.0.0',
12
+ mcp: '1.0.0',
13
+ lsp: '1.0.0'
14
+ });
@@ -0,0 +1,24 @@
1
+ import { type TrustedIngress } from './domain.js';
2
+ type Headers = Readonly<Record<string, string | string[] | undefined>>;
3
+ interface Identity {
4
+ readonly clientId: string;
5
+ readonly kind: 'native' | 'browser';
6
+ readonly mac: string | null;
7
+ readonly hardwareId: string | null;
8
+ readonly browserFingerprint: string | null;
9
+ }
10
+ export declare class PairStore {
11
+ #private;
12
+ readonly onRevoked: (clientId: string) => void;
13
+ readonly path: string;
14
+ constructor(root: string, path: string, onRevoked: (clientId: string) => void);
15
+ get metadataRevision(): number;
16
+ get pairDigest(): string;
17
+ load(): Promise<void>;
18
+ start(onError?: (error: Error) => void): void;
19
+ close(): void;
20
+ identify(headers: Headers): Identity;
21
+ authorize(headers: Headers, transport: 'http' | 'https', correlationId: string): Promise<TrustedIngress>;
22
+ reload(): Promise<void>;
23
+ }
24
+ export {};
@@ -0,0 +1,205 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { watch } from 'node:fs';
3
+ import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, resolve } from 'node:path';
5
+ import { digest, now } from './codec.js';
6
+ import { fail } from './domain.js';
7
+ export class PairStore {
8
+ onRevoked;
9
+ path;
10
+ #records = new Map();
11
+ #digest = digest([]);
12
+ #metadataRevision = 0;
13
+ #watcher;
14
+ #writeTail = Promise.resolve();
15
+ constructor(root, path, onRevoked) {
16
+ this.onRevoked = onRevoked;
17
+ this.path = resolve(root, path);
18
+ }
19
+ get metadataRevision() {
20
+ return this.#metadataRevision;
21
+ }
22
+ get pairDigest() {
23
+ return this.#digest;
24
+ }
25
+ async load() {
26
+ await mkdir(dirname(this.path), { recursive: true });
27
+ try {
28
+ await stat(this.path);
29
+ }
30
+ catch {
31
+ await writeFile(this.path, '{\n "clients": []\n}\n', { encoding: 'utf8', flag: 'wx' }).catch(() => undefined);
32
+ }
33
+ await this.reload();
34
+ }
35
+ start(onError) {
36
+ this.#watcher = watch(dirname(this.path), (_event, filename) => {
37
+ if (filename !== null && filename !== basename(this.path))
38
+ return;
39
+ this.#writeTail = this.#writeTail.then(() => this.reload()).catch((error) => {
40
+ this.#metadataRevision += 1;
41
+ onError?.(error instanceof Error ? error : new Error(String(error)));
42
+ });
43
+ });
44
+ }
45
+ close() {
46
+ this.#watcher?.close();
47
+ this.#watcher = undefined;
48
+ }
49
+ identify(headers) {
50
+ const kind = header(headers, 'x-lens-client-kind');
51
+ if (kind === 'native') {
52
+ const mac = normalizeMac(header(headers, 'x-lens-client-mac'));
53
+ const hardwareId = identityText(header(headers, 'x-lens-hardware-id'));
54
+ return identity('native', mac, hardwareId, null);
55
+ }
56
+ if (kind === 'browser') {
57
+ return identity('browser', null, null, identityText(header(headers, 'x-lens-browser-fingerprint')));
58
+ }
59
+ fail('CLIENT_ID_INVALID', 400, 'X-Lens-Client-Kind must be native or browser.');
60
+ }
61
+ async authorize(headers, transport, correlationId) {
62
+ const client = this.identify(headers);
63
+ const record = this.#records.get(client.clientId);
64
+ if (record?.isVerified !== true) {
65
+ if (record === undefined)
66
+ await this.#appendUnknown(client);
67
+ fail('CLIENT_UNVERIFIED', 403, 'The client has not been verified.', { client_id: client.clientId });
68
+ }
69
+ return { source: 'user', clientId: client.clientId, transport, correlationId };
70
+ }
71
+ async reload() {
72
+ const next = await readPairFile(this.path);
73
+ const previous = this.#records;
74
+ this.#records = new Map(next.map((entry) => [entry.client_id, entry]));
75
+ this.#digest = digest(next.filter((entry) => entry.isVerified).map((entry) => entry.client_id).sort());
76
+ this.#metadataRevision += 1;
77
+ for (const [clientId, record] of previous) {
78
+ if (record.isVerified && this.#records.get(clientId)?.isVerified !== true)
79
+ this.onRevoked(clientId);
80
+ }
81
+ }
82
+ #appendUnknown(identityValue) {
83
+ const write = this.#writeTail.then(async () => {
84
+ const records = await readPairFile(this.path);
85
+ if (records.some((entry) => entry.client_id === identityValue.clientId)) {
86
+ await this.reload();
87
+ return;
88
+ }
89
+ if (records.length >= 4096)
90
+ fail('PAIR_FILE_CAPACITY', 503, 'The pair file reached its client limit.');
91
+ const timestamp = now();
92
+ records.push({
93
+ client_id: identityValue.clientId,
94
+ client_kind: identityValue.kind,
95
+ mac: identityValue.mac,
96
+ hardware_id: identityValue.hardwareId,
97
+ browser_fingerprint: identityValue.browserFingerprint,
98
+ isVerified: false,
99
+ label: null,
100
+ first_seen_at: timestamp,
101
+ last_seen_at: timestamp
102
+ });
103
+ const source = `${JSON.stringify({ clients: records }, null, 2)}\n`;
104
+ if (Buffer.byteLength(source) > 1_048_576)
105
+ fail('PAIR_FILE_CAPACITY', 503, 'The pair file reached its size limit.');
106
+ const temporary = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
107
+ await writeFile(temporary, source, 'utf8');
108
+ await rename(temporary, this.path);
109
+ await this.reload();
110
+ });
111
+ this.#writeTail = write.catch(() => undefined);
112
+ return write;
113
+ }
114
+ }
115
+ async function readPairFile(path) {
116
+ let document;
117
+ try {
118
+ const source = await readFile(path, 'utf8');
119
+ if (Buffer.byteLength(source) > 1_048_576)
120
+ fail('PAIR_FILE_CAPACITY', 503, 'The pair file reached its size limit.');
121
+ document = JSON.parse(source);
122
+ }
123
+ catch (error) {
124
+ if (error instanceof Error && error.name === 'LensError')
125
+ throw error;
126
+ fail('PAIR_FILE_INVALID', 503, 'The pair file could not be loaded.');
127
+ }
128
+ if (typeof document !== 'object' || document === null || Array.isArray(document))
129
+ fail('PAIR_FILE_INVALID', 503, 'The pair file is invalid.');
130
+ const clients = document.clients;
131
+ if (!Array.isArray(clients) || clients.length > 4096)
132
+ fail('PAIR_FILE_INVALID', 503, 'The pair file is invalid.');
133
+ return clients.map(pairRecord);
134
+ }
135
+ function pairRecord(value) {
136
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
137
+ fail('PAIR_FILE_INVALID', 503, 'A pair record is invalid.');
138
+ const row = value;
139
+ const kind = row.client_kind;
140
+ if (kind !== 'native' && kind !== 'browser')
141
+ fail('PAIR_FILE_INVALID', 503, 'A pair client kind is invalid.');
142
+ const label = row.label === null ? null : string(row.label, 'label', 256);
143
+ return {
144
+ client_id: string(row.client_id, 'client_id'),
145
+ client_kind: kind,
146
+ mac: nullableString(row.mac),
147
+ hardware_id: nullableString(row.hardware_id),
148
+ browser_fingerprint: nullableString(row.browser_fingerprint),
149
+ isVerified: row.isVerified === true,
150
+ label,
151
+ first_seen_at: string(row.first_seen_at, 'first_seen_at'),
152
+ last_seen_at: string(row.last_seen_at, 'last_seen_at')
153
+ };
154
+ }
155
+ function identity(kind, mac, hardwareId, fingerprint) {
156
+ const payload = kind === 'native'
157
+ ? `lens-client-id-v1\nkind=native\nmac=${mac ?? ''}\nhardware_id_b64=${base64(hardwareId ?? '')}`
158
+ : `lens-client-id-v1\nkind=browser\nbrowser_fingerprint_b64=${base64(fingerprint ?? '')}`;
159
+ return {
160
+ clientId: `sha256:${createHash('sha256').update(payload).digest('hex')}`,
161
+ kind,
162
+ mac,
163
+ hardwareId,
164
+ browserFingerprint: fingerprint
165
+ };
166
+ }
167
+ function header(headers, name) {
168
+ const raw = headers[name];
169
+ const value = Array.isArray(raw) ? raw[0] : raw;
170
+ if (typeof value !== 'string' || value.length === 0)
171
+ fail('CLIENT_ID_INVALID', 400, `${name} is required.`);
172
+ return value;
173
+ }
174
+ function normalizeMac(value) {
175
+ const compact = value.trim().toLowerCase().replaceAll('-', '').replaceAll(':', '');
176
+ if (!/^[0-9a-f]{12}$/.test(compact))
177
+ fail('CLIENT_ID_INVALID', 400, 'The client MAC address is invalid.');
178
+ return compact.match(/.{2}/g)?.join(':') ?? '';
179
+ }
180
+ function identityText(value) {
181
+ const trimmed = value.replace(/^[ \t]+|[ \t]+$/g, '');
182
+ if (trimmed.length === 0 || Buffer.byteLength(trimmed) > 1024 || hasControl(trimmed)) {
183
+ fail('CLIENT_ID_INVALID', 400, 'The client identity is invalid.');
184
+ }
185
+ return trimmed;
186
+ }
187
+ function hasControl(value) {
188
+ for (let index = 0; index < value.length; index += 1) {
189
+ const code = value.charCodeAt(index);
190
+ if (code <= 31 || code === 127)
191
+ return true;
192
+ }
193
+ return false;
194
+ }
195
+ function base64(value) {
196
+ return Buffer.from(value).toString('base64url');
197
+ }
198
+ function nullableString(value) {
199
+ return value === null ? null : string(value, 'pair field');
200
+ }
201
+ function string(value, field, maximum = 4096) {
202
+ if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value) > maximum)
203
+ fail('PAIR_FILE_INVALID', 503, `${field} is invalid.`);
204
+ return value;
205
+ }
@@ -0,0 +1,33 @@
1
+ import { type Json } from './domain.js';
2
+ export declare function id(kind: string): string;
3
+ export declare function now(): string;
4
+ export declare function digest(value: Json): string;
5
+ export declare function json(value: Json): string;
6
+ export declare function parseJson(value: string | null): Json | null;
7
+ export declare function jsonValue(value: unknown): Json;
8
+ /**
9
+ * Tool-call arguments, always as an object.
10
+ *
11
+ * A Provider is entitled to hand back the arguments as a raw JSON string rather than a parsed object,
12
+ * and it does exactly that when the model calls a tool that was not in the request's schema manifest --
13
+ * there is nothing to parse against, so the text passes through. Stored verbatim, that string then
14
+ * replays as `toolUse.input`, which Bedrock rejects: "The format of the value at
15
+ * messages.N.content.0.toolUse.input is invalid. Provide a json object for the field and try again."
16
+ *
17
+ * The damage is that it is permanent. The bad block sits in the Context, so every later request in that
18
+ * Session is rejected before the model is reached, and Retry cannot help because the poison is in the
19
+ * history rather than in the attempt. Normalising here is what keeps one malformed tool call from
20
+ * ending a conversation.
21
+ *
22
+ * An empty or unparseable value becomes `{}` rather than a failure: the Call itself will be rejected on
23
+ * its own arguments by the Capability's schema, which reports something the reader can act on, whereas
24
+ * a corrupt Context reports nothing and cannot be recovered.
25
+ */
26
+ export declare function toolArguments(value: unknown): Json;
27
+ export declare function record(value: unknown, field?: string): Readonly<Record<string, unknown>>;
28
+ export declare function text(value: unknown, field: string, maximum?: number): string;
29
+ export declare function optionalText(value: unknown, field: string, maximum?: number): string | undefined;
30
+ export declare function integer(value: unknown, field: string, minimum?: number): number;
31
+ export declare function expectedRevision(value: unknown): number;
32
+ export declare function boolean(value: unknown, field: string): boolean;
33
+ export declare function nullable(value: unknown): string | null;
@@ -0,0 +1,105 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { canonicalize } from 'json-canonicalize';
3
+ import { fail } from './domain.js';
4
+ export function id(kind) {
5
+ return `${kind}_${randomUUID()}`;
6
+ }
7
+ export function now() {
8
+ return new Date().toISOString();
9
+ }
10
+ export function digest(value) {
11
+ return createHash('sha256').update(canonicalize(value)).digest('hex');
12
+ }
13
+ export function json(value) {
14
+ return JSON.stringify(value);
15
+ }
16
+ export function parseJson(value) {
17
+ return value === null ? null : JSON.parse(value);
18
+ }
19
+ export function jsonValue(value) {
20
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
21
+ return value;
22
+ if (typeof value === 'number' && Number.isFinite(value))
23
+ return value;
24
+ if (Array.isArray(value))
25
+ return value.map(jsonValue);
26
+ if (typeof value === 'object') {
27
+ const result = {};
28
+ for (const [key, entry] of Object.entries(value))
29
+ result[key] = jsonValue(entry);
30
+ return result;
31
+ }
32
+ fail('INVALID_INPUT', 400, 'The value must be JSON.');
33
+ }
34
+ /**
35
+ * Tool-call arguments, always as an object.
36
+ *
37
+ * A Provider is entitled to hand back the arguments as a raw JSON string rather than a parsed object,
38
+ * and it does exactly that when the model calls a tool that was not in the request's schema manifest --
39
+ * there is nothing to parse against, so the text passes through. Stored verbatim, that string then
40
+ * replays as `toolUse.input`, which Bedrock rejects: "The format of the value at
41
+ * messages.N.content.0.toolUse.input is invalid. Provide a json object for the field and try again."
42
+ *
43
+ * The damage is that it is permanent. The bad block sits in the Context, so every later request in that
44
+ * Session is rejected before the model is reached, and Retry cannot help because the poison is in the
45
+ * history rather than in the attempt. Normalising here is what keeps one malformed tool call from
46
+ * ending a conversation.
47
+ *
48
+ * An empty or unparseable value becomes `{}` rather than a failure: the Call itself will be rejected on
49
+ * its own arguments by the Capability's schema, which reports something the reader can act on, whereas
50
+ * a corrupt Context reports nothing and cannot be recovered.
51
+ */
52
+ export function toolArguments(value) {
53
+ if (typeof value === 'string') {
54
+ const trimmed = value.trim();
55
+ if (trimmed.length === 0)
56
+ return {};
57
+ try {
58
+ const parsed = JSON.parse(trimmed);
59
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? jsonValue(parsed) : {};
60
+ }
61
+ catch {
62
+ return {};
63
+ }
64
+ }
65
+ if (value === undefined || value === null)
66
+ return {};
67
+ const normalized = jsonValue(value);
68
+ return typeof normalized === 'object' && normalized !== null && !Array.isArray(normalized) ? normalized : {};
69
+ }
70
+ export function record(value, field = 'body') {
71
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
72
+ fail('INVALID_INPUT', 400, `${field} must be an object.`);
73
+ }
74
+ return value;
75
+ }
76
+ export function text(value, field, maximum = 1_048_576) {
77
+ if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value) > maximum) {
78
+ fail('INVALID_INPUT', 400, `${field} must be a non-empty string.`);
79
+ }
80
+ return value;
81
+ }
82
+ export function optionalText(value, field, maximum = 1_048_576) {
83
+ return value === undefined ? undefined : text(value, field, maximum);
84
+ }
85
+ export function integer(value, field, minimum = 0) {
86
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum) {
87
+ fail('INVALID_INPUT', 400, `${field} must be an integer greater than or equal to ${minimum}.`);
88
+ }
89
+ return value;
90
+ }
91
+ export function expectedRevision(value) {
92
+ return integer(value, 'expected_revision');
93
+ }
94
+ export function boolean(value, field) {
95
+ if (typeof value !== 'boolean')
96
+ fail('INVALID_INPUT', 400, `${field} must be a boolean.`);
97
+ return value;
98
+ }
99
+ export function nullable(value) {
100
+ if (value === null || value === undefined)
101
+ return null;
102
+ if (typeof value === 'string')
103
+ return value;
104
+ fail('DATABASE_CORRUPT', 503, 'A stored value has an invalid type.');
105
+ }
@@ -0,0 +1,13 @@
1
+ export * from '../contracts/index.js';
2
+ import type { Json } from '../contracts/index.js';
3
+ export declare class LensError extends Error {
4
+ readonly code: string;
5
+ readonly status: number;
6
+ readonly details: Readonly<Record<string, Json>>;
7
+ constructor(code: string, status: number, message: string, details?: Readonly<Record<string, Json>>);
8
+ }
9
+ export declare class UserInputRequiredError extends Error {
10
+ readonly prompt: Json;
11
+ constructor(prompt: Json);
12
+ }
13
+ export declare function fail(code: string, status: number, message: string, details?: Readonly<Record<string, Json>>): never;
@@ -0,0 +1,24 @@
1
+ export * from '../contracts/index.js';
2
+ export class LensError extends Error {
3
+ code;
4
+ status;
5
+ details;
6
+ constructor(code, status, message, details = {}) {
7
+ super(message);
8
+ this.code = code;
9
+ this.status = status;
10
+ this.details = details;
11
+ this.name = 'LensError';
12
+ }
13
+ }
14
+ export class UserInputRequiredError extends Error {
15
+ prompt;
16
+ constructor(prompt) {
17
+ super('The Call requires user input.');
18
+ this.prompt = prompt;
19
+ this.name = 'UserInputRequiredError';
20
+ }
21
+ }
22
+ export function fail(code, status, message, details) {
23
+ throw new LensError(code, status, message, details);
24
+ }
@@ -0,0 +1,19 @@
1
+ export interface LaunchOptions {
2
+ readonly installRoot?: string;
3
+ readonly configPath?: string;
4
+ readonly runtime?: 'node' | 'bun';
5
+ }
6
+ export interface LensRuntime {
7
+ readonly url: string;
8
+ /**
9
+ * Resolves when a graceful stop has been requested through the Server's shutdown control path.
10
+ *
11
+ * Deciding what to do about it belongs to the caller. The entry module closes the runtime and
12
+ * exits the process; an embedding host is free to do something else. Exiting here would make
13
+ * `launch()` unusable as a library, because a control request from outside would take down a
14
+ * process that Core does not own.
15
+ */
16
+ readonly shutdownRequested: Promise<void>;
17
+ close(): Promise<void>;
18
+ }
19
+ export declare function launchCore(options?: LaunchOptions): Promise<LensRuntime>;