nexus-ai-pro 2.2.0 → 2.3.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/API_STABILITY.md +39 -0
- package/CHANGELOG.md +142 -1
- package/README.md +10 -5
- package/dist/agent/capabilities.d.ts +39 -0
- package/dist/agent/capabilities.js +36 -0
- package/dist/agent/create-agent.d.ts +61 -11
- package/dist/agent/create-agent.js +136 -22
- package/dist/agent/index.d.ts +3 -2
- package/dist/agent/index.js +1 -1
- package/dist/agent/middleware/approval.d.ts +26 -0
- package/dist/agent/middleware/approval.js +23 -0
- package/dist/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist/agent/middleware/context-editor.d.ts +52 -0
- package/dist/agent/middleware/context-editor.js +66 -0
- package/dist/agent/middleware/filesystem.d.ts +44 -0
- package/dist/agent/middleware/filesystem.js +83 -0
- package/dist/agent/middleware/index.d.ts +19 -0
- package/dist/agent/middleware/index.js +9 -0
- package/dist/agent/middleware/limits.d.ts +42 -0
- package/dist/agent/middleware/limits.js +52 -0
- package/dist/agent/middleware/models.d.ts +48 -0
- package/dist/agent/middleware/models.js +44 -0
- package/dist/agent/middleware/pii.d.ts +67 -0
- package/dist/agent/middleware/pii.js +173 -0
- package/dist/agent/middleware/retry.d.ts +66 -0
- package/dist/agent/middleware/retry.js +59 -0
- package/dist/agent/middleware/shared.d.ts +41 -0
- package/dist/agent/middleware/shared.js +73 -0
- package/dist/agent/middleware/tool-selector.d.ts +64 -0
- package/dist/agent/middleware/tool-selector.js +106 -0
- package/dist/agent/permissions.d.ts +113 -0
- package/dist/agent/permissions.js +180 -0
- package/dist/agent/sandbox.d.ts +151 -0
- package/dist/agent/sandbox.js +257 -0
- package/dist/agent/tool.d.ts +8 -3
- package/dist/agent/tool.js +14 -3
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/graph/functional.d.ts +9 -0
- package/dist/graph/functional.js +20 -3
- package/dist/graph/graph.js +25 -4
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/rag/elasticsearch.d.ts +71 -0
- package/dist/rag/elasticsearch.js +130 -0
- package/dist/rag/pipeline.d.ts +140 -0
- package/dist/rag/pipeline.js +187 -0
- package/dist/rag/rerankers.d.ts +92 -0
- package/dist/rag/rerankers.js +97 -0
- package/dist/rag/retrievers.d.ts +12 -1
- package/dist/server/assistant.d.ts +5 -1
- package/dist/server/assistant.js +4 -2
- package/dist/server/auth.d.ts +250 -0
- package/dist/server/auth.js +392 -0
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/types/graph.d.ts +38 -0
- package/dist/types/messages.d.ts +36 -2
- package/dist/types/principal.d.ts +21 -0
- package/dist/types/principal.js +1 -0
- package/dist/types/server.d.ts +2 -9
- package/dist/utils/tenant.d.ts +11 -0
- package/dist/utils/tenant.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-cjs/agent/capabilities.d.ts +39 -0
- package/dist-cjs/agent/capabilities.js +41 -0
- package/dist-cjs/agent/create-agent.d.ts +61 -11
- package/dist-cjs/agent/create-agent.js +136 -22
- package/dist-cjs/agent/index.d.ts +3 -2
- package/dist-cjs/agent/index.js +4 -4
- package/dist-cjs/agent/middleware/approval.d.ts +26 -0
- package/dist-cjs/agent/middleware/approval.js +26 -0
- package/dist-cjs/agent/{middleware.d.ts → middleware/basic.d.ts} +5 -4
- package/dist-cjs/agent/{middleware.js → middleware/basic.js} +3 -5
- package/dist-cjs/agent/middleware/context-editor.d.ts +52 -0
- package/dist-cjs/agent/middleware/context-editor.js +70 -0
- package/dist-cjs/agent/middleware/filesystem.d.ts +44 -0
- package/dist-cjs/agent/middleware/filesystem.js +86 -0
- package/dist-cjs/agent/middleware/index.d.ts +19 -0
- package/dist-cjs/agent/middleware/index.js +28 -0
- package/dist-cjs/agent/middleware/limits.d.ts +42 -0
- package/dist-cjs/agent/middleware/limits.js +57 -0
- package/dist-cjs/agent/middleware/models.d.ts +48 -0
- package/dist-cjs/agent/middleware/models.js +48 -0
- package/dist-cjs/agent/middleware/pii.d.ts +67 -0
- package/dist-cjs/agent/middleware/pii.js +178 -0
- package/dist-cjs/agent/middleware/retry.d.ts +66 -0
- package/dist-cjs/agent/middleware/retry.js +63 -0
- package/dist-cjs/agent/middleware/shared.d.ts +41 -0
- package/dist-cjs/agent/middleware/shared.js +83 -0
- package/dist-cjs/agent/middleware/tool-selector.d.ts +64 -0
- package/dist-cjs/agent/middleware/tool-selector.js +109 -0
- package/dist-cjs/agent/permissions.d.ts +113 -0
- package/dist-cjs/agent/permissions.js +189 -0
- package/dist-cjs/agent/sandbox.d.ts +151 -0
- package/dist-cjs/agent/sandbox.js +297 -0
- package/dist-cjs/agent/tool.d.ts +8 -3
- package/dist-cjs/agent/tool.js +14 -3
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/graph/functional.d.ts +9 -0
- package/dist-cjs/graph/functional.js +20 -3
- package/dist-cjs/graph/graph.js +25 -4
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/rag/elasticsearch.d.ts +71 -0
- package/dist-cjs/rag/elasticsearch.js +135 -0
- package/dist-cjs/rag/pipeline.d.ts +140 -0
- package/dist-cjs/rag/pipeline.js +191 -0
- package/dist-cjs/rag/rerankers.d.ts +92 -0
- package/dist-cjs/rag/rerankers.js +107 -0
- package/dist-cjs/rag/retrievers.d.ts +12 -1
- package/dist-cjs/server/assistant.d.ts +5 -1
- package/dist-cjs/server/assistant.js +4 -2
- package/dist-cjs/server/auth.d.ts +250 -0
- package/dist-cjs/server/auth.js +402 -0
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/types/graph.d.ts +38 -0
- package/dist-cjs/types/messages.d.ts +36 -2
- package/dist-cjs/types/principal.d.ts +21 -0
- package/dist-cjs/types/principal.js +2 -0
- package/dist-cjs/types/server.d.ts +2 -9
- package/dist-cjs/utils/tenant.d.ts +11 -0
- package/dist-cjs/utils/tenant.js +15 -0
- package/dist-cjs/version.d.ts +1 -1
- package/dist-cjs/version.js +1 -1
- package/llms.txt +4 -3
- package/package.json +118 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OperationLifecycleLike } from '../types/lifecycle.js';
|
|
2
|
+
import type { Principal } from '../types/principal.js';
|
|
2
3
|
import { GraphError } from './errors.js';
|
|
3
4
|
import type { GraphCheckpoint, GraphCheckpointer, GraphDescription, GraphEvent, GraphStatus, InterruptRequest, PendingInterrupt, RetryPolicy, RunControlLike } from '../types/graph.js';
|
|
4
5
|
import type { Store } from '../types/store.js';
|
|
@@ -86,6 +87,8 @@ export interface WorkflowContext {
|
|
|
86
87
|
readonly threadId: string;
|
|
87
88
|
/** The tenant the run is for, when it has one. */
|
|
88
89
|
readonly tenantId?: string;
|
|
90
|
+
/** Who the run is for, when it was started with a principal: the caller the server authenticated. */
|
|
91
|
+
readonly principal?: Readonly<Principal>;
|
|
89
92
|
/** Aborted when the run is cancelled. */
|
|
90
93
|
readonly signal: AbortSignal;
|
|
91
94
|
/** Long-term memory, when the workflow was created with a store. In a tenant's run, the tenant's view of it. */
|
|
@@ -162,6 +165,12 @@ export interface WorkflowRunOptions {
|
|
|
162
165
|
* without one carries on the thread's.
|
|
163
166
|
*/
|
|
164
167
|
tenantId?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Who the run is for, read as `context.principal`. Its `tenantId` is the run's tenant when
|
|
170
|
+
* `tenantId` is not given, and a run whose two tenants differ is refused; its `userId` is recorded
|
|
171
|
+
* on every checkpoint as `metadata.userId`.
|
|
172
|
+
*/
|
|
173
|
+
principal?: Principal;
|
|
165
174
|
}
|
|
166
175
|
/** One event of a streamed workflow run. */
|
|
167
176
|
export type WorkflowEvent<O = unknown> = {
|
package/dist/graph/functional.js
CHANGED
|
@@ -2,6 +2,7 @@ import { MemoryGraphCheckpointer } from './checkpointer.js';
|
|
|
2
2
|
import { withLifecycle } from '../lifecycle/run.js';
|
|
3
3
|
import { migrateCheckpoint, toCheckpoint } from './checkpoint-migration.js';
|
|
4
4
|
import { tenantStore } from '../store/tenant.js';
|
|
5
|
+
import { withPrincipal } from '../utils/tenant.js';
|
|
5
6
|
import { GraphDrainedError, GraphError, GraphNotInterruptedError, GraphThreadNotFoundError } from './errors.js';
|
|
6
7
|
export class WorkflowStepTimeoutError extends GraphError {
|
|
7
8
|
stepName;
|
|
@@ -52,9 +53,10 @@ export class Workflow {
|
|
|
52
53
|
const threadId = runOptions.threadId ?? generatedId();
|
|
53
54
|
return this.execute(threadId, runOptions, async () => {
|
|
54
55
|
const latest = await this.latest(threadId);
|
|
55
|
-
|
|
56
|
+
const tenantId = runOptions.tenantId ?? runOptions.principal?.tenantId;
|
|
57
|
+
if (latest && tenantId !== undefined) {
|
|
56
58
|
const owner = latest.metadata?.tenantId;
|
|
57
|
-
if (typeof owner === 'string' && owner !==
|
|
59
|
+
if (typeof owner === 'string' && owner !== tenantId) {
|
|
58
60
|
throw new GraphError(`Thread "${threadId}" cannot be used by this tenant; choose another id`, 'GRAPH_VALIDATION_ERROR');
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -148,7 +150,21 @@ export class Workflow {
|
|
|
148
150
|
}
|
|
149
151
|
execute(threadId, runOptions, start) {
|
|
150
152
|
const queue = new EventQueue();
|
|
151
|
-
|
|
153
|
+
let options;
|
|
154
|
+
try {
|
|
155
|
+
options = withPrincipal(runOptions);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
queue.fail(error);
|
|
159
|
+
return queue;
|
|
160
|
+
}
|
|
161
|
+
const metadata = {
|
|
162
|
+
threadId,
|
|
163
|
+
graph: this.name,
|
|
164
|
+
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
165
|
+
...(options.principal?.userId !== undefined ? { userId: options.principal.userId } : {}),
|
|
166
|
+
};
|
|
167
|
+
void withLifecycle(this.options.lifecycle, { family: 'graph', operation: `workflow.${this.name}`, metadata }, options.signal ? { signal: options.signal } : {}, () => this.run(threadId, options, start, queue)).then(() => queue.close(), (error) => queue.fail(error));
|
|
152
168
|
return queue;
|
|
153
169
|
}
|
|
154
170
|
async run(threadId, runOptions, start, queue) {
|
|
@@ -203,6 +219,7 @@ export class Workflow {
|
|
|
203
219
|
const context = {
|
|
204
220
|
threadId,
|
|
205
221
|
...(tenantId === undefined ? {} : { tenantId }),
|
|
222
|
+
...(runOptions.principal ? { principal: runOptions.principal } : {}),
|
|
206
223
|
signal: controller.signal,
|
|
207
224
|
...(this.options.store
|
|
208
225
|
? { store: tenantId === undefined ? this.options.store : tenantStore(this.options.store, tenantId) }
|
package/dist/graph/graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { Command, END, Send, START } from '../types/graph.js';
|
|
3
3
|
import { tenantStore } from '../store/tenant.js';
|
|
4
|
+
import { withPrincipal } from '../utils/tenant.js';
|
|
4
5
|
import { linkSignals } from '../utils/signals.js';
|
|
5
6
|
import { MemoryGraphCheckpointer } from './checkpointer.js';
|
|
6
7
|
import { migrateCheckpoint, toCheckpoint } from './checkpoint-migration.js';
|
|
@@ -152,9 +153,10 @@ export class CompiledGraph {
|
|
|
152
153
|
const threadId = resolveThreadId(runOptions.threadId);
|
|
153
154
|
return this.run(threadId, runOptions, async () => {
|
|
154
155
|
this.assertInput(input);
|
|
155
|
-
|
|
156
|
+
const tenantId = runOptions.tenantId ?? runOptions.principal?.tenantId;
|
|
157
|
+
if (tenantId !== undefined) {
|
|
156
158
|
const existing = await this.checkpointer()?.get(threadId);
|
|
157
|
-
if (existing && !ownedBy(existing,
|
|
159
|
+
if (existing && !ownedBy(existing, tenantId)) {
|
|
158
160
|
throw new GraphValidationError(`Thread "${threadId}" cannot be used by this tenant; choose another id`);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
@@ -266,6 +268,8 @@ export class CompiledGraph {
|
|
|
266
268
|
const runOptions = {
|
|
267
269
|
threadId: `${context.threadId}:${context.taskId}`,
|
|
268
270
|
signal: context.signal,
|
|
271
|
+
...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
|
|
272
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
269
273
|
...(forward ? { onEvent: forward, subgraphEvents: true } : {}),
|
|
270
274
|
};
|
|
271
275
|
const paused = await this.state(runOptions.threadId);
|
|
@@ -309,7 +313,7 @@ export class CompiledGraph {
|
|
|
309
313
|
describe() {
|
|
310
314
|
const nodes = [...this.nodes.entries()].map(([id, definition]) => {
|
|
311
315
|
const subgraph = definition.fn[SUBGRAPH];
|
|
312
|
-
const { ends, defer, timeoutMs, cache, idempotent } = definition.options;
|
|
316
|
+
const { ends, defer, timeoutMs, cache, idempotent, effects, interrupts } = definition.options;
|
|
313
317
|
const maxAttempts = this.retryPolicy(definition).maxAttempts;
|
|
314
318
|
const timeout = this.timeoutOf(definition);
|
|
315
319
|
return {
|
|
@@ -322,6 +326,8 @@ export class CompiledGraph {
|
|
|
322
326
|
...(this.errorHandler(definition) ? { onError: true } : {}),
|
|
323
327
|
...(idempotent ? { idempotent: true } : {}),
|
|
324
328
|
...(cache ? { cache: true } : {}),
|
|
329
|
+
...(effects?.length ? { effects: [...effects] } : {}),
|
|
330
|
+
...(interrupts ? { interrupts: true } : {}),
|
|
325
331
|
...(subgraph ? { subgraph: subgraph.describe() } : {}),
|
|
326
332
|
};
|
|
327
333
|
});
|
|
@@ -358,6 +364,14 @@ export class CompiledGraph {
|
|
|
358
364
|
durability: this.options.durability ?? 'sync',
|
|
359
365
|
checkpointer: this.options.checkpointer === false ? 'none' : this.options.checkpointer === undefined ? 'memory' : 'custom',
|
|
360
366
|
maxConcurrency: this.options.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
|
|
367
|
+
...(this.options.tools?.length
|
|
368
|
+
? {
|
|
369
|
+
tools: this.options.tools.map((tool) => ({
|
|
370
|
+
...tool,
|
|
371
|
+
...(tool.capabilities ? { capabilities: [...tool.capabilities] } : {}),
|
|
372
|
+
})),
|
|
373
|
+
}
|
|
374
|
+
: {}),
|
|
361
375
|
};
|
|
362
376
|
}
|
|
363
377
|
async updateState(threadId, update, options = {}) {
|
|
@@ -418,6 +432,7 @@ export class CompiledGraph {
|
|
|
418
432
|
return forkId;
|
|
419
433
|
}
|
|
420
434
|
async *run(threadId, runOptions, seed) {
|
|
435
|
+
runOptions = withPrincipal(runOptions);
|
|
421
436
|
const lifecycle = this.options.lifecycle;
|
|
422
437
|
if (!lifecycle) {
|
|
423
438
|
yield* this.runSteps(threadId, runOptions, seed);
|
|
@@ -426,7 +441,12 @@ export class CompiledGraph {
|
|
|
426
441
|
const ticket = await lifecycle.start({
|
|
427
442
|
family: this.options.lifecycleFamily ?? 'graph',
|
|
428
443
|
operation: this.options.name ? `${this.options.lifecycleFamily ?? 'graph'}.${this.options.name}` : 'graph.run',
|
|
429
|
-
metadata: {
|
|
444
|
+
metadata: {
|
|
445
|
+
threadId,
|
|
446
|
+
...(this.options.name ? { graph: this.options.name } : {}),
|
|
447
|
+
...(runOptions.tenantId !== undefined ? { tenantId: runOptions.tenantId } : {}),
|
|
448
|
+
...(runOptions.principal?.userId !== undefined ? { userId: runOptions.principal.userId } : {}),
|
|
449
|
+
},
|
|
430
450
|
}, runOptions.signal ? { signal: runOptions.signal } : {});
|
|
431
451
|
let last;
|
|
432
452
|
let settled = false;
|
|
@@ -850,6 +870,7 @@ export class CompiledGraph {
|
|
|
850
870
|
step,
|
|
851
871
|
threadId,
|
|
852
872
|
...(runOptions.tenantId !== undefined ? { tenantId: runOptions.tenantId } : {}),
|
|
873
|
+
...(runOptions.principal ? { principal: runOptions.principal } : {}),
|
|
853
874
|
taskId: task.id,
|
|
854
875
|
input: task.input,
|
|
855
876
|
attempt,
|
package/dist/graph/lint.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { GraphDescription } from '../types/graph.js';
|
|
|
2
2
|
/** How much a lint finding matters: `error` fails `nexus graph lint`, the others only report. */
|
|
3
3
|
export type GraphLintSeverity = 'error' | 'warning' | 'info';
|
|
4
4
|
/** The rules `lintGraph()` checks. */
|
|
5
|
-
export type GraphLintCode = 'UNBOUNDED_CYCLE' | 'RETRY_WITHOUT_IDEMPOTENCY' | 'RETRY_WITHOUT_TIMEOUT' | 'DEFERRED_DURABILITY_WITH_SIDE_EFFECTS' | 'MEMORY_CHECKPOINTER' | 'NO_CHECKPOINTER' | 'UNBOUNDED_CONCURRENCY' | 'DYNAMIC_ROUTE';
|
|
5
|
+
export type GraphLintCode = 'UNBOUNDED_CYCLE' | 'RETRY_WITHOUT_IDEMPOTENCY' | 'RETRY_WITHOUT_TIMEOUT' | 'DEFERRED_DURABILITY_WITH_SIDE_EFFECTS' | 'MEMORY_CHECKPOINTER' | 'NO_CHECKPOINTER' | 'UNBOUNDED_CONCURRENCY' | 'DYNAMIC_ROUTE' | 'SENSITIVE_TOOL_WITHOUT_APPROVAL' | 'UNDECLARED_TOOL_CAPABILITIES' | 'SIDE_EFFECT_BEFORE_INTERRUPT';
|
|
6
6
|
/** One design problem `lintGraph()` found. */
|
|
7
7
|
export interface GraphLintFinding {
|
|
8
8
|
/** Which rule found it. */
|
|
@@ -43,6 +43,9 @@ export interface GraphLintOptions {
|
|
|
43
43
|
* | `NO_CHECKPOINTER` | info | No checkpoints, so no interrupt, resume, or recovery |
|
|
44
44
|
* | `UNBOUNDED_CONCURRENCY` | warning | A fan-out with no limit on tasks at once |
|
|
45
45
|
* | `DYNAMIC_ROUTE` | info | A router with no mapping or `ends`, so routes cannot be checked |
|
|
46
|
+
* | `SENSITIVE_TOOL_WITHOUT_APPROVAL` | warning, or error when `deployed` | A tool that writes, runs commands or code, or reaches the network, with no approval and no permission policy |
|
|
47
|
+
* | `UNDECLARED_TOOL_CAPABILITIES` | info | A tool with no capabilities and no approval, whose effects cannot be judged |
|
|
48
|
+
* | `SIDE_EFFECT_BEFORE_INTERRUPT` | warning | A node that declares effects and interrupts, so its effects repeat when it runs again on the answer |
|
|
46
49
|
*
|
|
47
50
|
* @example
|
|
48
51
|
* ```ts
|
package/dist/graph/lint.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSensitiveCapability } from '../agent/capabilities.js';
|
|
1
2
|
export function lintGraph(graph, options = {}) {
|
|
2
3
|
const description = 'describe' in graph && typeof graph.describe === 'function' ? graph.describe() : graph;
|
|
3
4
|
const findings = [];
|
|
@@ -39,9 +40,43 @@ function lintDescription(description, prefix, options, findings, top) {
|
|
|
39
40
|
fix: 'Set `timeout: { runMs }`, or `idleMs` with `context.heartbeat()` for long work.',
|
|
40
41
|
});
|
|
41
42
|
}
|
|
43
|
+
if (node.effects?.length && node.interrupts && !node.idempotent) {
|
|
44
|
+
findings.push({
|
|
45
|
+
code: 'SIDE_EFFECT_BEFORE_INTERRUPT',
|
|
46
|
+
severity: 'warning',
|
|
47
|
+
node: at(node.id),
|
|
48
|
+
message: `"${at(node.id)}" declares effects (${node.effects.join(', ')}) and may interrupt, so when the answer comes the node runs again from the top and repeats whatever it did before asking.`,
|
|
49
|
+
fix: 'Ask in a node of its own before the effect, or give the effect an idempotency key and declare the node idempotent.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
42
52
|
if (node.subgraph)
|
|
43
53
|
lintDescription(node.subgraph, `${at(node.id)}/`, options, findings, false);
|
|
44
54
|
}
|
|
55
|
+
for (const tool of description.tools ?? []) {
|
|
56
|
+
if (tool.approval !== 'none')
|
|
57
|
+
continue;
|
|
58
|
+
const sensitive = (tool.capabilities ?? []).filter((capability) => isSensitiveCapability(capability));
|
|
59
|
+
if (sensitive.length > 0 || tool.dynamic) {
|
|
60
|
+
findings.push({
|
|
61
|
+
code: 'SENSITIVE_TOOL_WITHOUT_APPROVAL',
|
|
62
|
+
severity: options.deployed ? 'error' : 'warning',
|
|
63
|
+
node: `tool:${tool.name}`,
|
|
64
|
+
message: sensitive.length
|
|
65
|
+
? `Tool "${tool.name}" can ${sensitive.join(', ')}, and nothing approves or limits its calls.`
|
|
66
|
+
: `Tool "${tool.name}" computes its capabilities per call, and nothing approves or limits its calls.`,
|
|
67
|
+
fix: 'Give the agent a permission policy that grants what the tool may do, or approve its calls with interruptOn.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (tool.capabilities === undefined) {
|
|
71
|
+
findings.push({
|
|
72
|
+
code: 'UNDECLARED_TOOL_CAPABILITIES',
|
|
73
|
+
severity: 'info',
|
|
74
|
+
node: `tool:${tool.name}`,
|
|
75
|
+
message: `Tool "${tool.name}" declares no capabilities, so what it does cannot be judged.`,
|
|
76
|
+
fix: 'Declare its capabilities, or `capabilities: []` for a tool that changes nothing.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
45
80
|
if (!top)
|
|
46
81
|
return;
|
|
47
82
|
if ((description.durability === 'async' || description.durability === 'exit') && sideEffecting.length > 0) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RagChunk } from '../hallucination/rag.js';
|
|
2
|
+
import type { VectorSearchResult } from '../hallucination/retrieval.js';
|
|
3
|
+
import type { RetrieveOptions, SparseRetriever } from '../rag/retrievers.js';
|
|
4
|
+
import type { MigrationResult, SchemaMigration } from '../utils/schema-migrations.js';
|
|
5
|
+
import { type PostgresLikeClient } from './client.js';
|
|
6
|
+
import type { PostgresMigrateOptions } from './migrations.js';
|
|
7
|
+
/** Options for `PostgresKeywordIndex`. */
|
|
8
|
+
export interface PostgresKeywordIndexOptions {
|
|
9
|
+
/** Table name, optionally schema-qualified. Defaults to `nexus_keywords`. */
|
|
10
|
+
table?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Searches the table of a `PostgresVectorStore` instead of keeping chunks of its own. `migrate()`
|
|
13
|
+
* adds a generated search column and its index to that table, the store's writes keep it current,
|
|
14
|
+
* and `add()` and `delete()` leave writing to the store. One copy of every chunk, and keyword and
|
|
15
|
+
* vector search can never disagree about what exists.
|
|
16
|
+
*/
|
|
17
|
+
shared?: boolean;
|
|
18
|
+
/** The text search configuration: `english`, `simple`, `german`, and so on. Defaults to `english`. */
|
|
19
|
+
language?: string;
|
|
20
|
+
/**
|
|
21
|
+
* `any` (the default) finds chunks with any of the query's terms, ranked by how many and how close,
|
|
22
|
+
* as keyword search for retrieval should. `all` needs every term, read as a web search query, with
|
|
23
|
+
* quoted phrases and `-` for exclusion.
|
|
24
|
+
*/
|
|
25
|
+
match?: 'any' | 'all';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The versioned schema, which `migrate()` applies. Version 1 creates the table with a generated
|
|
29
|
+
* `tsvector` column and a GIN index, or, `shared`, adds the column and the index to a vector table.
|
|
30
|
+
*/
|
|
31
|
+
export declare function keywordIndexMigrations(options?: PostgresKeywordIndexOptions): SchemaMigration[];
|
|
32
|
+
/**
|
|
33
|
+
* Keyword search in Postgres, with its full-text search: stemmed terms, a GIN index, and
|
|
34
|
+
* `ts_rank_cd` ranking, so only the best matches leave the database however large the table.
|
|
35
|
+
*
|
|
36
|
+
* A `SparseRetriever`, so `hybridRetriever()` fuses it with `PostgresVectorStore`. With `shared`, it
|
|
37
|
+
* searches the vector store's own table, and the hybrid search runs over one copy of the data. A
|
|
38
|
+
* metadata filter runs in SQL, with the same meaning as the vector store's.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const vectors = new PostgresVectorStore(client, { dimensions: 1536, embed });
|
|
42
|
+
* const keywords = new PostgresKeywordIndex(client, { table: 'nexus_vectors', shared: true });
|
|
43
|
+
* await vectors.migrate();
|
|
44
|
+
* await keywords.migrate();
|
|
45
|
+
* const retriever = hybridRetriever([vectorRetriever(vectors), keywords]);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare class PostgresKeywordIndex implements SparseRetriever {
|
|
49
|
+
private readonly client;
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly table;
|
|
52
|
+
private readonly language;
|
|
53
|
+
constructor(client: PostgresLikeClient, options?: PostgresKeywordIndexOptions);
|
|
54
|
+
/**
|
|
55
|
+
* Applies this index's pending migrations, recorded in `nexus_schema_migrations`, under a lock.
|
|
56
|
+
* Never runs implicitly. With `shared`, run the vector store's `migrate()` first.
|
|
57
|
+
*/
|
|
58
|
+
migrate(options?: PostgresMigrateOptions): Promise<MigrationResult>;
|
|
59
|
+
/** Adds chunks, or replaces those whose id exists. With `shared`, does nothing: the vector store writes. */
|
|
60
|
+
add(chunks: readonly RagChunk[]): Promise<void>;
|
|
61
|
+
/** Removes chunks by id. With `shared`, does nothing: the vector store deletes. */
|
|
62
|
+
delete(ids: readonly string[]): Promise<void>;
|
|
63
|
+
/** The chunks that best match a query's terms, best first, scored from 0 to 1. */
|
|
64
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<VectorSearchResult[]>;
|
|
65
|
+
/** Chunks held. */
|
|
66
|
+
size(): Promise<number>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fromJson, quoteDerived, quoteTable } from './client.js';
|
|
2
|
+
import { SqlParams, textArray } from './sql.js';
|
|
3
|
+
const DEFAULT_TABLE = 'nexus_keywords';
|
|
4
|
+
function languageOf(options) {
|
|
5
|
+
const language = options.language ?? 'english';
|
|
6
|
+
if (!/^[a-z_]{1,63}$/.test(language))
|
|
7
|
+
throw new RangeError(`"${language}" is not a text search configuration name`);
|
|
8
|
+
return language;
|
|
9
|
+
}
|
|
10
|
+
export function keywordIndexMigrations(options = {}) {
|
|
11
|
+
const name = options.table ?? DEFAULT_TABLE;
|
|
12
|
+
const table = quoteTable(name);
|
|
13
|
+
const generated = `fts tsvector GENERATED ALWAYS AS (to_tsvector('${languageOf(options)}'::regconfig, content)) STORED`;
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
component: `keywords:${name}`,
|
|
17
|
+
version: 1,
|
|
18
|
+
name: options.shared ? 'add full-text search to a vector table' : 'create the keyword table',
|
|
19
|
+
statements: [
|
|
20
|
+
options.shared
|
|
21
|
+
? `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${generated}`
|
|
22
|
+
: `CREATE TABLE IF NOT EXISTS ${table} (
|
|
23
|
+
id text PRIMARY KEY,
|
|
24
|
+
content text NOT NULL,
|
|
25
|
+
source text,
|
|
26
|
+
metadata jsonb,
|
|
27
|
+
${generated}
|
|
28
|
+
)`,
|
|
29
|
+
`CREATE INDEX IF NOT EXISTS ${quoteDerived(name, 'fts')} ON ${table} USING gin (fts)`,
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
export class PostgresKeywordIndex {
|
|
35
|
+
client;
|
|
36
|
+
options;
|
|
37
|
+
table;
|
|
38
|
+
language;
|
|
39
|
+
constructor(client, options = {}) {
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.table = quoteTable(options.table ?? DEFAULT_TABLE);
|
|
43
|
+
this.language = languageOf(options);
|
|
44
|
+
}
|
|
45
|
+
async migrate(options = {}) {
|
|
46
|
+
const { applyPostgresMigrations } = await import('./migrations.js');
|
|
47
|
+
return applyPostgresMigrations(this.client, keywordIndexMigrations(this.options), options);
|
|
48
|
+
}
|
|
49
|
+
async add(chunks) {
|
|
50
|
+
if (this.options.shared || chunks.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
const params = new SqlParams();
|
|
53
|
+
const rows = chunks.map((chunk) => `(${params.add(chunk.id)}, ${params.add(chunk.content)}, ${params.add(chunk.source ?? null)}, ${params.add(chunk.metadata === undefined ? null : JSON.stringify(chunk.metadata))}::jsonb)`);
|
|
54
|
+
await this.client.query(`INSERT INTO ${this.table} (id, content, source, metadata) VALUES ${rows.join(', ')}
|
|
55
|
+
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, source = EXCLUDED.source, metadata = EXCLUDED.metadata`, params.values);
|
|
56
|
+
}
|
|
57
|
+
async delete(ids) {
|
|
58
|
+
if (this.options.shared || ids.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
await this.client.query(`DELETE FROM ${this.table} WHERE id = ANY($1::text[])`, [textArray(ids)]);
|
|
61
|
+
}
|
|
62
|
+
async retrieve(query, options = {}) {
|
|
63
|
+
options.signal?.throwIfAborted();
|
|
64
|
+
const params = new SqlParams();
|
|
65
|
+
const text = params.add(query);
|
|
66
|
+
const language = `'${this.language}'::regconfig`;
|
|
67
|
+
const tsquery = this.options.match === 'all'
|
|
68
|
+
? `websearch_to_tsquery(${language}, ${text})`
|
|
69
|
+
: `NULLIF(replace(plainto_tsquery(${language}, ${text})::text, ' & ', ' | '), '')::tsquery`;
|
|
70
|
+
const conditions = ['fts @@ q.query'];
|
|
71
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
72
|
+
conditions.push(`metadata @> ${params.add(JSON.stringify(options.filter))}::jsonb`);
|
|
73
|
+
}
|
|
74
|
+
const limit = params.add(options.topK ?? 5);
|
|
75
|
+
const { rows } = await this.client.query(`SELECT id, content, source, metadata::text AS metadata, ts_rank_cd(fts, q.query, 32) AS score
|
|
76
|
+
FROM ${this.table}, (SELECT ${tsquery} AS query) AS q
|
|
77
|
+
WHERE ${conditions.join(' AND ')}
|
|
78
|
+
ORDER BY score DESC, id LIMIT ${limit}`, params.values);
|
|
79
|
+
return rows.map((row) => ({
|
|
80
|
+
id: row.id,
|
|
81
|
+
content: row.content,
|
|
82
|
+
...(row.source === null ? {} : { source: row.source }),
|
|
83
|
+
...(row.metadata === null ? {} : { metadata: fromJson(row.metadata) }),
|
|
84
|
+
score: Number(row.score),
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
async size() {
|
|
88
|
+
const { rows } = await this.client.query(`SELECT count(*)::int AS count FROM ${this.table}`);
|
|
89
|
+
return Number(rows[0]?.count ?? 0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RagChunk } from '../hallucination/rag.js';
|
|
2
|
+
import type { VectorSearchResult } from '../hallucination/retrieval.js';
|
|
3
|
+
import type { RetrieveOptions, SparseRetriever } from './retrievers.js';
|
|
4
|
+
/** Options for the Elasticsearch keyword index. */
|
|
5
|
+
export interface ElasticsearchKeywordIndexOptions {
|
|
6
|
+
/** The cluster, such as `http://localhost:9200`, an Elastic Cloud endpoint, or an OpenSearch domain. */
|
|
7
|
+
url: string;
|
|
8
|
+
/** The index chunks are kept in. */
|
|
9
|
+
index: string;
|
|
10
|
+
/** Sent as `Authorization: ApiKey <key>`. */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
/** Sent as basic authentication, for a cluster with users instead of API keys. */
|
|
13
|
+
username?: string;
|
|
14
|
+
/** The password for `username`. */
|
|
15
|
+
password?: string;
|
|
16
|
+
/** The analyzer `migrate()` gives the `content` field, such as `english`. Defaults to the cluster's standard one. */
|
|
17
|
+
analyzer?: string;
|
|
18
|
+
/**
|
|
19
|
+
* When a write becomes searchable. `wait_for` (the default) returns once a search would see it, as
|
|
20
|
+
* the vector stores do; `false` returns sooner and leaves it to the index's refresh interval.
|
|
21
|
+
*/
|
|
22
|
+
refresh?: 'wait_for' | boolean;
|
|
23
|
+
/** Replaces the global `fetch`, for a proxy, retries, signed requests, or tests. */
|
|
24
|
+
fetch?: typeof globalThis.fetch;
|
|
25
|
+
/** Headers added to every request. */
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
/** Aborts a request that takes longer, in milliseconds. Defaults to 30 seconds. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Raised when the cluster answers with an error. */
|
|
31
|
+
export declare class ElasticsearchError extends Error {
|
|
32
|
+
/** The HTTP status the cluster answered with. */
|
|
33
|
+
readonly status: number;
|
|
34
|
+
/** The cluster's response body, as text. */
|
|
35
|
+
readonly body?: string | undefined;
|
|
36
|
+
constructor(message: string,
|
|
37
|
+
/** The HTTP status the cluster answered with. */
|
|
38
|
+
status: number,
|
|
39
|
+
/** The cluster's response body, as text. */
|
|
40
|
+
body?: string | undefined);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Keyword search in Elasticsearch or OpenSearch, through their REST API.
|
|
44
|
+
*
|
|
45
|
+
* No client library is a dependency: the index speaks HTTP through `fetch`, as the vector stores do,
|
|
46
|
+
* so it runs wherever `fetch` does. Scores are the engine's own BM25, ranked in the cluster, so only
|
|
47
|
+
* the best matches come back. A `SparseRetriever`, so `hybridRetriever()` fuses it with any vector
|
|
48
|
+
* store. A metadata filter is an exact match on top-level fields, as in the vector stores.
|
|
49
|
+
*/
|
|
50
|
+
export declare class ElasticsearchKeywordIndex implements SparseRetriever {
|
|
51
|
+
private readonly options;
|
|
52
|
+
private readonly base;
|
|
53
|
+
private readonly fetchImpl;
|
|
54
|
+
constructor(options: ElasticsearchKeywordIndexOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Creates the index unless it exists: `content` as analyzed text, `id` and `source` as keywords,
|
|
57
|
+
* and every string under `metadata` as a keyword, so a filter matches it exactly. Never runs
|
|
58
|
+
* implicitly.
|
|
59
|
+
*/
|
|
60
|
+
migrate(): Promise<void>;
|
|
61
|
+
/** Adds chunks, or replaces those whose id exists, in one bulk request. */
|
|
62
|
+
add(chunks: readonly RagChunk[]): Promise<void>;
|
|
63
|
+
/** Removes chunks by id. An id that is not held is ignored. */
|
|
64
|
+
delete(ids: readonly string[]): Promise<void>;
|
|
65
|
+
/** The chunks that best match a query's terms, best first, scored by the engine's BM25. */
|
|
66
|
+
retrieve(query: string, options?: RetrieveOptions): Promise<VectorSearchResult[]>;
|
|
67
|
+
/** Chunks held. */
|
|
68
|
+
size(): Promise<number>;
|
|
69
|
+
private bulk;
|
|
70
|
+
private request;
|
|
71
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { linkSignals } from '../utils/signals.js';
|
|
2
|
+
export class ElasticsearchError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
this.name = 'ElasticsearchError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class ElasticsearchKeywordIndex {
|
|
13
|
+
options;
|
|
14
|
+
base;
|
|
15
|
+
fetchImpl;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(options.index)) {
|
|
19
|
+
throw new RangeError(`"${options.index}" is not a valid index name: use lowercase letters, digits, ".", "_", and "-"`);
|
|
20
|
+
}
|
|
21
|
+
this.base = options.url.replace(/\/+$/, '');
|
|
22
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
23
|
+
}
|
|
24
|
+
async migrate() {
|
|
25
|
+
const existing = await this.request('HEAD', `/${this.options.index}`, undefined, { allowNotFound: true });
|
|
26
|
+
if (existing !== undefined)
|
|
27
|
+
return;
|
|
28
|
+
await this.request('PUT', `/${this.options.index}`, {
|
|
29
|
+
mappings: {
|
|
30
|
+
dynamic_templates: [
|
|
31
|
+
{
|
|
32
|
+
metadata_strings: { path_match: 'metadata.*', match_mapping_type: 'string', mapping: { type: 'keyword' } },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
properties: {
|
|
36
|
+
id: { type: 'keyword' },
|
|
37
|
+
content: { type: 'text', ...(this.options.analyzer ? { analyzer: this.options.analyzer } : {}) },
|
|
38
|
+
source: { type: 'keyword' },
|
|
39
|
+
metadata: { type: 'object', dynamic: true },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async add(chunks) {
|
|
45
|
+
if (chunks.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
const lines = chunks.flatMap((chunk) => [
|
|
48
|
+
{ index: { _index: this.options.index, _id: chunk.id } },
|
|
49
|
+
{ id: chunk.id, content: chunk.content, source: chunk.source ?? null, metadata: chunk.metadata ?? null },
|
|
50
|
+
]);
|
|
51
|
+
await this.bulk(lines);
|
|
52
|
+
}
|
|
53
|
+
async delete(ids) {
|
|
54
|
+
if (ids.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
await this.bulk(ids.map((id) => ({ delete: { _index: this.options.index, _id: id } })));
|
|
57
|
+
}
|
|
58
|
+
async retrieve(query, options = {}) {
|
|
59
|
+
const filter = Object.entries(options.filter ?? {});
|
|
60
|
+
const clauses = [];
|
|
61
|
+
for (const [key, value] of filter) {
|
|
62
|
+
if (value === null)
|
|
63
|
+
clauses.push({ bool: { must_not: { exists: { field: `metadata.${key}` } } } });
|
|
64
|
+
else if (['string', 'number', 'boolean'].includes(typeof value))
|
|
65
|
+
clauses.push({ term: { [`metadata.${key}`]: value } });
|
|
66
|
+
else
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const result = (await this.request('POST', `/${this.options.index}/_search`, {
|
|
70
|
+
size: options.topK ?? 5,
|
|
71
|
+
query: { bool: { must: [{ match: { content: { query, operator: 'or' } } }], filter: clauses } },
|
|
72
|
+
_source: ['id', 'content', 'source', 'metadata'],
|
|
73
|
+
}, { signal: options.signal }));
|
|
74
|
+
return (result.hits?.hits ?? []).map((hit) => ({
|
|
75
|
+
id: hit._source?.id ?? hit._id,
|
|
76
|
+
content: hit._source?.content ?? '',
|
|
77
|
+
...(hit._source?.source ? { source: hit._source.source } : {}),
|
|
78
|
+
...(hit._source?.metadata ? { metadata: hit._source.metadata } : {}),
|
|
79
|
+
score: hit._score ?? 0,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
async size() {
|
|
83
|
+
const result = (await this.request('GET', `/${this.options.index}/_count`));
|
|
84
|
+
return result.count ?? 0;
|
|
85
|
+
}
|
|
86
|
+
async bulk(lines) {
|
|
87
|
+
const refresh = this.options.refresh ?? 'wait_for';
|
|
88
|
+
const result = (await this.request('POST', `/_bulk${refresh === false ? '' : `?refresh=${refresh === true ? 'true' : 'wait_for'}`}`, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`));
|
|
89
|
+
if (!result.errors)
|
|
90
|
+
return;
|
|
91
|
+
const failed = (result.items ?? [])
|
|
92
|
+
.map((item) => Object.entries(item)[0])
|
|
93
|
+
.map((entry) => (entry?.[1]?.error && !(entry[0] === 'delete' && entry[1].status === 404) ? entry[1] : undefined))
|
|
94
|
+
.find((outcome) => outcome !== undefined);
|
|
95
|
+
if (failed)
|
|
96
|
+
throw new ElasticsearchError(`A bulk write failed: ${failed.error?.reason ?? 'unknown reason'}`, failed.status ?? 500);
|
|
97
|
+
}
|
|
98
|
+
async request(method, path, body, options = {}) {
|
|
99
|
+
const timeout = AbortSignal.timeout(this.options.timeoutMs ?? 30_000);
|
|
100
|
+
const linked = linkSignals([timeout, options.signal]);
|
|
101
|
+
const ndjson = typeof body === 'string';
|
|
102
|
+
const authorization = this.options.apiKey
|
|
103
|
+
? `ApiKey ${this.options.apiKey}`
|
|
104
|
+
: this.options.username !== undefined
|
|
105
|
+
? `Basic ${btoa(`${this.options.username}:${this.options.password ?? ''}`)}`
|
|
106
|
+
: undefined;
|
|
107
|
+
try {
|
|
108
|
+
const response = await this.fetchImpl(`${this.base}${path}`, {
|
|
109
|
+
method,
|
|
110
|
+
headers: {
|
|
111
|
+
...(body === undefined ? {} : { 'content-type': ndjson ? 'application/x-ndjson' : 'application/json' }),
|
|
112
|
+
...(authorization ? { authorization } : {}),
|
|
113
|
+
...this.options.headers,
|
|
114
|
+
},
|
|
115
|
+
...(body === undefined ? {} : { body: ndjson ? body : JSON.stringify(body) }),
|
|
116
|
+
signal: linked.signal,
|
|
117
|
+
});
|
|
118
|
+
if (response.status === 404 && options.allowNotFound)
|
|
119
|
+
return undefined;
|
|
120
|
+
const text = method === 'HEAD' ? '' : await response.text();
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new ElasticsearchError(`Elasticsearch ${method} ${path} failed with ${response.status}`, response.status, text);
|
|
123
|
+
}
|
|
124
|
+
return text ? JSON.parse(text) : {};
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
linked.dispose();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|