nexus-ai-pro 2.2.0 → 2.4.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 +225 -0
- package/CHANGELOG.md +325 -1
- package/README.md +20 -6
- package/SECURITY.md +140 -0
- package/dist/adapter-kit/contracts.d.ts +87 -0
- package/dist/adapter-kit/contracts.js +237 -0
- package/dist/adapter-kit/errors.d.ts +39 -0
- package/dist/adapter-kit/errors.js +52 -0
- package/dist/adapter-kit/index.d.ts +144 -0
- package/dist/adapter-kit/index.js +178 -0
- package/dist/adapter-kit/semver.d.ts +12 -0
- package/dist/adapter-kit/semver.js +55 -0
- 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 +142 -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/core/secure-stream.js +4 -3
- package/dist/deep-agent/index.d.ts +109 -0
- package/dist/deep-agent/index.js +216 -0
- package/dist/embeddings/adapters.js +2 -1
- package/dist/evals/judge.js +2 -5
- package/dist/evaluate/cache.d.ts +0 -14
- package/dist/evaluate/cache.js +0 -30
- package/dist/evaluate/file-cache.d.ts +15 -0
- package/dist/evaluate/file-cache.js +30 -0
- package/dist/evaluate/index.d.ts +2 -1
- package/dist/evaluate/index.js +2 -1
- package/dist/evaluate/review.js +2 -2
- package/dist/evaluate/run.js +2 -2
- package/dist/evaluate/version.js +3 -3
- package/dist/graph/functional.d.ts +9 -0
- package/dist/graph/functional.js +20 -3
- package/dist/graph/graph.js +27 -6
- package/dist/graph/lint.d.ts +4 -1
- package/dist/graph/lint.js +35 -0
- package/dist/graph/node-cache.js +3 -3
- package/dist/hallucination/rag.d.ts +4 -1
- package/dist/hallucination/rag.js +6 -1
- package/dist/images/asset-stores.js +2 -3
- package/dist/images/asset-support.js +2 -2
- package/dist/images/assets.js +1 -2
- package/dist/images/evals.js +4 -4
- package/dist/insights/proposals.js +1 -2
- package/dist/insights/regressions.d.ts +26 -2
- package/dist/insights/regressions.js +129 -3
- package/dist/loaders/git.js +8 -1
- package/dist/loaders/web.js +7 -1
- package/dist/mcp/client.d.ts +14 -0
- package/dist/mcp/client.js +59 -1
- package/dist/mcp/registry.d.ts +6 -0
- package/dist/mcp/registry.js +7 -0
- package/dist/postgres/fulltext.d.ts +67 -0
- package/dist/postgres/fulltext.js +91 -0
- package/dist/prompts/file.js +28 -11
- package/dist/protocols/a2a.d.ts +235 -0
- package/dist/protocols/a2a.js +386 -0
- package/dist/protocols/acp.d.ts +87 -0
- package/dist/protocols/acp.js +322 -0
- package/dist/protocols/ag-ui.d.ts +120 -0
- package/dist/protocols/ag-ui.js +180 -0
- package/dist/protocols/shared.d.ts +80 -0
- package/dist/protocols/shared.js +49 -0
- package/dist/providers/anthropic.js +1 -0
- package/dist/providers/cohere.js +2 -2
- package/dist/providers/google.js +2 -11
- package/dist/providers/openai.js +1 -0
- package/dist/rag/elasticsearch.d.ts +71 -0
- package/dist/rag/elasticsearch.js +130 -0
- package/dist/rag/file-ingestion.js +15 -9
- 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/runtime/detect.d.ts +17 -0
- package/dist/runtime/detect.js +16 -0
- package/dist/runtime/index.d.ts +19 -0
- package/dist/runtime/index.js +7 -0
- package/dist/security/injection-detector.d.ts +2 -0
- package/dist/security/injection-detector.js +7 -1
- package/dist/security/input-guard.js +5 -2
- package/dist/security/output-guard.js +8 -4
- package/dist/security/pii-detector.js +22 -6
- package/dist/security/prompt-hardening.js +6 -2
- package/dist/security/semantic-injection-classifier.d.ts +2 -1
- package/dist/security/semantic-injection-classifier.js +31 -35
- package/dist/security/upload-scanner.d.ts +8 -0
- package/dist/security/upload-scanner.js +51 -9
- 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/server/deployments.d.ts +63 -1
- package/dist/server/deployments.js +75 -2
- package/dist/server/runs.d.ts +9 -0
- package/dist/server/runs.js +20 -0
- package/dist/server/server.js +5 -0
- package/dist/store/memory.d.ts +10 -0
- package/dist/store/memory.js +10 -0
- package/dist/testing/record.js +2 -2
- package/dist/tracing/stores.js +2 -1
- package/dist/tracing/tracer.js +2 -2
- package/dist/types/config.d.ts +20 -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 +8 -9
- package/dist/utils/base64.d.ts +4 -0
- package/dist/utils/base64.js +21 -0
- package/dist/utils/checksums.d.ts +22 -0
- package/dist/utils/checksums.js +39 -0
- package/dist/utils/ids.d.ts +8 -0
- package/dist/utils/ids.js +26 -2
- package/dist/utils/sha256.d.ts +22 -0
- package/dist/utils/sha256.js +120 -0
- 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/adapter-kit/contracts.d.ts +87 -0
- package/dist-cjs/adapter-kit/contracts.js +243 -0
- package/dist-cjs/adapter-kit/errors.d.ts +39 -0
- package/dist-cjs/adapter-kit/errors.js +57 -0
- package/dist-cjs/adapter-kit/index.d.ts +144 -0
- package/dist-cjs/adapter-kit/index.js +194 -0
- package/dist-cjs/adapter-kit/semver.d.ts +12 -0
- package/dist-cjs/adapter-kit/semver.js +59 -0
- 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 +147 -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/core/secure-stream.js +4 -3
- package/dist-cjs/deep-agent/index.d.ts +109 -0
- package/dist-cjs/deep-agent/index.js +219 -0
- package/dist-cjs/embeddings/adapters.js +2 -1
- package/dist-cjs/evals/judge.js +2 -5
- package/dist-cjs/evaluate/cache.d.ts +0 -14
- package/dist-cjs/evaluate/cache.js +1 -65
- package/dist-cjs/evaluate/file-cache.d.ts +15 -0
- package/dist-cjs/evaluate/file-cache.js +67 -0
- package/dist-cjs/evaluate/index.d.ts +2 -1
- package/dist-cjs/evaluate/index.js +3 -2
- package/dist-cjs/evaluate/review.js +2 -2
- package/dist-cjs/evaluate/run.js +2 -2
- package/dist-cjs/evaluate/version.js +3 -3
- package/dist-cjs/graph/functional.d.ts +9 -0
- package/dist-cjs/graph/functional.js +20 -3
- package/dist-cjs/graph/graph.js +27 -6
- package/dist-cjs/graph/lint.d.ts +4 -1
- package/dist-cjs/graph/lint.js +35 -0
- package/dist-cjs/graph/node-cache.js +3 -3
- package/dist-cjs/hallucination/rag.d.ts +4 -1
- package/dist-cjs/hallucination/rag.js +6 -1
- package/dist-cjs/images/asset-stores.js +2 -3
- package/dist-cjs/images/asset-support.js +2 -2
- package/dist-cjs/images/assets.js +1 -2
- package/dist-cjs/images/evals.js +4 -4
- package/dist-cjs/insights/proposals.js +1 -2
- package/dist-cjs/insights/regressions.d.ts +26 -2
- package/dist-cjs/insights/regressions.js +129 -3
- package/dist-cjs/loaders/git.js +8 -1
- package/dist-cjs/loaders/web.js +7 -1
- package/dist-cjs/mcp/client.d.ts +14 -0
- package/dist-cjs/mcp/client.js +60 -1
- package/dist-cjs/mcp/registry.d.ts +6 -0
- package/dist-cjs/mcp/registry.js +7 -0
- package/dist-cjs/postgres/fulltext.d.ts +67 -0
- package/dist-cjs/postgres/fulltext.js +129 -0
- package/dist-cjs/prompts/file.js +28 -11
- package/dist-cjs/protocols/a2a.d.ts +235 -0
- package/dist-cjs/protocols/a2a.js +393 -0
- package/dist-cjs/protocols/acp.d.ts +87 -0
- package/dist-cjs/protocols/acp.js +326 -0
- package/dist-cjs/protocols/ag-ui.d.ts +120 -0
- package/dist-cjs/protocols/ag-ui.js +186 -0
- package/dist-cjs/protocols/shared.d.ts +80 -0
- package/dist-cjs/protocols/shared.js +59 -0
- package/dist-cjs/providers/anthropic.js +1 -0
- package/dist-cjs/providers/cohere.js +2 -2
- package/dist-cjs/providers/google.js +3 -12
- package/dist-cjs/providers/openai.js +1 -0
- package/dist-cjs/rag/elasticsearch.d.ts +71 -0
- package/dist-cjs/rag/elasticsearch.js +135 -0
- package/dist-cjs/rag/file-ingestion.js +14 -8
- 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/runtime/detect.d.ts +17 -0
- package/dist-cjs/runtime/detect.js +19 -0
- package/dist-cjs/runtime/index.d.ts +19 -0
- package/dist-cjs/runtime/index.js +31 -0
- package/dist-cjs/security/injection-detector.d.ts +2 -0
- package/dist-cjs/security/injection-detector.js +7 -1
- package/dist-cjs/security/input-guard.js +5 -2
- package/dist-cjs/security/output-guard.js +8 -4
- package/dist-cjs/security/pii-detector.js +22 -6
- package/dist-cjs/security/prompt-hardening.js +6 -2
- package/dist-cjs/security/semantic-injection-classifier.d.ts +2 -1
- package/dist-cjs/security/semantic-injection-classifier.js +31 -35
- package/dist-cjs/security/upload-scanner.d.ts +8 -0
- package/dist-cjs/security/upload-scanner.js +53 -9
- 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/server/deployments.d.ts +63 -1
- package/dist-cjs/server/deployments.js +75 -2
- package/dist-cjs/server/runs.d.ts +9 -0
- package/dist-cjs/server/runs.js +20 -0
- package/dist-cjs/server/server.js +5 -0
- package/dist-cjs/store/memory.d.ts +10 -0
- package/dist-cjs/store/memory.js +10 -0
- package/dist-cjs/testing/record.js +2 -2
- package/dist-cjs/tracing/stores.js +2 -1
- package/dist-cjs/tracing/tracer.js +2 -2
- package/dist-cjs/types/config.d.ts +20 -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 +8 -9
- package/dist-cjs/utils/base64.d.ts +4 -0
- package/dist-cjs/utils/base64.js +25 -0
- package/dist-cjs/utils/checksums.d.ts +22 -0
- package/dist-cjs/utils/checksums.js +46 -0
- package/dist-cjs/utils/ids.d.ts +8 -0
- package/dist-cjs/utils/ids.js +28 -2
- package/dist-cjs/utils/sha256.d.ts +22 -0
- package/dist-cjs/utils/sha256.js +125 -0
- 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 +7 -3
- package/package.json +193 -6
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { modelCallsInTurn } from './shared.js';
|
|
2
|
+
export class ModelCallLimitError extends Error {
|
|
3
|
+
scope;
|
|
4
|
+
limit;
|
|
5
|
+
code = 'MODEL_CALL_LIMIT';
|
|
6
|
+
constructor(scope, limit) {
|
|
7
|
+
super(`The agent reached its limit of ${limit} model calls per ${scope}`);
|
|
8
|
+
this.scope = scope;
|
|
9
|
+
this.limit = limit;
|
|
10
|
+
this.name = 'ModelCallLimitError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const NAMESPACE = ['nexus', 'agent', 'model-calls'];
|
|
14
|
+
export function modelCallLimit(options) {
|
|
15
|
+
if (options.run === undefined && options.thread === undefined) {
|
|
16
|
+
throw new RangeError('modelCallLimit() needs a run limit, a thread limit, or both');
|
|
17
|
+
}
|
|
18
|
+
const local = new Map();
|
|
19
|
+
return {
|
|
20
|
+
name: 'model-call-limit',
|
|
21
|
+
async wrapModelCall(request, next, context) {
|
|
22
|
+
let reached;
|
|
23
|
+
if (options.run !== undefined && modelCallsInTurn(context.state.messages) >= options.run) {
|
|
24
|
+
reached = ['run', options.run];
|
|
25
|
+
}
|
|
26
|
+
let used = 0;
|
|
27
|
+
if (!reached && options.thread !== undefined) {
|
|
28
|
+
used = context.store
|
|
29
|
+
? ((await context.store.get(NAMESPACE, context.threadId))?.value ?? 0)
|
|
30
|
+
: (local.get(context.threadId) ?? 0);
|
|
31
|
+
if (used >= options.thread)
|
|
32
|
+
reached = ['thread', options.thread];
|
|
33
|
+
}
|
|
34
|
+
if (reached) {
|
|
35
|
+
const [scope, limit] = reached;
|
|
36
|
+
if (options.onLimit === 'error')
|
|
37
|
+
throw new ModelCallLimitError(scope, limit);
|
|
38
|
+
return context.stop(options.message ?? `I stopped here: this ${scope} reached its limit of ${limit} model calls.`);
|
|
39
|
+
}
|
|
40
|
+
if (options.thread !== undefined) {
|
|
41
|
+
if (context.store)
|
|
42
|
+
await context.store.put(NAMESPACE, context.threadId, used + 1);
|
|
43
|
+
else {
|
|
44
|
+
local.set(context.threadId, used + 1);
|
|
45
|
+
if (local.size > 10_000)
|
|
46
|
+
local.delete(local.keys().next().value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return next(request);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CompletionRequest } from '../../types/messages.js';
|
|
2
|
+
import type { AgentMiddleware, AgentMiddlewareContext, AgentModelClient } from '../create-agent.js';
|
|
3
|
+
/** A model to fall back to: a model name for the agent's own client, or another client. */
|
|
4
|
+
export type ModelFallbackTarget = string | {
|
|
5
|
+
client: AgentModelClient;
|
|
6
|
+
model?: string;
|
|
7
|
+
};
|
|
8
|
+
/** One fallback about to happen. */
|
|
9
|
+
export interface ModelFallbackEvent {
|
|
10
|
+
/** The model that failed. */
|
|
11
|
+
from: string;
|
|
12
|
+
/** The model tried next. */
|
|
13
|
+
to: string;
|
|
14
|
+
/** What the failed call threw. */
|
|
15
|
+
error: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** Options for `modelFallback()`. */
|
|
18
|
+
export interface ModelFallbackOptions {
|
|
19
|
+
/** Whether an error moves on to the next model. Defaults to every error but a cancellation. */
|
|
20
|
+
fallbackOn?: (error: unknown, model: string) => boolean;
|
|
21
|
+
/** Called before each fallback, for logs and metrics. */
|
|
22
|
+
onFallback?: (event: ModelFallbackEvent) => void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tries other models, in order, when the model call fails.
|
|
26
|
+
*
|
|
27
|
+
* The client's router already falls back between providers for one request; this is the fallback
|
|
28
|
+
* it cannot make: to a client of a different kind, or once the client's own routes are exhausted. A
|
|
29
|
+
* target with its own client is called through `complete()`, never streamed, and middleware after
|
|
30
|
+
* this one does not wrap it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function modelFallback(models: readonly ModelFallbackTarget[], options?: ModelFallbackOptions): AgentMiddleware;
|
|
33
|
+
/** What `dynamicModel()` decides from: the step's context and the request about to be sent. */
|
|
34
|
+
export type DynamicModelContext = AgentMiddlewareContext & {
|
|
35
|
+
request: CompletionRequest;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Chooses the model for each call from the run so far.
|
|
39
|
+
*
|
|
40
|
+
* A cheap model for the first steps and a stronger one once the task proves hard, a model with a
|
|
41
|
+
* longer context once the transcript grows, or a tenant's own model. Returning nothing keeps the
|
|
42
|
+
* model the request already names.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* dynamicModel(({ state }) => (state.iterations >= 4 ? 'gpt-5' : 'gpt-5-mini'))
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function dynamicModel(select: (context: DynamicModelContext) => string | undefined | Promise<string | undefined>): AgentMiddleware;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isCancellation } from './shared.js';
|
|
2
|
+
export function modelFallback(models, options = {}) {
|
|
3
|
+
if (models.length === 0)
|
|
4
|
+
throw new RangeError('modelFallback() needs at least one model to fall back to');
|
|
5
|
+
return {
|
|
6
|
+
name: 'model-fallback',
|
|
7
|
+
async wrapModelCall(request, next, context) {
|
|
8
|
+
try {
|
|
9
|
+
return await next(request);
|
|
10
|
+
}
|
|
11
|
+
catch (first) {
|
|
12
|
+
let error = first;
|
|
13
|
+
let from = request.model;
|
|
14
|
+
for (const target of models) {
|
|
15
|
+
if (isCancellation(error, context.signal) || !(options.fallbackOn?.(error, from) ?? true))
|
|
16
|
+
throw error;
|
|
17
|
+
const model = typeof target === 'string' ? target : (target.model ?? request.model);
|
|
18
|
+
options.onFallback?.({ from, to: model, error });
|
|
19
|
+
try {
|
|
20
|
+
return typeof target === 'string'
|
|
21
|
+
? await next({ ...request, model })
|
|
22
|
+
: await target.client.complete({ ...request, model });
|
|
23
|
+
}
|
|
24
|
+
catch (failure) {
|
|
25
|
+
error = failure;
|
|
26
|
+
from = model;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function dynamicModel(select) {
|
|
35
|
+
return {
|
|
36
|
+
name: 'dynamic-model',
|
|
37
|
+
async beforeModel(context) {
|
|
38
|
+
const model = await select(context);
|
|
39
|
+
if (!model || model === context.request.model)
|
|
40
|
+
return;
|
|
41
|
+
return { ...context.request, model };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentMiddleware } from '../create-agent.js';
|
|
2
|
+
/** The personal data and secrets `piiMiddleware()` finds on its own. */
|
|
3
|
+
export type PiiKind = 'email' | 'credit-card' | 'ssn' | 'phone' | 'iban' | 'ip-address' | 'secret';
|
|
4
|
+
/**
|
|
5
|
+
* What happens to a match: `redact` replaces it with its kind, `mask` keeps its last four
|
|
6
|
+
* characters, `hash` replaces it with a stable token so two values stay distinguishable, and `block`
|
|
7
|
+
* refuses the whole message.
|
|
8
|
+
*/
|
|
9
|
+
export type PiiStrategy = 'redact' | 'mask' | 'hash' | 'block';
|
|
10
|
+
/** Where a match was found. */
|
|
11
|
+
export type PiiWhere = 'input' | 'output' | 'tool';
|
|
12
|
+
/** One match, as `onDetect` sees it: never the value itself. */
|
|
13
|
+
export interface PiiFinding {
|
|
14
|
+
/** Its kind: a built-in kind or a name from `patterns`. */
|
|
15
|
+
kind: string;
|
|
16
|
+
/** Where it was found. */
|
|
17
|
+
where: PiiWhere;
|
|
18
|
+
/** What was done with it. */
|
|
19
|
+
strategy: PiiStrategy;
|
|
20
|
+
}
|
|
21
|
+
/** Options for `piiMiddleware()`. */
|
|
22
|
+
export interface PiiMiddlewareOptions {
|
|
23
|
+
/** The built-in kinds to find. Defaults to all of them. */
|
|
24
|
+
kinds?: readonly PiiKind[];
|
|
25
|
+
/** Kinds of your own, by name: `{ 'employee-id': /\bEMP-\d{6}\b/ }`. */
|
|
26
|
+
patterns?: Readonly<Record<string, RegExp>>;
|
|
27
|
+
/**
|
|
28
|
+
* What happens to a match, for every kind or per kind: `'mask'`, or
|
|
29
|
+
* `{ email: 'hash', 'credit-card': 'block' }`. Kinds not named are redacted.
|
|
30
|
+
*/
|
|
31
|
+
strategy?: PiiStrategy | Readonly<Record<string, PiiStrategy>>;
|
|
32
|
+
/**
|
|
33
|
+
* Where to look. `input` is every message sent to the model but the system prompt; `output` is
|
|
34
|
+
* the model's answer; `toolResults` cleans a result before it enters the transcript, so it is never
|
|
35
|
+
* checkpointed either. Defaults to input and tool results.
|
|
36
|
+
*/
|
|
37
|
+
apply?: {
|
|
38
|
+
input?: boolean;
|
|
39
|
+
output?: boolean;
|
|
40
|
+
toolResults?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** Called for each match, for an audit trail. */
|
|
43
|
+
onDetect?: (finding: PiiFinding) => void;
|
|
44
|
+
}
|
|
45
|
+
/** Thrown when a match under the `block` strategy is found in the model's input or output. */
|
|
46
|
+
export declare class PiiBlockedError extends Error {
|
|
47
|
+
/** The kind that was found. */
|
|
48
|
+
readonly kind: string;
|
|
49
|
+
/** Where it was found. */
|
|
50
|
+
readonly where: PiiWhere;
|
|
51
|
+
/** Always `PII_BLOCKED`. */
|
|
52
|
+
readonly code = "PII_BLOCKED";
|
|
53
|
+
constructor(
|
|
54
|
+
/** The kind that was found. */
|
|
55
|
+
kind: string,
|
|
56
|
+
/** Where it was found. */
|
|
57
|
+
where: PiiWhere);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Keeps personal data and secrets away from the model, the transcript, or both.
|
|
61
|
+
*
|
|
62
|
+
* Finds emails, card numbers (checked with Luhn), US social security numbers, phone numbers, IBANs
|
|
63
|
+
* (checked with their checksum), IP addresses, and common API keys and private keys, plus any kinds
|
|
64
|
+
* of your own; and redacts, masks, hashes, or blocks each. The validated kinds keep false positives
|
|
65
|
+
* low: an order number is not a card number unless its checksum says so.
|
|
66
|
+
*/
|
|
67
|
+
export declare function piiMiddleware(options?: PiiMiddlewareOptions): AgentMiddleware;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ibanChecksum, luhn, phoneNumber } from '../../utils/checksums.js';
|
|
2
|
+
import { shortHash } from './shared.js';
|
|
3
|
+
export class PiiBlockedError extends Error {
|
|
4
|
+
kind;
|
|
5
|
+
where;
|
|
6
|
+
code = 'PII_BLOCKED';
|
|
7
|
+
constructor(kind, where) {
|
|
8
|
+
super(`The ${where === 'input' ? 'request' : 'response'} contains ${kind}, which is blocked`);
|
|
9
|
+
this.kind = kind;
|
|
10
|
+
this.where = where;
|
|
11
|
+
this.name = 'PiiBlockedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const DETECTORS = {
|
|
15
|
+
secret: {
|
|
16
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----|\b(?:sk-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|xox[abprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{35})\b/g,
|
|
17
|
+
},
|
|
18
|
+
email: { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g },
|
|
19
|
+
'credit-card': { pattern: /\b\d(?:[ -]?\d){12,18}\b/g, valid: luhn },
|
|
20
|
+
iban: { pattern: /\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]{4}){2,7}(?: ?[A-Z0-9]{1,4})?\b/g, valid: ibanChecksum },
|
|
21
|
+
ssn: { pattern: /\b(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b/g },
|
|
22
|
+
'ip-address': { pattern: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g },
|
|
23
|
+
phone: {
|
|
24
|
+
pattern: /(?<![\w+])\+?(?:\(\d{1,4}\)|\d{1,4})(?:[ .-]?(?:\(\d{1,4}\)|\d{2,4})){2,4}(?!\w)/g,
|
|
25
|
+
valid: phoneNumber,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const ORDER = ['secret', 'email', 'credit-card', 'iban', 'ssn', 'ip-address', 'phone'];
|
|
29
|
+
export function piiMiddleware(options = {}) {
|
|
30
|
+
const kinds = new Set(options.kinds ?? ORDER);
|
|
31
|
+
const detectors = [
|
|
32
|
+
...ORDER.filter((kind) => kinds.has(kind)).map((kind) => [kind, DETECTORS[kind]]),
|
|
33
|
+
...Object.entries(options.patterns ?? {}).map(([kind, pattern]) => [
|
|
34
|
+
kind,
|
|
35
|
+
{ pattern: pattern.flags.includes('g') ? pattern : new RegExp(pattern.source, `${pattern.flags}g`) },
|
|
36
|
+
]),
|
|
37
|
+
];
|
|
38
|
+
const apply = { input: true, output: false, toolResults: true, ...options.apply };
|
|
39
|
+
const strategyOf = (kind) => typeof options.strategy === 'string' ? options.strategy : (options.strategy?.[kind] ?? 'redact');
|
|
40
|
+
const clean = (text, where) => {
|
|
41
|
+
let current = text;
|
|
42
|
+
for (const [kind, detector] of detectors) {
|
|
43
|
+
const strategy = strategyOf(kind);
|
|
44
|
+
let blocked = false;
|
|
45
|
+
current = current.replace(detector.pattern, (match) => {
|
|
46
|
+
if (detector.valid && !detector.valid(match))
|
|
47
|
+
return match;
|
|
48
|
+
options.onDetect?.({ kind, where, strategy });
|
|
49
|
+
if (strategy === 'block')
|
|
50
|
+
blocked = true;
|
|
51
|
+
return replace(match, kind, strategy);
|
|
52
|
+
});
|
|
53
|
+
if (blocked)
|
|
54
|
+
return { blocked: kind };
|
|
55
|
+
}
|
|
56
|
+
return { text: current };
|
|
57
|
+
};
|
|
58
|
+
const cleanMessage = (message, where) => {
|
|
59
|
+
if (typeof message.content === 'string') {
|
|
60
|
+
const result = clean(message.content, where);
|
|
61
|
+
if ('blocked' in result)
|
|
62
|
+
throw new PiiBlockedError(result.blocked, where);
|
|
63
|
+
return result.text === message.content ? message : { ...message, content: result.text };
|
|
64
|
+
}
|
|
65
|
+
let changed = false;
|
|
66
|
+
const content = message.content.map((part) => {
|
|
67
|
+
if (part.type !== 'text')
|
|
68
|
+
return part;
|
|
69
|
+
const result = clean(part.text, where);
|
|
70
|
+
if ('blocked' in result)
|
|
71
|
+
throw new PiiBlockedError(result.blocked, where);
|
|
72
|
+
if (result.text === part.text)
|
|
73
|
+
return part;
|
|
74
|
+
changed = true;
|
|
75
|
+
return { ...part, text: result.text };
|
|
76
|
+
});
|
|
77
|
+
return changed ? { ...message, content } : message;
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
name: 'pii',
|
|
81
|
+
beforeModel({ request }) {
|
|
82
|
+
if (!apply.input)
|
|
83
|
+
return;
|
|
84
|
+
return {
|
|
85
|
+
...request,
|
|
86
|
+
messages: request.messages.map((message) => message.role === 'system' ? message : cleanMessage(message, 'input')),
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
afterModel({ response }) {
|
|
90
|
+
if (!apply.output || !response.content)
|
|
91
|
+
return;
|
|
92
|
+
const result = clean(response.content, 'output');
|
|
93
|
+
if ('blocked' in result)
|
|
94
|
+
throw new PiiBlockedError(result.blocked, 'output');
|
|
95
|
+
return result.text === response.content ? undefined : { ...response, content: result.text };
|
|
96
|
+
},
|
|
97
|
+
async wrapToolCall(_call, next) {
|
|
98
|
+
const result = await next();
|
|
99
|
+
if (!apply.toolResults || !result.ok)
|
|
100
|
+
return result;
|
|
101
|
+
let blocked;
|
|
102
|
+
const walk = (value) => {
|
|
103
|
+
if (blocked)
|
|
104
|
+
return value;
|
|
105
|
+
if (typeof value === 'string') {
|
|
106
|
+
const cleaned = clean(value, 'tool');
|
|
107
|
+
if ('blocked' in cleaned) {
|
|
108
|
+
blocked = cleaned.blocked;
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
return cleaned.text;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(value))
|
|
114
|
+
return value.map(walk);
|
|
115
|
+
if (isToolOutput(value)) {
|
|
116
|
+
const content = value.content.map((part) => part.type === 'text' ? { ...part, text: walk(part.text) } : part);
|
|
117
|
+
return { ...value, content };
|
|
118
|
+
}
|
|
119
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
120
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, walk(entry)]));
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
};
|
|
124
|
+
const cleaned = walk(result.result);
|
|
125
|
+
if (blocked)
|
|
126
|
+
return { ok: false, error: `The tool's result was withheld: it contains ${blocked}` };
|
|
127
|
+
return { ...result, result: cleaned };
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function replace(match, kind, strategy) {
|
|
132
|
+
if (strategy === 'mask') {
|
|
133
|
+
const visible = match.replace(/[^A-Za-z0-9]/g, '').slice(-4);
|
|
134
|
+
return `${'*'.repeat(Math.max(4, match.length - visible.length))}${visible}`;
|
|
135
|
+
}
|
|
136
|
+
if (strategy === 'hash')
|
|
137
|
+
return `[${kind}:${shortHash(match)}]`;
|
|
138
|
+
return `[${kind}]`;
|
|
139
|
+
}
|
|
140
|
+
function isToolOutput(value) {
|
|
141
|
+
return value?.type === 'tool_output' && Array.isArray(value.content);
|
|
142
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { NexusResponse } from '../../types/response.js';
|
|
2
|
+
import type { AgentMiddleware, AgentToolCall, AgentToolResult } from '../create-agent.js';
|
|
3
|
+
import { type RetryBackoff } from './shared.js';
|
|
4
|
+
/** One retry about to happen. */
|
|
5
|
+
export interface RetryEvent {
|
|
6
|
+
/** The attempt that failed, from 1. */
|
|
7
|
+
attempt: number;
|
|
8
|
+
/** How long until the next attempt, in milliseconds. */
|
|
9
|
+
delayMs: number;
|
|
10
|
+
/** The error a model call threw, when it threw. */
|
|
11
|
+
error?: unknown;
|
|
12
|
+
/** The failed tool call's result, for a tool retry. */
|
|
13
|
+
result?: AgentToolResult;
|
|
14
|
+
/** The tool's name, for a tool retry. */
|
|
15
|
+
tool?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Options for `modelRetry()`. */
|
|
18
|
+
export interface ModelRetryOptions {
|
|
19
|
+
/** Attempts in all, the first included. Defaults to 3. */
|
|
20
|
+
attempts?: number;
|
|
21
|
+
/** How long to wait between attempts. */
|
|
22
|
+
backoff?: RetryBackoff;
|
|
23
|
+
/**
|
|
24
|
+
* Whether an error is worth another attempt. Defaults to every error not marked
|
|
25
|
+
* `retryable: false`, such as a provider's authentication error, and never a cancellation.
|
|
26
|
+
*/
|
|
27
|
+
retryOn?: (error: unknown, attempt: number) => boolean;
|
|
28
|
+
/** Whether a response that did arrive is tried again, such as an empty one. Defaults to never. */
|
|
29
|
+
retryResponse?: (response: NexusResponse, attempt: number) => boolean;
|
|
30
|
+
/** Called before each new attempt, for logs and metrics. */
|
|
31
|
+
onRetry?: (event: RetryEvent) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Tries a failed model call again, with exponential backoff.
|
|
35
|
+
*
|
|
36
|
+
* The client already retries one provider request; this retries the agent's step as a whole, after
|
|
37
|
+
* the client gave up, and can retry a response that arrived but is unusable. A streamed call that
|
|
38
|
+
* fails halfway streams again from the start. Waits stop when the run is cancelled.
|
|
39
|
+
*/
|
|
40
|
+
export declare function modelRetry(options?: ModelRetryOptions): AgentMiddleware;
|
|
41
|
+
/** Options for `toolRetry()`. */
|
|
42
|
+
export interface ToolRetryOptions {
|
|
43
|
+
/** Attempts in all, the first included. Defaults to 3. */
|
|
44
|
+
attempts?: number;
|
|
45
|
+
/** How long to wait between attempts. */
|
|
46
|
+
backoff?: RetryBackoff;
|
|
47
|
+
/**
|
|
48
|
+
* Which calls may run again: tool names, `'all'`, or a function of the call. Defaults to calls
|
|
49
|
+
* that are safe to repeat, whose tool declares capabilities and none of them writes, runs a
|
|
50
|
+
* command or code, reaches the network, or is the application's own. A call that may already have
|
|
51
|
+
* changed something is retried only when you say so.
|
|
52
|
+
*/
|
|
53
|
+
tools?: readonly string[] | 'all' | ((call: AgentToolCall) => boolean);
|
|
54
|
+
/** Whether a result is a failure worth another attempt. Defaults to any result with `ok: false`. */
|
|
55
|
+
retryOn?: (result: AgentToolResult, call: AgentToolCall, attempt: number) => boolean;
|
|
56
|
+
/** Called before each new attempt, for logs and metrics. */
|
|
57
|
+
onRetry?: (event: RetryEvent) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs a failed tool call again, with exponential backoff.
|
|
61
|
+
*
|
|
62
|
+
* By default only a call that cannot have changed anything is repeated: retrying a payment or a
|
|
63
|
+
* write that timed out after it landed would do it twice. Name the tools that are idempotent to
|
|
64
|
+
* retry those too.
|
|
65
|
+
*/
|
|
66
|
+
export declare function toolRetry(options?: ToolRetryOptions): AgentMiddleware;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isSensitiveCapability } from '../capabilities.js';
|
|
2
|
+
import { backoffDelay, isCancellation, sleep } from './shared.js';
|
|
3
|
+
export function modelRetry(options = {}) {
|
|
4
|
+
const attempts = Math.max(1, options.attempts ?? 3);
|
|
5
|
+
const retryOn = options.retryOn ?? ((error) => error?.retryable !== false);
|
|
6
|
+
return {
|
|
7
|
+
name: 'model-retry',
|
|
8
|
+
async wrapModelCall(request, next, context) {
|
|
9
|
+
for (let attempt = 1;; attempt += 1) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await next(request);
|
|
12
|
+
if (attempt < attempts && options.retryResponse?.(response, attempt)) {
|
|
13
|
+
const delayMs = backoffDelay(attempt, options.backoff);
|
|
14
|
+
options.onRetry?.({ attempt, delayMs });
|
|
15
|
+
await sleep(delayMs, context.signal);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
return response;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (attempt >= attempts || isCancellation(error, context.signal) || !retryOn(error, attempt))
|
|
22
|
+
throw error;
|
|
23
|
+
const delayMs = backoffDelay(attempt, options.backoff);
|
|
24
|
+
options.onRetry?.({ attempt, delayMs, error });
|
|
25
|
+
await sleep(delayMs, context.signal);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function toolRetry(options = {}) {
|
|
32
|
+
const attempts = Math.max(1, options.attempts ?? 3);
|
|
33
|
+
const retryOn = options.retryOn ?? ((result) => !result.ok);
|
|
34
|
+
const eligible = (call) => {
|
|
35
|
+
const tools = options.tools;
|
|
36
|
+
if (tools === 'all')
|
|
37
|
+
return true;
|
|
38
|
+
if (typeof tools === 'function')
|
|
39
|
+
return tools(call);
|
|
40
|
+
if (tools)
|
|
41
|
+
return tools.includes(call.name);
|
|
42
|
+
return call.capabilities !== undefined && !call.capabilities.some(isSensitiveCapability);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
name: 'tool-retry',
|
|
46
|
+
async wrapToolCall(call, next, context) {
|
|
47
|
+
if (!eligible(call))
|
|
48
|
+
return next();
|
|
49
|
+
for (let attempt = 1;; attempt += 1) {
|
|
50
|
+
const result = await next();
|
|
51
|
+
if (attempt >= attempts || context.signal.aborted || !retryOn(result, call, attempt))
|
|
52
|
+
return result;
|
|
53
|
+
const delayMs = backoffDelay(attempt, options.backoff);
|
|
54
|
+
options.onRetry?.({ attempt, delayMs, result, tool: call.name });
|
|
55
|
+
await sleep(delayMs, context.signal);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Message } from '../../types/messages.js';
|
|
2
|
+
import type { AgentToolCall } from '../create-agent.js';
|
|
3
|
+
/**
|
|
4
|
+
* What the middleware share. Every count here is read from the transcript rather than kept in
|
|
5
|
+
* memory, so it survives a restart, a resumed approval, and a second process, and two threads never
|
|
6
|
+
* share one.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The current turn: every message after the latest user message. One `invoke()` is one turn; a run
|
|
10
|
+
* that resumes after an approval is the same turn.
|
|
11
|
+
*/
|
|
12
|
+
export declare function turnOf(messages: readonly Message[]): readonly Message[];
|
|
13
|
+
/** Model calls made in the current turn: its assistant messages. */
|
|
14
|
+
export declare function modelCallsInTurn(messages: readonly Message[]): number;
|
|
15
|
+
/**
|
|
16
|
+
* Where a call sits among the current turn's calls to the same tool, from 1. Calls the model made in
|
|
17
|
+
* parallel are ordered as the model listed them, so the position never depends on which finished
|
|
18
|
+
* first.
|
|
19
|
+
*/
|
|
20
|
+
export declare function callPosition(messages: readonly Message[], call: Pick<AgentToolCall, 'id' | 'name'>): number;
|
|
21
|
+
/** A message's text: the string, or its text parts joined. */
|
|
22
|
+
export declare function textOf(content: Message['content']): string;
|
|
23
|
+
/** How long to wait between attempts. */
|
|
24
|
+
export interface RetryBackoff {
|
|
25
|
+
/** The wait before the second attempt, in milliseconds. Defaults to 500. */
|
|
26
|
+
initialMs?: number;
|
|
27
|
+
/** The longest wait, in milliseconds. Defaults to 10,000. */
|
|
28
|
+
maxMs?: number;
|
|
29
|
+
/** What each wait is multiplied by. Defaults to 2. */
|
|
30
|
+
factor?: number;
|
|
31
|
+
/** Spreads each wait between half and all of it, so runs that failed together retry apart. Defaults to true. */
|
|
32
|
+
jitter?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** The wait before attempt `attempt + 1`, after `attempt` failed. */
|
|
35
|
+
export declare function backoffDelay(attempt: number, backoff?: RetryBackoff): number;
|
|
36
|
+
/** Waits, or rejects with the signal's reason as soon as it aborts. */
|
|
37
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
38
|
+
/** True for a cancellation: never worth retrying or falling back from. */
|
|
39
|
+
export declare function isCancellation(error: unknown, signal?: AbortSignal): boolean;
|
|
40
|
+
/** A short, stable hash of a text: cyrb53 as 8 hex digits. Not a security boundary. */
|
|
41
|
+
export declare function shortHash(text: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function turnOf(messages) {
|
|
2
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
3
|
+
if (messages[index]?.role === 'user')
|
|
4
|
+
return messages.slice(index + 1);
|
|
5
|
+
}
|
|
6
|
+
return messages;
|
|
7
|
+
}
|
|
8
|
+
export function modelCallsInTurn(messages) {
|
|
9
|
+
return turnOf(messages).filter((message) => message.role === 'assistant').length;
|
|
10
|
+
}
|
|
11
|
+
export function callPosition(messages, call) {
|
|
12
|
+
let position = 0;
|
|
13
|
+
for (const message of turnOf(messages)) {
|
|
14
|
+
for (const made of message.toolCalls ?? []) {
|
|
15
|
+
if (made.function.name !== call.name)
|
|
16
|
+
continue;
|
|
17
|
+
position += 1;
|
|
18
|
+
if (made.id === call.id)
|
|
19
|
+
return position;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return position + 1;
|
|
23
|
+
}
|
|
24
|
+
export function textOf(content) {
|
|
25
|
+
if (typeof content === 'string')
|
|
26
|
+
return content;
|
|
27
|
+
return content
|
|
28
|
+
.filter((part) => part.type === 'text')
|
|
29
|
+
.map((part) => part.text)
|
|
30
|
+
.join('\n');
|
|
31
|
+
}
|
|
32
|
+
export function backoffDelay(attempt, backoff = {}) {
|
|
33
|
+
const initial = backoff.initialMs ?? 500;
|
|
34
|
+
const delay = Math.min(backoff.maxMs ?? 10_000, initial * (backoff.factor ?? 2) ** Math.max(0, attempt - 1));
|
|
35
|
+
return backoff.jitter === false ? delay : delay / 2 + Math.random() * (delay / 2);
|
|
36
|
+
}
|
|
37
|
+
export function sleep(ms, signal) {
|
|
38
|
+
if (ms <= 0)
|
|
39
|
+
return signal?.aborted ? Promise.reject(signal.reason) : Promise.resolve();
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
if (signal?.aborted) {
|
|
42
|
+
reject(signal.reason);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const onAbort = () => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
reject(signal?.reason);
|
|
48
|
+
};
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
signal?.removeEventListener('abort', onAbort);
|
|
51
|
+
resolve();
|
|
52
|
+
}, ms);
|
|
53
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function isCancellation(error, signal) {
|
|
57
|
+
if (signal?.aborted)
|
|
58
|
+
return true;
|
|
59
|
+
const name = error?.name;
|
|
60
|
+
return name === 'AbortError';
|
|
61
|
+
}
|
|
62
|
+
export function shortHash(text) {
|
|
63
|
+
let h1 = 0xdeadbeef;
|
|
64
|
+
let h2 = 0x41c6ce57;
|
|
65
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
66
|
+
const code = text.charCodeAt(index);
|
|
67
|
+
h1 = Math.imul(h1 ^ code, 2654435761);
|
|
68
|
+
h2 = Math.imul(h2 ^ code, 1597334677);
|
|
69
|
+
}
|
|
70
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
71
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
72
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, '0').slice(-8);
|
|
73
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CompletionRequest, ToolDefinition } from '../../types/messages.js';
|
|
2
|
+
import type { AgentMiddleware, AgentMiddlewareContext } from '../create-agent.js';
|
|
3
|
+
/** What a rule chooses tools from. */
|
|
4
|
+
export interface ToolSelectionContext extends AgentMiddlewareContext {
|
|
5
|
+
/** The request about to be sent. */
|
|
6
|
+
request: CompletionRequest;
|
|
7
|
+
/** Every tool the request offers. */
|
|
8
|
+
tools: readonly ToolDefinition[];
|
|
9
|
+
/** What the turn is about: the text `query` returned. */
|
|
10
|
+
query: string;
|
|
11
|
+
}
|
|
12
|
+
/** Options for `toolSelector()`. Give it `select`, `embed`, or both. */
|
|
13
|
+
export interface ToolSelectorOptions {
|
|
14
|
+
/**
|
|
15
|
+
* A rule: the names of the tools to send. With `embed` too, the rule narrows the candidates and
|
|
16
|
+
* similarity ranks what is left.
|
|
17
|
+
*/
|
|
18
|
+
select?: (context: ToolSelectionContext) => readonly string[] | Promise<readonly string[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Embeds texts, for choosing by similarity: any embedding function, such as one from
|
|
21
|
+
* `toEmbeddingFunction()`. Each tool is embedded once, from `describe`; the query once per turn.
|
|
22
|
+
*/
|
|
23
|
+
embed?: (texts: string[]) => Promise<readonly (readonly number[])[]> | readonly (readonly number[])[];
|
|
24
|
+
/** The most tools sent, `always` included. Defaults to 12. */
|
|
25
|
+
maxTools?: number;
|
|
26
|
+
/** The lowest similarity a tool needs to be sent, from -1 to 1. Defaults to none. */
|
|
27
|
+
minScore?: number;
|
|
28
|
+
/** Tools sent on every call, whatever the turn is about. */
|
|
29
|
+
always?: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* Keeps every tool the model already called this turn, so a tool it is halfway through using
|
|
32
|
+
* never disappears from under it. Defaults to true.
|
|
33
|
+
*/
|
|
34
|
+
keepUsed?: boolean;
|
|
35
|
+
/** What the turn is about. Defaults to the latest user message. */
|
|
36
|
+
query?: (context: AgentMiddlewareContext & {
|
|
37
|
+
request: CompletionRequest;
|
|
38
|
+
}) => string;
|
|
39
|
+
/** The text a tool is embedded from. Defaults to its name and description. */
|
|
40
|
+
describe?: (tool: ToolDefinition) => string;
|
|
41
|
+
/** Called with each selection, for logs and for measuring what the selector saves. */
|
|
42
|
+
onSelect?: (selection: ToolSelection) => void;
|
|
43
|
+
}
|
|
44
|
+
/** What `toolSelector()` sent for one call. */
|
|
45
|
+
export interface ToolSelection {
|
|
46
|
+
/** The query the tools were chosen for. */
|
|
47
|
+
query: string;
|
|
48
|
+
/** The names of the tools sent. */
|
|
49
|
+
selected: string[];
|
|
50
|
+
/** How many tools the request offered. */
|
|
51
|
+
offered: number;
|
|
52
|
+
/** Each ranked tool's similarity to the query, when tools were chosen by similarity. */
|
|
53
|
+
scores?: Record<string, number>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sends a model only the tools relevant to the turn.
|
|
57
|
+
*
|
|
58
|
+
* An agent with 150 tools otherwise sends 150 schemas on every call: the tokens are paid each time,
|
|
59
|
+
* the call is slower, and a model choosing among that many picks worse. A rule, embedding
|
|
60
|
+
* similarity, or both choose a dozen; tools named in `always`, the tool a `toolChoice` forces, and
|
|
61
|
+
* the tools the model already used this turn are always kept. The agent can still run every tool —
|
|
62
|
+
* only what the model is offered changes.
|
|
63
|
+
*/
|
|
64
|
+
export declare function toolSelector(options: ToolSelectorOptions): AgentMiddleware;
|