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
package/API_STABILITY.md
CHANGED
|
@@ -41,6 +41,45 @@ Both are covered by this policy:
|
|
|
41
41
|
Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
|
|
42
42
|
one of its explicit subpaths.
|
|
43
43
|
|
|
44
|
+
## 2.3 stage (2.3.0)
|
|
45
|
+
|
|
46
|
+
Everything 2.3 adds is stable from the start and additive: no default changes meaning.
|
|
47
|
+
|
|
48
|
+
- `ToolDefinition.capabilities` and the capability grammar; `permissionPolicy()`, `PermissionPolicy`,
|
|
49
|
+
`PermissionRules`, `PermissionRequest`, `PermissionVerdict`, `PermissionDecision`,
|
|
50
|
+
`PermissionPolicyLike`, `parseCapability()`, `capabilitiesOf()`, `isSensitiveCapability()`, and
|
|
51
|
+
`normalizePath()` (`nexus-ai-pro/agent/permissions`); `CreateAgentOptions.permissions`;
|
|
52
|
+
- the sandbox contract: `Sandbox`, its option, result, and isolation types, `sandboxTools()`,
|
|
53
|
+
`runSandboxConformance()` and its report types, `SandboxPathError` (`nexus-ai-pro/agent/sandbox`).
|
|
54
|
+
`processSandbox()` is a development reference and may change; conformance checks may be added;
|
|
55
|
+
- `NodeOptions.effects` and `interrupts`, `CompileOptions.tools`, `GraphToolDescription`, and the lint
|
|
56
|
+
codes `SENSITIVE_TOOL_WITHOUT_APPROVAL`, `UNDECLARED_TOOL_CAPABILITIES`, and
|
|
57
|
+
`SIDE_EFFECT_BEFORE_INTERRUPT`;
|
|
58
|
+
- `nexus-ai-pro/agent/middleware` and every middleware and option type on it;
|
|
59
|
+
`AgentMiddleware.wrapModelCall`, `AgentMiddlewareContext`, `AgentModelCallContext`, the third
|
|
60
|
+
argument of `wrapToolCall`, `next(call)`, and `AgentStopReason` `'stopped'`. The PII patterns
|
|
61
|
+
may be tightened in a minor release to cut false positives, and new `PiiKind` members may be
|
|
62
|
+
added;
|
|
63
|
+
- `nexus-ai-pro/server/auth`: `jwtAuth()`, `apiKeyAuth()`, `trustedProxyAuth()`, `anyAuth()`,
|
|
64
|
+
`createJwtVerifier()`, `hashApiKey()`, `JwtError` and its codes, and every option type. New
|
|
65
|
+
`JwtAlgorithm` and `JwtErrorCode` members may be added;
|
|
66
|
+
- `Principal.roles`, `method`, and `claims`; `GraphRunOptions.principal`, `NodeContext.principal`,
|
|
67
|
+
`WorkflowRunOptions.principal`, and `WorkflowContext.principal`; `ToolContext` and the second
|
|
68
|
+
argument of `ToolDefinition.execute`; `AgentMiddlewareContext.principal` and
|
|
69
|
+
`PermissionRequest.principal`. A checkpoint's `metadata.userId` is recorded when a run has a
|
|
70
|
+
principal with a subject;
|
|
71
|
+
- `SparseRetriever`; `PostgresKeywordIndex` and `keywordIndexMigrations()`
|
|
72
|
+
(`nexus-ai-pro/postgres/fulltext`); `ElasticsearchKeywordIndex` and `ElasticsearchError`
|
|
73
|
+
(`nexus-ai-pro/rag/elasticsearch`); and every reranker and option type on
|
|
74
|
+
`nexus-ai-pro/rag/rerankers`. The default endpoints of hosted rerankers follow their providers'
|
|
75
|
+
current API versions;
|
|
76
|
+
- `nexus-ai-pro/rag/pipeline`: `createIngestionPipeline()`, `ingestionExecutor()`, and their types.
|
|
77
|
+
The manifest's layout under `['nexus', 'ingestion', <name>]` is stable: a later release reads a
|
|
78
|
+
manifest an earlier one wrote;
|
|
79
|
+
- `nexus-ai-pro/deep-agent`: `createDeepAgent()`, `DeepAgentOptions`, `DeepSubagent`, and
|
|
80
|
+
`DeepSkill`, and the names and arguments of the tools it gives the model (`write_todos`,
|
|
81
|
+
`delegate`, `load_skill`, `edit_file`). The wording of its system prompt may change in any release.
|
|
82
|
+
|
|
44
83
|
## 2.2 stage (2.2.0)
|
|
45
84
|
|
|
46
85
|
Everything 2.2 adds is stable from the start and additive: no default changes meaning.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,146 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.3.0] - 2026-10-06
|
|
8
|
+
|
|
9
|
+
Agents that act safely, on production retrieval.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Capabilities and permission policies.** A tool declares what each call does —
|
|
14
|
+
`filesystem:write:<path>`, `network:<host>`, `shell:<command>`, `code`, or an effect of the
|
|
15
|
+
application's own — as a list or a function of its arguments. `permissionPolicy()`, on the new
|
|
16
|
+
`nexus-ai-pro/agent/permissions`, grants filesystem globs, network hosts, and shell command
|
|
17
|
+
prefixes, and decides each call: allow, deny, or ask. Paths are normalized before any grant sees
|
|
18
|
+
them, and a command that chains, pipes, or substitutes is never granted by a prefix.
|
|
19
|
+
`createAgent({ permissions })` enforces it before a tool runs: a denied call never runs, an `ask`
|
|
20
|
+
interrupts as `interruptOn` does, and arguments an approver edits are decided again.
|
|
21
|
+
- **A sandbox contract**, on the new `nexus-ai-pro/agent/sandbox`: the `Sandbox` interface a
|
|
22
|
+
container, VM, or hosted interpreter fills; `sandboxTools()`, whose calls declare exactly the
|
|
23
|
+
command or path they use; `runSandboxConformance()`, which proves any sandbox against the
|
|
24
|
+
contract; and `processSandbox()`, a reference for development that is never a security boundary
|
|
25
|
+
and says so.
|
|
26
|
+
- **The linter reads capabilities.** `SENSITIVE_TOOL_WITHOUT_APPROVAL` reports a tool that writes, runs
|
|
27
|
+
commands or code, or reaches the network with neither approval nor a policy;
|
|
28
|
+
`UNDECLARED_TOOL_CAPABILITIES` a tool whose effects cannot be judged; and
|
|
29
|
+
`SIDE_EFFECT_BEFORE_INTERRUPT` a node that declares `effects` and `interrupts`. An agent's graph
|
|
30
|
+
describes its tools for it.
|
|
31
|
+
- **A middleware catalog**, on the new `nexus-ai-pro/agent/middleware`, each a separate import:
|
|
32
|
+
`modelRetry()`, `modelFallback()` (to another model or another client), `dynamicModel()`,
|
|
33
|
+
`toolRetry()` (which by default repeats only a call that cannot have changed anything),
|
|
34
|
+
`toolSelector()` (by rule, embedding similarity, or both), `contextEditor()` (clears old tool
|
|
35
|
+
results in batches, so a prompt cache keeps working), `piiMiddleware()` (validated card numbers
|
|
36
|
+
and IBANs; redact, mask, hash, or block), `humanApproval()` (per call, in band or out of band),
|
|
37
|
+
`modelCallLimit()` (per run and per thread, the thread count shared through the store), and
|
|
38
|
+
`filesystemContext()` (instruction files in, large results out to a file). The three existing
|
|
39
|
+
middleware are there too.
|
|
40
|
+
- **A wider middleware seam.** `wrapModelCall` wraps the model call itself, and may call `next()`
|
|
41
|
+
again to retry or fall back. Every hook receives an `AgentMiddlewareContext`: the state, the
|
|
42
|
+
store, the thread and tenant, the run's signal, and `interrupt()`. `context.stop()` ends a run
|
|
43
|
+
with an answer and the new `stopReason: 'stopped'`. A `wrapToolCall` can pass `next()` changed
|
|
44
|
+
arguments, which the permission policy decides again.
|
|
45
|
+
- **Server authentication**, on the new `nexus-ai-pro/server/auth`. It is built on Web Crypto, with no
|
|
46
|
+
dependency:
|
|
47
|
+
- `jwtAuth()` takes an identity provider's key set (cached, and refetched when a token names a new
|
|
48
|
+
key), a public key, or a shared secret, and checks issuer, audience, expiry, and age. It refuses
|
|
49
|
+
`alg: none` and key confusion, and requires an audience with a key set.
|
|
50
|
+
- `apiKeyAuth()` compares keys by their SHA-256 hash, from `hashApiKey()`.
|
|
51
|
+
- `trustedProxyAuth()` refuses to exist without proof that the proxy sent the request.
|
|
52
|
+
- `anyAuth()` combines hooks.
|
|
53
|
+
- `createJwtVerifier()` verifies tokens outside the server.
|
|
54
|
+
- **One Principal, everywhere a run goes.** It gains `roles`, `method`, and `claims`.
|
|
55
|
+
- Graph and workflow runs take a `principal`, which nodes read as `context.principal`. Its tenant
|
|
56
|
+
becomes the run's, and its subject is recorded on every checkpoint and in the run's trace.
|
|
57
|
+
- `graphAssistant()` passes the authenticated caller on.
|
|
58
|
+
- Tools receive a `ToolContext` as the second argument of `execute`: the principal, the thread
|
|
59
|
+
and tenant, the run's signal, and the tenant's store.
|
|
60
|
+
- Middleware and permission policies see the principal too.
|
|
61
|
+
- **Sparse retrieval as a contract.** A `SparseRetriever` is keyword search that takes writes, so it
|
|
62
|
+
fuses with vector search and stays in step with a vector store. `KeywordIndex` is one. Two more
|
|
63
|
+
ship:
|
|
64
|
+
- `PostgresKeywordIndex` (`nexus-ai-pro/postgres/fulltext`) uses Postgres full-text search, with a
|
|
65
|
+
GIN index and ranking in the database. With `shared`, it searches a pgvector table through a
|
|
66
|
+
generated column, so hybrid search keeps one copy of the data.
|
|
67
|
+
- `ElasticsearchKeywordIndex` (`nexus-ai-pro/rag/elasticsearch`) works with Elasticsearch or
|
|
68
|
+
OpenSearch over the REST API, with no client library.
|
|
69
|
+
- **Rerankers**, on the new `nexus-ai-pro/rag/rerankers`, with no SDK:
|
|
70
|
+
- `cohereReranker()`, `voyageReranker()`, and `jinaReranker()` for hosted APIs;
|
|
71
|
+
- `teiReranker()` for a self-hosted cross-encoder;
|
|
72
|
+
- `httpReranker()` for any other API;
|
|
73
|
+
- `crossEncoderReranker()` for a model run in process.
|
|
74
|
+
- **Durable ingestion**, on the new `nexus-ai-pro/rag/pipeline`. `createIngestionPipeline()` keeps a
|
|
75
|
+
manifest in any `Store`: each document's content version, its chunking, the embedding model, and
|
|
76
|
+
each chunk's version. A run:
|
|
77
|
+
- embeds only new and changed chunks; chunks are named by their text, so one that only moved keeps
|
|
78
|
+
its embedding;
|
|
79
|
+
- deletes the chunks and documents that are gone;
|
|
80
|
+
- keeps keyword indexes in step;
|
|
81
|
+
- writes to the stores before the manifest.
|
|
82
|
+
|
|
83
|
+
`ingestionExecutor()` runs it as a durable operation, and a retry starts at the document the
|
|
84
|
+
failed attempt stopped on.
|
|
85
|
+
- **A deep-agent preset**, on the new `nexus-ai-pro/deep-agent`. `createDeepAgent()` is
|
|
86
|
+
`createAgent()` composed with:
|
|
87
|
+
- a plan, kept in the transcript;
|
|
88
|
+
- a sandboxed workspace, with `edit_file` for precise changes and commands granted by name;
|
|
89
|
+
- helpers behind one `delegate` tool, which never get a tool that needs approval;
|
|
90
|
+
- skills loaded on demand;
|
|
91
|
+
- instruction files and context offloading;
|
|
92
|
+
- context editing, optional tool selection, and a default permission policy.
|
|
93
|
+
|
|
94
|
+
It returns the same compiled graph as any agent; it is not a new engine.
|
|
95
|
+
- `processSandbox()` loads Node's modules when first used, so `nexus-ai-pro/agent/sandbox`, and a
|
|
96
|
+
deep agent over a container sandbox, import on any runtime.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- `MemoryStore` swept every item for expiry on every write, so writing n items took time
|
|
101
|
+
proportional to n². It now sweeps only once an item has a time to live, and at most once a second.
|
|
102
|
+
Expired items were already hidden from reads.
|
|
103
|
+
|
|
104
|
+
- `limitToolCalls()` counted calls in memory shared by every run of an agent, so one thread's calls
|
|
105
|
+
used up another's limit, and the count never reset. It now counts per run from the transcript.
|
|
106
|
+
- A subgraph run through `asNode()` now runs for its parent's tenant, so its store is the tenant's
|
|
107
|
+
view, as the parent's is.
|
|
108
|
+
- `sandboxTools()` file tools accept a path that names the mount, such as `/workspace/notes.txt`,
|
|
109
|
+
as well as one relative to it, and say in their descriptions which to use.
|
|
110
|
+
|
|
111
|
+
### Proof
|
|
112
|
+
|
|
113
|
+
- In a real agent tool loop, a write outside the workspace and a request to an unlisted host are
|
|
114
|
+
refused before their tools run, and a granted command that needs approval waits for a person.
|
|
115
|
+
- The process sandbox passes the conformance suite, and a sandbox that leaks the host's environment
|
|
116
|
+
and writes outside its root fails exactly those two checks.
|
|
117
|
+
- An agent over 150 tools completes 40 tasks, a third of them needing two tools, with
|
|
118
|
+
`toolSelector()` at equal quality: every task succeeds either way. Input tokens per task fall from
|
|
119
|
+
about 64,000 to about 5,200, and latency by about 70%: the model's time computed from
|
|
120
|
+
each request's size, plus the agent's own measured time. Two experiments, reported by
|
|
121
|
+
`compareExperiments()`, show it.
|
|
122
|
+
- Each middleware is proved in a real agent loop. For example:
|
|
123
|
+
- a charge that timed out is never retried;
|
|
124
|
+
- an approver who raises a refund past what the policy allows has the edit refused;
|
|
125
|
+
- two agents sharing a store share one thread budget;
|
|
126
|
+
- a 45,000-character tool result reaches the transcript as a preview and a file.
|
|
127
|
+
- A JWT from a test identity provider is verified against its key set and reaches an agent's tool
|
|
128
|
+
call as a `Principal` with its subject, tenant, roles, and scopes.
|
|
129
|
+
- The tool's writes land in that tenant's view of the store.
|
|
130
|
+
- The checkpoint records who ran the step.
|
|
131
|
+
- Another tenant's token gets 404 for the thread.
|
|
132
|
+
- Expired, wrong-audience, wrong-issuer, unsigned, and forged tokens are refused.
|
|
133
|
+
- The provider rotates its key with one refetch.
|
|
134
|
+
- On the stored support dataset, on real Postgres with full-text search and pgvector, hybrid retrieval
|
|
135
|
+
beats vector-only: recall@5 0.71 → 0.97 and mean reciprocal rank 0.51 → 0.71, both with a 95%
|
|
136
|
+
interval above zero. A hosted reranker and a local cross-encoder, compared in the same experiment,
|
|
137
|
+
each lift mean reciprocal rank to 0.97. One contract test runs against all four keyword indexes.
|
|
138
|
+
- An ingestion of 100,000 documents dies halfway inside a durable operation. Its retry starts at the
|
|
139
|
+
document the first attempt stopped on, and embeds no chunk that had finished. A second run after
|
|
140
|
+
ten documents are edited embeds exactly ten chunks and deletes their old versions.
|
|
141
|
+
- A deep agent completes a multi-step task on a fixture repository inside the reference sandbox. It
|
|
142
|
+
plans, runs the failing test, reads and edits the bug, verifies the fix, has a helper review it,
|
|
143
|
+
loads a skill, and records the change; the test then passes outside the agent. Along the way, a
|
|
144
|
+
30,000-character log is offloaded to a file. The default policy refuses an unlisted command, a
|
|
145
|
+
chained command, a write outside the workspace, and an ungranted tool.
|
|
146
|
+
|
|
7
147
|
## [2.2.0] - 2026-10-05
|
|
8
148
|
|
|
9
149
|
Production scale and safe upgrades. Every database schema is versioned, so an upgrade applies only
|
|
@@ -1637,7 +1777,8 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
1637
1777
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
1638
1778
|
- CLI and audit findings no longer disclose detected secret values.
|
|
1639
1779
|
|
|
1640
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.
|
|
1780
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.3.0...HEAD
|
|
1781
|
+
[2.3.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.2.0...v2.3.0
|
|
1641
1782
|
[2.2.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.1.0...v2.2.0
|
|
1642
1783
|
[2.1.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.0.0...v2.1.0
|
|
1643
1784
|
[2.0.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.25.0...v2.0.0
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ team can share.
|
|
|
8
8
|
Import only what you use. The root import is the client and nothing else, and every other capability
|
|
9
9
|
has its own entry point with a size budget CI enforces; [the packaging guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md)
|
|
10
10
|
publishes what each one costs. The package has no required dependencies: a graph-only application
|
|
11
|
-
loads
|
|
11
|
+
loads 80 KB, and installs nothing but this package.
|
|
12
12
|
|
|
13
13
|
- NPM: https://www.npmjs.com/package/nexus-ai-pro
|
|
14
14
|
- GitHub: https://github.com/mkhitar-abrahamyan/nexus-ai
|
|
@@ -132,7 +132,8 @@ from the doc comments. The guides live in the repository, so these links go to G
|
|
|
132
132
|
| [Providers and routing](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/providers.md) | The twelve completion providers, routing and failover, the model registry |
|
|
133
133
|
| [Guardrails](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/security.md) | Injection, PII, secrets, schema validation, output redaction |
|
|
134
134
|
| [Graphs](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/graphs.md) | Parallel branches, interrupts, checkpoints, time travel, subgraphs, caching, diagrams |
|
|
135
|
-
| [Agents and tools](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/agents.md) | Graph-based agents with approvals and middleware, and the simple tool loop |
|
|
135
|
+
| [Agents and tools](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/agents.md) | Graph-based agents with approvals, capabilities and permission policies, a sandbox contract, the middleware catalog, and the simple tool loop |
|
|
136
|
+
| [The deep-agent preset](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/deep-agents.md) | Long, multi-step agents: a plan, a sandboxed workspace, helpers, skills, and a context that stays small |
|
|
136
137
|
| [Long-term memory](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/memory.md) | Namespaced memory with semantic search, in memory or Redis |
|
|
137
138
|
| [MCP](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/mcp.md) | Borrowing tools from MCP servers, a registry of many, and serving your own |
|
|
138
139
|
| [Traces](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/tracing.md) | Run trees, queries, feedback, comparison, and alerts |
|
|
@@ -251,12 +252,16 @@ Shipped so far:
|
|
|
251
252
|
backend, one event stream, and dashboard rollups;
|
|
252
253
|
- 2.2 production scale and safe upgrades: versioned database migrations, queue claims and routing
|
|
253
254
|
that cost the same at any size, a circuit breaker that counts across workers, one tenant model
|
|
254
|
-
across every store, and `nexus doctor` to check a deployment before it serves traffic
|
|
255
|
+
across every store, and `nexus doctor` to check a deployment before it serves traffic;
|
|
256
|
+
- 2.3 agents that act safely, on production retrieval: capabilities and permission policies, a
|
|
257
|
+
sandbox contract, a middleware catalog, server authentication with one principal on every run,
|
|
258
|
+
keyword search in Postgres and Elasticsearch with rerankers, durable ingestion, and a deep-agent
|
|
259
|
+
preset.
|
|
255
260
|
|
|
256
261
|
Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
|
|
257
262
|
[MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
|
|
258
|
-
Next, 2.
|
|
259
|
-
|
|
263
|
+
Next, 2.4 takes the experimental surfaces out of experimental with evidence, adds an adapter kit, and
|
|
264
|
+
speaks the agent protocols beyond MCP from a portable kernel. The [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists the
|
|
260
265
|
releases after it.
|
|
261
266
|
|
|
262
267
|
## Before Production
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../types/messages.js';
|
|
2
|
+
/**
|
|
3
|
+
* Capabilities: what a tool does to the world, written as strings a policy can decide on.
|
|
4
|
+
*
|
|
5
|
+
* | Capability | Means |
|
|
6
|
+
* | --- | --- |
|
|
7
|
+
* | `filesystem:read`, `filesystem:read:<path>` | Reads files, or one path |
|
|
8
|
+
* | `filesystem:write`, `filesystem:write:<path>` | Writes, creates, or deletes files |
|
|
9
|
+
* | `network`, `network:<host or URL>` | Opens a network connection, to a host when given |
|
|
10
|
+
* | `shell`, `shell:<command line>` | Runs a shell command |
|
|
11
|
+
* | `code` | Runs code it was given, as an interpreter does |
|
|
12
|
+
* | `<name>`, `<name>:<detail>` | Anything the application names, such as `payments:refund` |
|
|
13
|
+
*
|
|
14
|
+
* A tool that declares `capabilities: []` does nothing to the world; one that declares none at all
|
|
15
|
+
* is undeclared, and a policy decides on it as such.
|
|
16
|
+
*/
|
|
17
|
+
/** One capability, split into its parts. */
|
|
18
|
+
export interface ParsedCapability {
|
|
19
|
+
/** `filesystem`, `network`, `shell`, `code`, or the application's own name. */
|
|
20
|
+
kind: string;
|
|
21
|
+
/** `read` or `write`, for the filesystem. */
|
|
22
|
+
access?: 'read' | 'write';
|
|
23
|
+
/** The path, host, command, or detail, when the capability names one. */
|
|
24
|
+
target?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Splits a capability string into its kind, access, and target. */
|
|
27
|
+
export declare function parseCapability(capability: string): ParsedCapability;
|
|
28
|
+
/**
|
|
29
|
+
* The capabilities one call of a tool declares: its static list, or what its function computes from
|
|
30
|
+
* the call's arguments. `undefined` means undeclared — no list, or a function that threw — which is
|
|
31
|
+
* not the same as `[]`, a tool that declares it does nothing to the world.
|
|
32
|
+
*/
|
|
33
|
+
export declare function capabilitiesOf(tool: Pick<ToolDefinition, 'capabilities'> | undefined, args: Record<string, unknown>): string[] | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Whether a capability is sensitive: anything but reading files. A pure tool declares
|
|
36
|
+
* `capabilities: []`; whatever it does declare, a write, a command, code, the network, or an
|
|
37
|
+
* effect of the application's own such as `payments`, changes the world or leaves the process.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isSensitiveCapability(capability: string): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function parseCapability(capability) {
|
|
2
|
+
const text = capability.trim();
|
|
3
|
+
const colon = text.indexOf(':');
|
|
4
|
+
const kind = (colon < 0 ? text : text.slice(0, colon)).toLowerCase();
|
|
5
|
+
const rest = colon < 0 ? undefined : text.slice(colon + 1);
|
|
6
|
+
if (kind === 'filesystem') {
|
|
7
|
+
if (!rest)
|
|
8
|
+
return { kind };
|
|
9
|
+
const [access, ...path] = rest.split(':');
|
|
10
|
+
const parsed = { kind };
|
|
11
|
+
if (access === 'read' || access === 'write')
|
|
12
|
+
parsed.access = access;
|
|
13
|
+
const target = parsed.access ? path.join(':') : rest;
|
|
14
|
+
if (target)
|
|
15
|
+
parsed.target = target;
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
return rest === undefined || rest === '' ? { kind } : { kind, target: rest };
|
|
19
|
+
}
|
|
20
|
+
export function capabilitiesOf(tool, args) {
|
|
21
|
+
const declared = tool?.capabilities;
|
|
22
|
+
if (!declared)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof declared !== 'function')
|
|
25
|
+
return [...declared];
|
|
26
|
+
try {
|
|
27
|
+
return [...declared(args)];
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function isSensitiveCapability(capability) {
|
|
34
|
+
const parsed = parseCapability(capability);
|
|
35
|
+
return !(parsed.kind === 'filesystem' && parsed.access === 'read');
|
|
36
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { OperationLifecycleLike } from '../types/lifecycle.js';
|
|
2
2
|
import { type CompiledGraph } from '../graph/graph.js';
|
|
3
|
-
import type { GraphCheckpointer, NodeContext } from '../types/graph.js';
|
|
3
|
+
import type { GraphCheckpointer, InterruptRequest, NodeContext } from '../types/graph.js';
|
|
4
4
|
import type { CompletionRequest, Message, ToolDefinition } from '../types/messages.js';
|
|
5
5
|
import type { NexusResponse, StreamChunk } from '../types/response.js';
|
|
6
|
+
import type { Principal } from '../types/principal.js';
|
|
6
7
|
import type { Store } from '../types/store.js';
|
|
8
|
+
import type { PermissionPolicyLike } from './permissions.js';
|
|
7
9
|
/**
|
|
8
10
|
* An agent as a graph.
|
|
9
11
|
*
|
|
@@ -28,6 +30,10 @@ export interface AgentToolCall {
|
|
|
28
30
|
name: string;
|
|
29
31
|
/** Arguments, parsed from the model's JSON. */
|
|
30
32
|
args: Record<string, unknown>;
|
|
33
|
+
/** What the call declares it does, when the tool declares capabilities. */
|
|
34
|
+
capabilities?: readonly string[];
|
|
35
|
+
/** Why the permission policy asked for approval, when it did. */
|
|
36
|
+
permission?: string;
|
|
31
37
|
}
|
|
32
38
|
/** What an approver may answer: allow, refuse with a reason, or allow with corrected arguments. */
|
|
33
39
|
export type AgentApproval = boolean | {
|
|
@@ -42,24 +48,58 @@ export interface AgentApprovalPolicy {
|
|
|
42
48
|
/** The question an operator sees. Defaults to naming the tool and its arguments. */
|
|
43
49
|
reason?: (call: AgentToolCall) => string;
|
|
44
50
|
}
|
|
51
|
+
/** What every middleware hook can read about the step it runs in. */
|
|
52
|
+
export interface AgentMiddlewareContext {
|
|
53
|
+
/** The agent's state at this step: the whole transcript, tool calls and results included. */
|
|
54
|
+
state: AgentState;
|
|
55
|
+
/** Long-term memory, when the agent has a store. */
|
|
56
|
+
store?: Store;
|
|
57
|
+
/** The thread the run belongs to. */
|
|
58
|
+
threadId: string;
|
|
59
|
+
/** The tenant the run is for, when it has one. */
|
|
60
|
+
tenantId?: string;
|
|
61
|
+
/** Who the run is for, when it has a principal: the caller the server authenticated. */
|
|
62
|
+
principal?: Readonly<Principal>;
|
|
63
|
+
/** Aborted when the run is cancelled or the step times out. A hook that waits should stop with it. */
|
|
64
|
+
signal: AbortSignal;
|
|
65
|
+
/**
|
|
66
|
+
* Suspends the run until a person answers, as `interruptOn` does. When the run resumes, the step
|
|
67
|
+
* runs again and this returns the answer instead of suspending.
|
|
68
|
+
*/
|
|
69
|
+
interrupt<T = unknown>(request: InterruptRequest): T;
|
|
70
|
+
}
|
|
71
|
+
/** What `wrapModelCall` can read, and how it ends the run without calling the model. */
|
|
72
|
+
export interface AgentModelCallContext extends AgentMiddlewareContext {
|
|
73
|
+
/**
|
|
74
|
+
* Ends the run with this answer and `stopReason: 'stopped'`, without another model call. Return
|
|
75
|
+
* what it returns: `return context.stop('The model call limit was reached.')`.
|
|
76
|
+
*/
|
|
77
|
+
stop(answer: string): NexusResponse;
|
|
78
|
+
}
|
|
45
79
|
/** Hooks around the agent's model calls and tool calls. Each hook is optional. */
|
|
46
80
|
export interface AgentMiddleware {
|
|
47
81
|
/** Names the middleware in traces and errors. */
|
|
48
82
|
name?: string;
|
|
49
83
|
/** Adjusts the request before it is sent: trim history, add context, swap the model. */
|
|
50
|
-
beforeModel?(context: {
|
|
84
|
+
beforeModel?(context: AgentMiddlewareContext & {
|
|
51
85
|
request: CompletionRequest;
|
|
52
|
-
state: AgentState;
|
|
53
|
-
store?: Store;
|
|
54
86
|
}): CompletionRequest | void | Promise<CompletionRequest | void>;
|
|
55
87
|
/** Inspects or replaces the response: redact, validate, count. */
|
|
56
|
-
afterModel?(context: {
|
|
88
|
+
afterModel?(context: AgentMiddlewareContext & {
|
|
57
89
|
response: NexusResponse;
|
|
58
|
-
state: AgentState;
|
|
59
|
-
store?: Store;
|
|
60
90
|
}): NexusResponse | void | Promise<NexusResponse | void>;
|
|
61
|
-
/**
|
|
62
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Wraps the model call itself, after every `beforeModel` and before every `afterModel`. `next`
|
|
93
|
+
* sends a request and may be called again, with the same request or a changed one, to retry or to
|
|
94
|
+
* fall back to another model. The first middleware is the outermost.
|
|
95
|
+
*/
|
|
96
|
+
wrapModelCall?(request: CompletionRequest, next: (request: CompletionRequest) => Promise<NexusResponse>, context: AgentModelCallContext): Promise<NexusResponse> | NexusResponse;
|
|
97
|
+
/**
|
|
98
|
+
* Wraps a tool call, so a policy can log it, time it, retry it, or refuse it. `next` runs the
|
|
99
|
+
* rest of the chain and may be called again; given a call, it runs that call's arguments instead,
|
|
100
|
+
* and the permission policy decides them again first. The first middleware is the outermost.
|
|
101
|
+
*/
|
|
102
|
+
wrapToolCall?(call: AgentToolCall, next: (call?: AgentToolCall) => Promise<AgentToolResult>, context: AgentMiddlewareContext): Promise<AgentToolResult> | AgentToolResult;
|
|
63
103
|
}
|
|
64
104
|
/** What a tool call produced. */
|
|
65
105
|
export interface AgentToolResult {
|
|
@@ -88,6 +128,13 @@ export interface CreateAgentOptions {
|
|
|
88
128
|
interruptOn?: Record<string, AgentApprovalPolicy | true>;
|
|
89
129
|
/** Hooks around model and tool calls, applied in order. */
|
|
90
130
|
middleware?: AgentMiddleware[];
|
|
131
|
+
/**
|
|
132
|
+
* Decides every tool call from the capabilities it declares, before it runs: allow it, deny it,
|
|
133
|
+
* or interrupt for a person's approval, as `interruptOn` does. A denied call reaches the model as a
|
|
134
|
+
* failed tool result naming the reason, and never runs. See `permissionPolicy()` in
|
|
135
|
+
* `nexus-ai-pro/agent/permissions`.
|
|
136
|
+
*/
|
|
137
|
+
permissions?: PermissionPolicyLike;
|
|
91
138
|
/** Long-term memory, available to tools and middleware as `store`. */
|
|
92
139
|
store?: Store;
|
|
93
140
|
/**
|
|
@@ -136,8 +183,11 @@ export type AgentState = {
|
|
|
136
183
|
/** Why the agent stopped. */
|
|
137
184
|
stopReason?: AgentStopReason;
|
|
138
185
|
};
|
|
139
|
-
/**
|
|
140
|
-
|
|
186
|
+
/**
|
|
187
|
+
* `completed` when the model answered, `max_iterations` when it ran out of model calls, `stopped`
|
|
188
|
+
* when a middleware ended the run through `context.stop()`.
|
|
189
|
+
*/
|
|
190
|
+
export type AgentStopReason = 'completed' | 'max_iterations' | 'stopped';
|
|
141
191
|
/** An agent: a compiled graph over the agent's channels. */
|
|
142
192
|
export type AgentGraph = CompiledGraph<AgentChannels>;
|
|
143
193
|
/** Wraps a question into the state an agent starts from. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendList, counter, lastValue } from '../graph/channels.js';
|
|
2
2
|
import { createGraph } from '../graph/graph.js';
|
|
3
3
|
import { Command, END, Send } from '../types/graph.js';
|
|
4
|
+
import { capabilitiesOf } from './capabilities.js';
|
|
4
5
|
import { ToolExecutor, toolMessageContent } from './tool.js';
|
|
5
6
|
const agentChannels = () => ({
|
|
6
7
|
messages: appendList(),
|
|
@@ -34,24 +35,47 @@ export function createAgent(options) {
|
|
|
34
35
|
...(options.responseFormat ? { responseFormat: options.responseFormat } : {}),
|
|
35
36
|
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
36
37
|
};
|
|
38
|
+
const hooks = hookContext(context, state, options.store);
|
|
37
39
|
for (const item of middleware) {
|
|
38
|
-
request = (await item.beforeModel?.({
|
|
40
|
+
request = (await item.beforeModel?.({ ...hooks, request })) ?? request;
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const streaming = Boolean(options.streamTokens && options.client.stream);
|
|
43
|
+
const send = (next) => streaming && options.client.stream
|
|
44
|
+
? streamed(options.client.stream(next), (chunk) => context.message(chunk))
|
|
45
|
+
: options.client.complete(next);
|
|
46
|
+
const stops = new WeakSet();
|
|
47
|
+
const modelContext = {
|
|
48
|
+
...hooks,
|
|
49
|
+
stop: (answer) => {
|
|
50
|
+
const stopped = stopResponse(answer, request.model);
|
|
51
|
+
stops.add(stopped);
|
|
52
|
+
return stopped;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const wrapped = middleware.reduceRight((next, item) => {
|
|
56
|
+
const wrap = item.wrapModelCall?.bind(item);
|
|
57
|
+
return wrap ? (current) => Promise.resolve(wrap(current, next, modelContext)) : next;
|
|
58
|
+
}, send);
|
|
59
|
+
let response = await wrapped(request);
|
|
60
|
+
const stopped = stops.has(response);
|
|
61
|
+
if ((!streaming || stopped) && response.content) {
|
|
44
62
|
context.message({ content: response.content });
|
|
45
63
|
}
|
|
46
64
|
for (const item of middleware) {
|
|
47
|
-
response = (await item.afterModel?.({
|
|
65
|
+
response = (await item.afterModel?.({ ...hooks, response })) ?? response;
|
|
48
66
|
}
|
|
49
67
|
const assistant = {
|
|
50
68
|
role: 'assistant',
|
|
51
69
|
content: response.content,
|
|
52
|
-
...(response.toolCalls?.length ? { toolCalls: response.toolCalls } : {}),
|
|
70
|
+
...(response.toolCalls?.length && !stopped ? { toolCalls: response.toolCalls } : {}),
|
|
53
71
|
};
|
|
54
|
-
const calls = response.toolCalls ?? [];
|
|
72
|
+
const calls = stopped ? [] : (response.toolCalls ?? []);
|
|
73
|
+
if (stopped) {
|
|
74
|
+
return new Command({
|
|
75
|
+
update: { messages: [assistant], iterations: 1, answer: response.content, stopReason: 'stopped' },
|
|
76
|
+
goto: END,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
55
79
|
if (calls.length === 0) {
|
|
56
80
|
return new Command({
|
|
57
81
|
update: { messages: [assistant], iterations: 1, answer: response.content, stopReason: 'completed' },
|
|
@@ -76,27 +100,82 @@ export function createAgent(options) {
|
|
|
76
100
|
return { messages: [toolMessage(call, { ok: false, error: parsed.error })] };
|
|
77
101
|
}
|
|
78
102
|
let args = parsed.args;
|
|
79
|
-
const
|
|
103
|
+
const definition = executor.get(call.function.name);
|
|
104
|
+
const capabilities = capabilitiesOf(definition, args);
|
|
105
|
+
const description = {
|
|
106
|
+
id: call.id,
|
|
107
|
+
name: call.function.name,
|
|
108
|
+
args,
|
|
109
|
+
...(capabilities ? { capabilities } : {}),
|
|
110
|
+
};
|
|
111
|
+
const refuse = (error) => {
|
|
112
|
+
context.tool({ phase: 'error', id: call.id, name: description.name, error });
|
|
113
|
+
return { messages: [toolMessage(call, { ok: false, error })] };
|
|
114
|
+
};
|
|
115
|
+
let permission;
|
|
116
|
+
if (options.permissions) {
|
|
117
|
+
const verdict = await options.permissions.decide({
|
|
118
|
+
tool: description.name,
|
|
119
|
+
args,
|
|
120
|
+
capabilities,
|
|
121
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
122
|
+
});
|
|
123
|
+
if (verdict.decision === 'deny')
|
|
124
|
+
return refuse(`Permission denied: ${verdict.reason}`);
|
|
125
|
+
if (verdict.decision === 'ask')
|
|
126
|
+
permission = verdict.reason;
|
|
127
|
+
}
|
|
80
128
|
const policy = approvals[description.name];
|
|
81
|
-
if (policy) {
|
|
129
|
+
if (policy || permission) {
|
|
130
|
+
const asked = permission ? { ...description, permission } : description;
|
|
82
131
|
const decision = context.interrupt({
|
|
83
|
-
reason: (policy
|
|
84
|
-
|
|
132
|
+
reason: (policy && policy !== true ? policy.reason?.(asked) : undefined) ??
|
|
133
|
+
(permission ? `${defaultReason(asked)} (${permission})` : defaultReason(asked)),
|
|
134
|
+
payload: asked,
|
|
85
135
|
});
|
|
86
136
|
const verdict = normalizeApproval(decision);
|
|
87
|
-
if (!verdict.approved)
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
if (verdict.args)
|
|
137
|
+
if (!verdict.approved)
|
|
138
|
+
return refuse(verdict.reason ?? 'A human refused this tool call');
|
|
139
|
+
if (verdict.args) {
|
|
93
140
|
args = verdict.args;
|
|
141
|
+
if (options.permissions) {
|
|
142
|
+
const recheck = await options.permissions.decide({
|
|
143
|
+
tool: description.name,
|
|
144
|
+
args,
|
|
145
|
+
capabilities: capabilitiesOf(definition, args),
|
|
146
|
+
});
|
|
147
|
+
if (recheck.decision === 'deny')
|
|
148
|
+
return refuse(`Permission denied: ${recheck.reason}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
94
151
|
}
|
|
95
|
-
const run = async () =>
|
|
152
|
+
const run = async (current) => {
|
|
153
|
+
if (options.permissions && current.args !== args) {
|
|
154
|
+
const recheck = await options.permissions.decide({
|
|
155
|
+
tool: description.name,
|
|
156
|
+
args: current.args,
|
|
157
|
+
capabilities: capabilitiesOf(definition, current.args),
|
|
158
|
+
});
|
|
159
|
+
if (recheck.decision === 'deny')
|
|
160
|
+
return { ok: false, error: `Permission denied: ${recheck.reason}` };
|
|
161
|
+
}
|
|
162
|
+
return executor.execute(description.name, current.args, {
|
|
163
|
+
toolCallId: call.id,
|
|
164
|
+
threadId: context.threadId,
|
|
165
|
+
...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
|
|
166
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
167
|
+
signal: context.signal,
|
|
168
|
+
...(context.store ? { store: context.store } : {}),
|
|
169
|
+
});
|
|
170
|
+
};
|
|
96
171
|
context.tool({ phase: 'start', id: call.id, name: description.name, args });
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
172
|
+
const hooks = hookContext(context, context.state, options.store);
|
|
173
|
+
const result = await middleware.reduceRight((next, item) => {
|
|
174
|
+
const wrap = item.wrapToolCall?.bind(item);
|
|
175
|
+
return wrap
|
|
176
|
+
? (current) => Promise.resolve(wrap(current, (changed) => next(changed ?? current), hooks))
|
|
177
|
+
: next;
|
|
178
|
+
}, run)({ ...description, args });
|
|
100
179
|
context.tool(result.ok
|
|
101
180
|
? { phase: 'result', id: call.id, name: description.name, result: result.result }
|
|
102
181
|
: { phase: 'error', id: call.id, name: description.name, error: result.error ?? 'The tool failed' });
|
|
@@ -111,6 +190,12 @@ export function createAgent(options) {
|
|
|
111
190
|
...(options.store ? { store: options.store } : {}),
|
|
112
191
|
...(options.name ? { name: options.name } : {}),
|
|
113
192
|
...(options.lifecycle ? { lifecycle: options.lifecycle, lifecycleFamily: 'agent' } : {}),
|
|
193
|
+
tools: executor.list().map((definition) => ({
|
|
194
|
+
name: definition.name,
|
|
195
|
+
...(Array.isArray(definition.capabilities) ? { capabilities: [...definition.capabilities] } : {}),
|
|
196
|
+
...(typeof definition.capabilities === 'function' ? { dynamic: true } : {}),
|
|
197
|
+
approval: approvals[definition.name] ? 'interrupt' : options.permissions ? 'policy' : 'none',
|
|
198
|
+
})),
|
|
114
199
|
});
|
|
115
200
|
}
|
|
116
201
|
export function agentAsTool(options) {
|
|
@@ -165,6 +250,35 @@ function parseArguments(raw) {
|
|
|
165
250
|
return { ok: false, error: `Tool arguments are not valid JSON (${reason}); the tool was not run` };
|
|
166
251
|
}
|
|
167
252
|
}
|
|
253
|
+
function hookContext(context, state, store) {
|
|
254
|
+
return {
|
|
255
|
+
state,
|
|
256
|
+
...((context.store ?? store) ? { store: context.store ?? store } : {}),
|
|
257
|
+
threadId: context.threadId,
|
|
258
|
+
...(context.tenantId ? { tenantId: context.tenantId } : {}),
|
|
259
|
+
...(context.principal ? { principal: context.principal } : {}),
|
|
260
|
+
signal: context.signal,
|
|
261
|
+
interrupt: (request) => context.interrupt(request),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function stopResponse(answer, model) {
|
|
265
|
+
return {
|
|
266
|
+
content: answer,
|
|
267
|
+
role: 'assistant',
|
|
268
|
+
finishReason: 'stop',
|
|
269
|
+
meta: {
|
|
270
|
+
requestId: '',
|
|
271
|
+
providerUsed: 'none',
|
|
272
|
+
modelUsed: model,
|
|
273
|
+
latencyMs: 0,
|
|
274
|
+
tokensInput: 0,
|
|
275
|
+
tokensOutput: 0,
|
|
276
|
+
tokensSaved: 0,
|
|
277
|
+
cacheHit: false,
|
|
278
|
+
guardrailsApplied: ['stopped-by-middleware'],
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
168
282
|
async function streamed(chunks, onChunk) {
|
|
169
283
|
let content = '';
|
|
170
284
|
const toolCalls = [];
|