From 233daae3e16ec82fe2372fb059dc44b2c9a57cbc Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Sat, 23 May 2026 00:10:55 +0530 Subject: [PATCH 01/10] added ts sdk --- CONTRIBUTING.md | 2 +- Makefile | 10 +- backend/Makefile | 4 +- backend/proto/README.md | 4 +- sdk/python/adrian/__init__.py | 17 +- sdk/python/adrian/mcp.py | 5 +- sdk/python/adrian/proto/buf.gen.yaml | 6 + sdk/python/adrian/proto/buf.lock | 6 + sdk/python/adrian/proto/buf.yaml | 5 + .../python/adrian/proto}/event.proto | 0 sdk/typescript/.gitignore | 5 + sdk/typescript/README.md | 29 + sdk/typescript/package-lock.json | 2704 +++++++++++++++++ sdk/typescript/package.json | 59 +- sdk/typescript/src/config.ts | 73 + sdk/typescript/src/context.ts | 84 + sdk/typescript/src/format/types.ts | 47 + sdk/typescript/src/handler.ts | 217 ++ sdk/typescript/src/handlers/jsonl.ts | 36 + sdk/typescript/src/hooks.ts | 34 + sdk/typescript/src/identity.ts | 22 + sdk/typescript/src/index.ts | 114 + .../src/instrumentation/langchain.ts | 136 + sdk/typescript/src/mcp.ts | 106 + sdk/typescript/src/pairing.ts | 107 + sdk/typescript/src/pii/engine.ts | 24 + sdk/typescript/src/pii/index.ts | 6 + sdk/typescript/src/pii/patterns.ts | 65 + sdk/typescript/src/pii/redactor.ts | 82 + sdk/typescript/src/pii/strategies.ts | 38 + sdk/typescript/src/proto/schema.ts | 161 + sdk/typescript/src/sessionPersistence.ts | 54 + sdk/typescript/src/types.ts | 86 + sdk/typescript/src/ws.ts | 298 ++ sdk/typescript/tests/jsonl.test.ts | 28 + sdk/typescript/tests/langchain.test.ts | 79 + sdk/typescript/tests/pairing.test.ts | 18 + sdk/typescript/tests/pii.test.ts | 12 + sdk/typescript/tests/proto.test.ts | 24 + sdk/typescript/tests/session.test.ts | 6 + sdk/typescript/tests/ws.test.ts | 64 + sdk/typescript/tsconfig.build.json | 12 + sdk/typescript/tsconfig.json | 18 + 43 files changed, 4884 insertions(+), 23 deletions(-) create mode 100644 sdk/python/adrian/proto/buf.gen.yaml create mode 100644 sdk/python/adrian/proto/buf.lock create mode 100644 sdk/python/adrian/proto/buf.yaml rename {proto => sdk/python/adrian/proto}/event.proto (100%) create mode 100644 sdk/typescript/.gitignore create mode 100644 sdk/typescript/README.md create mode 100644 sdk/typescript/package-lock.json create mode 100644 sdk/typescript/src/config.ts create mode 100644 sdk/typescript/src/context.ts create mode 100644 sdk/typescript/src/format/types.ts create mode 100644 sdk/typescript/src/handler.ts create mode 100644 sdk/typescript/src/handlers/jsonl.ts create mode 100644 sdk/typescript/src/hooks.ts create mode 100644 sdk/typescript/src/identity.ts create mode 100644 sdk/typescript/src/index.ts create mode 100644 sdk/typescript/src/instrumentation/langchain.ts create mode 100644 sdk/typescript/src/mcp.ts create mode 100644 sdk/typescript/src/pairing.ts create mode 100644 sdk/typescript/src/pii/engine.ts create mode 100644 sdk/typescript/src/pii/index.ts create mode 100644 sdk/typescript/src/pii/patterns.ts create mode 100644 sdk/typescript/src/pii/redactor.ts create mode 100644 sdk/typescript/src/pii/strategies.ts create mode 100644 sdk/typescript/src/proto/schema.ts create mode 100644 sdk/typescript/src/sessionPersistence.ts create mode 100644 sdk/typescript/src/types.ts create mode 100644 sdk/typescript/src/ws.ts create mode 100644 sdk/typescript/tests/jsonl.test.ts create mode 100644 sdk/typescript/tests/langchain.test.ts create mode 100644 sdk/typescript/tests/pairing.test.ts create mode 100644 sdk/typescript/tests/pii.test.ts create mode 100644 sdk/typescript/tests/proto.test.ts create mode 100644 sdk/typescript/tests/session.test.ts create mode 100644 sdk/typescript/tests/ws.test.ts create mode 100644 sdk/typescript/tsconfig.build.json create mode 100644 sdk/typescript/tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5d2dfb..06b377e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ Thanks for considering a contribution. Adrian is open-source under the [Apache 2 ## Local dev setup -The Python SDK lives at `sdk/python/` (the TypeScript SDK lives at `sdk/typescript/`). From the repo root: +The Python SDK lives at `sdk/python/`. From the repo root: ```sh make sdk-install # creates .venv and installs sdk + dev deps via uv diff --git a/Makefile b/Makefile index af6d231..bf545e9 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ # Adrian OSS — top-level developer Makefile # # Local-dev convention: every Python entry point goes through the -# project-local ``.venv`` so the bundled ``sdk/python/`` install (``pip -# install -e ./sdk/python``) cannot collide with a system-wide -# ``adrian-sdk`` wheel from PyPI. ``make sdk-install`` creates the venv -# and the editable install in one shot. +# project-local ``.venv`` so the bundled Python SDK install (``pip +# install -e ./sdk/python``) cannot collide with a system-wide ``adrian-sdk`` +# wheel from PyPI. ``make sdk-install`` creates the venv and the +# editable install in one shot. .PHONY: sdk-install sdk-test sdk-clean help @@ -24,7 +24,7 @@ endif @echo "Or run anything via uv: 'uv run python ...'" sdk-test: ## Run the bundled SDK test suite - uv run --project ./sdk/python pytest sdk/python/tests + uv run --project ./sdk/python --extra dev pytest sdk/python/tests sdk-clean: ## Remove .venv and SDK build artefacts rm -rf .venv sdk/python/.venv sdk/python/dist sdk/python/build sdk/python/*.egg-info diff --git a/backend/Makefile b/backend/Makefile index 8a00e50..c4f3841 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -28,12 +28,12 @@ clean: # the Go bindings. Strips the buf.validate import and field annotations # (see proto/README.md). Requires protoc + protoc-gen-go on PATH. proto-sync: - @echo "Syncing proto/event.proto from ../proto/..." + @echo "Syncing proto/event.proto from sdk/python/adrian/proto/..." @sed \ -e '/^import "buf\/validate\/validate.proto";$$/d' \ -e 's| \[(buf\.validate\.field)[^]]*\]||g' \ -e '/^package adrian\.core_api\.v1;$$/a\option go_package = "github.com/secureagentics/Adrian/backend/internal/proto;proto";' \ - ../proto/event.proto > proto/event.proto + ../sdk/python/adrian/proto/event.proto > proto/event.proto @$(MAKE) proto-regen # Regenerate Go bindings from proto/event.proto. diff --git a/backend/proto/README.md b/backend/proto/README.md index db15bf0..593737a 100644 --- a/backend/proto/README.md +++ b/backend/proto/README.md @@ -4,7 +4,7 @@ The wire schema for SDK <-> backend WebSocket frames. ## Layout -- `event.proto` is the Go-side copy of the wire schema. The single source of truth is `proto/event.proto` at the repo root. +- `event.proto` is the Go-side copy of the wire schema. The single source of truth is `sdk/python/adrian/proto/event.proto` at the repo root. - The Go-side copy is **stripped** of the `buf.validate` import and field annotations: - The `import "buf/validate/validate.proto";` line is removed. - Every `[(buf.validate.field).*]` annotation is removed. @@ -15,7 +15,7 @@ The wire schema for SDK <-> backend WebSocket frames. When the wire schema changes: -1. Edit `proto/event.proto` (the source of truth). +1. Edit `sdk/python/adrian/proto/event.proto` (the source of truth). 2. Regenerate the SDK's Python bindings: `cd sdk/python && uv run bash tools/regen-proto.sh`. 3. Sync the backend copy and regenerate the Go bindings: `cd backend && make proto-sync`. 4. Commit both. diff --git a/sdk/python/adrian/__init__.py b/sdk/python/adrian/__init__.py index 03b7fd4..212b4ff 100644 --- a/sdk/python/adrian/__init__.py +++ b/sdk/python/adrian/__init__.py @@ -23,10 +23,11 @@ import asyncio import atexit +import importlib import logging import os from pathlib import Path -from typing import Any +from typing import Any, cast from uuid import uuid4 from langchain_core.callbacks.manager import CallbackManager @@ -679,10 +680,15 @@ def _patch_langgraph() -> None: top-level call so all sub-agent events share the same ID. """ try: - from langgraph.pregel import Pregel + pregel_mod = importlib.import_module("langgraph.pregel") except ImportError: return + Pregel = cast("Any", getattr(pregel_mod, "Pregel", None)) + + if Pregel is None: + return + if getattr(Pregel, "_adrian_pregel_patched", False): return @@ -864,10 +870,15 @@ def _patch_tool_node() -> None: tools. On timeout it fails open. """ try: - from langgraph.prebuilt import ToolNode + prebuilt_mod = importlib.import_module("langgraph.prebuilt") except ImportError: return + ToolNode = cast("Any", getattr(prebuilt_mod, "ToolNode", None)) + + if ToolNode is None: + return + if getattr(ToolNode, "_adrian_tool_node_patched", False): return diff --git a/sdk/python/adrian/mcp.py b/sdk/python/adrian/mcp.py index b41eae9..7131a82 100644 --- a/sdk/python/adrian/mcp.py +++ b/sdk/python/adrian/mcp.py @@ -26,6 +26,7 @@ from __future__ import annotations import contextlib +import importlib import logging import sys from collections.abc import Mapping @@ -179,11 +180,11 @@ def _patch_mcp_adapter() -> None: # pyright: ignore[reportUnusedFunction] def _patch_langchain_mcp_adapters() -> None: """Patch ``MultiServerMCPClient.__init__`` to snapshot declarations.""" try: - from langchain_mcp_adapters import client as client_mod + client_mod = importlib.import_module("langchain_mcp_adapters.client") except ImportError: return - cls = getattr(client_mod, "MultiServerMCPClient", None) + cls = cast("Any", getattr(client_mod, "MultiServerMCPClient", None)) if cls is None or getattr(cls, "_adrian_mcp_patched", False): return diff --git a/sdk/python/adrian/proto/buf.gen.yaml b/sdk/python/adrian/proto/buf.gen.yaml new file mode 100644 index 0000000..5845eec --- /dev/null +++ b/sdk/python/adrian/proto/buf.gen.yaml @@ -0,0 +1,6 @@ +version: v2 +plugins: + - remote: buf.build/protocolbuffers/python + out: . + - remote: buf.build/protocolbuffers/pyi + out: . diff --git a/sdk/python/adrian/proto/buf.lock b/sdk/python/adrian/proto/buf.lock new file mode 100644 index 0000000..709ae02 --- /dev/null +++ b/sdk/python/adrian/proto/buf.lock @@ -0,0 +1,6 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 50325440f8f24053b047484a6bf60b76 + digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31 diff --git a/sdk/python/adrian/proto/buf.yaml b/sdk/python/adrian/proto/buf.yaml new file mode 100644 index 0000000..25426d9 --- /dev/null +++ b/sdk/python/adrian/proto/buf.yaml @@ -0,0 +1,5 @@ +version: v2 +modules: + - path: . +deps: + - buf.build/bufbuild/protovalidate diff --git a/proto/event.proto b/sdk/python/adrian/proto/event.proto similarity index 100% rename from proto/event.proto rename to sdk/python/adrian/proto/event.proto diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore new file mode 100644 index 0000000..7c96d6d --- /dev/null +++ b/sdk/typescript/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +coverage/ +.vite/ +.env diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md new file mode 100644 index 0000000..55193b8 --- /dev/null +++ b/sdk/typescript/README.md @@ -0,0 +1,29 @@ +# @secureagentics/adrian + +TypeScript SDK for Adrian multi-agent event capture in Node.js LangChain.js / LangGraph.js applications. + +```ts +import { init } from "@secureagentics/adrian"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); +``` + +The SDK mirrors the Python package: it pairs LLM/tool callbacks into `PairedEvent` objects, redacts PII, writes JSONL locally, streams protobuf frames to the Adrian WebSocket endpoint, tracks MCP servers, and applies BLOCK/HITL tool gating when LangGraph ToolNode instrumentation is available. + +## Environment + +- `ADRIAN_API_KEY` +- `ADRIAN_LOG_FILE` +- `ADRIAN_WS_URL` +- `ADRIAN_SESSION_ID` +- `ADRIAN_BLOCK_TIMEOUT` +- `ADRIAN_REPLAY_BUFFER_FRAMES` + +## Manual Callback Wiring + +```ts +import { init, getHandler } from "@secureagentics/adrian"; + +await init({ autoInstrument: false }); +const handler = getHandler(); +``` diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 0000000..c321d7c --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,2704 @@ +{ + "name": "@secureagentics/adrian", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@secureagentics/adrian", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.0", + "@langchain/langgraph": ">=0.2.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "@langchain/langgraph": { + "optional": true + } + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/protobufjs": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.2.tgz", + "integrity": "sha512-64rfNzkWOZAIazXzpBFPWq6F9up6gMvTzjE2oWIzApx2N/dqVUEE7+bCn2+40780dFVtKOUab8QfxJ6KJDWbqA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index c9a6ace..61cc108 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,17 +1,60 @@ { - "name": "adrian-typescript", - "private": true, - "description": "Adrian TypeScript SDK monorepo (npm workspaces).", - "workspaces": [ - "packages/*" + "name": "@secureagentics/adrian", + "version": "1.0.0", + "description": "Multi-agent security monitoring SDK for LangChain.js / LangGraph.js.", + "license": "Apache-2.0", + "author": "Secure Agentics ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md" ], "scripts": { - "build": "npm run build --workspaces --if-present", - "typecheck": "npm run typecheck --workspaces --if-present", - "test": "npm run test --workspaces --if-present" + "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "langchain", + "langgraph", + "ai", + "agents", + "security", + "monitoring", + "observability", + "llm", + "multi-agent", + "prompt-injection" + ], + "peerDependencies": { + "@langchain/core": ">=0.3.0", + "@langchain/langgraph": ">=0.2.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "@langchain/langgraph": { + "optional": true + } + }, + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" }, "devDependencies": { "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.7" diff --git a/sdk/typescript/src/config.ts b/sdk/typescript/src/config.ts new file mode 100644 index 0000000..0e6a2e1 --- /dev/null +++ b/sdk/typescript/src/config.ts @@ -0,0 +1,73 @@ +import type { EventData, McpServer, VerdictContext } from "./types.js"; + +export type MaybePromise = T | Promise; +export type OnVerdictCallback = (ctx: VerdictContext) => MaybePromise; +export type OnBlockCallback = (ctx: VerdictContext) => MaybePromise; +export type OnAuditCallback = (ctx: VerdictContext) => MaybePromise; +export type OnEventCallback = ( + eventType: string, + data: EventData, + runId: string, + parentRunId: string | null, + eventId: string, +) => MaybePromise; +export type OnDisconnectCallback = (reason: string) => MaybePromise; +export type OnReconnectCallback = () => MaybePromise; +export type OnMcpServerCallback = (server: McpServer) => MaybePromise; + +export interface AdrianConfig { + apiKey: string | null; + logFile: string; + logLevel: string | null; + sessionId: string; + wsUrl: string | null; + blockTimeout: number; + onEvent: OnEventCallback | null; + onVerdict: OnVerdictCallback | null; + onBlock: OnBlockCallback | null; + onAudit: OnAuditCallback | null; + onDisconnect: OnDisconnectCallback | null; + onReconnect: OnReconnectCallback | null; + onMcpServer: OnMcpServerCallback | null; + replayBufferFrames: number; +} + +export interface InitOptions { + apiKey?: string | null; + logFile?: string; + handlers?: import("./types.js").EventHandler[] | null; + autoInstrument?: boolean; + logLevel?: string | null; + wsUrl?: string | null; + sessionId?: string | null; + blockTimeout?: number; + onEvent?: OnEventCallback | null; + onVerdict?: OnVerdictCallback | null; + onBlock?: OnBlockCallback | null; + onAudit?: OnAuditCallback | null; + onDisconnect?: OnDisconnectCallback | null; + onReconnect?: OnReconnectCallback | null; + onMcpServer?: OnMcpServerCallback | null; + replayBufferFrames?: number; +} + +let config: AdrianConfig | null = null; + +export function getConfig(): AdrianConfig { + if (config === null) { + throw new Error("Adrian SDK has not been initialised. Call init() first."); + } + return config; +} + +export function currentConfig(): AdrianConfig | null { + return config; +} + +export function setConfig(next: AdrianConfig | null): void { + config = next; +} + +export function isInitialized(): boolean { + return config !== null; +} diff --git a/sdk/typescript/src/context.ts b/sdk/typescript/src/context.ts new file mode 100644 index 0000000..0ad59db --- /dev/null +++ b/sdk/typescript/src/context.ts @@ -0,0 +1,84 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import type { AgentContext, ParentContext } from "./format/types.js"; + +const invocationStorage = new AsyncLocalStorage(); + +export function getInvocationId(): string | null { + return invocationStorage.getStore() ?? null; +} + +export function runWithInvocationId(invocationId: string, fn: () => T): T { + return invocationStorage.run(invocationId, fn); +} + +export class AgentContextTracker { + private contexts = new Map(); + private parentMap = new Map(); + private delegatedBy: string | null = null; + + markDelegated(agentId: string): void { + this.delegatedBy = agentId; + } + + update(agentId: string, systemPrompt: string, userInstruction: string): ParentContext | null { + this.contexts.set(agentId, { agentId, systemPrompt, userInstruction }); + + if (!this.parentMap.has(agentId)) { + let parent: ParentContext | null = null; + if (this.delegatedBy !== null) { + const previous = this.contexts.get(this.delegatedBy); + if (previous && previous.agentId !== agentId) { + parent = { ...previous }; + } + } + + if (parent === null) { + const newParts = normalize(agentId.split("|")); + let bestCandidate: string | null = null; + let bestCommon = 0; + for (const otherId of this.contexts.keys()) { + if (otherId === agentId) continue; + const otherParts = normalize(otherId.split("|")); + if (otherParts.length >= newParts.length) continue; + let common = 0; + for (let idx = 0; idx < Math.min(otherParts.length, newParts.length); idx += 1) { + if (otherParts[idx] !== newParts[idx]) break; + common += 1; + } + if (common > bestCommon) { + bestCommon = common; + bestCandidate = otherId; + } + } + if (bestCandidate !== null && bestCommon > 0) { + const previous = this.contexts.get(bestCandidate); + if (previous) parent = { ...previous }; + } + } + + this.parentMap.set(agentId, parent); + } + + if (agentId === this.delegatedBy) { + this.delegatedBy = null; + } + + return this.parentMap.get(agentId) ?? null; + } + + getParent(agentId: string): ParentContext | null { + return this.parentMap.get(agentId) ?? null; + } + + hasContext(agentId: string): boolean { + return this.contexts.has(agentId); + } + + getContext(agentId: string): AgentContext | null { + return this.contexts.get(agentId) ?? null; + } +} + +function normalize(parts: string[]): string[] { + return parts.filter((part) => !/^\d+$/.test(part)); +} diff --git a/sdk/typescript/src/format/types.ts b/sdk/typescript/src/format/types.ts new file mode 100644 index 0000000..5018f99 --- /dev/null +++ b/sdk/typescript/src/format/types.ts @@ -0,0 +1,47 @@ +import type { CallbackMetadata, ChatMessage, TokenUsage, ToolCallRecord } from "../types.js"; + +export interface AgentContext { + agentId: string; + systemPrompt: string; + userInstruction: string; +} + +export interface ParentContext { + agentId: string; + systemPrompt: string; + userInstruction: string; +} + +export interface LlmPairData { + kind: "llm"; + model: string; + messages: ChatMessage[]; + output: string; + toolCalls: ToolCallRecord[]; + usage: TokenUsage | null; +} + +export interface ToolPairData { + kind: "tool"; + toolName: string; + toolCallId: string | null; + input: string; + output: string; +} + +export type PairType = "llm" | "tool"; +export type PairData = LlmPairData | ToolPairData; + +export interface PairedEvent { + eventId: string; + invocationId: string; + sessionId: string; + runId: string; + parentRunId: string; + timestamp: string; + pairType: PairType; + agent: AgentContext; + parent: ParentContext | null; + data: PairData; + metadata: CallbackMetadata | null; +} diff --git a/sdk/typescript/src/handler.ts b/sdk/typescript/src/handler.ts new file mode 100644 index 0000000..9681787 --- /dev/null +++ b/sdk/typescript/src/handler.ts @@ -0,0 +1,217 @@ +import { currentConfig, type AdrianConfig } from "./config.js"; +import { getInvocationId } from "./context.js"; +import { AgentContextTracker } from "./context.js"; +import type { PairedEvent } from "./format/types.js"; +import { HookRegistry } from "./hooks.js"; +import { deriveAgentId } from "./identity.js"; +import { EventPairBuffer } from "./pairing.js"; +import type { CallbackMetadata, ChatMessage, EventData, EventRecord, LlmEndData, ToolCallRecord, ToolEndData, VerdictContext } from "./types.js"; +import type { Verdict } from "./proto/schema.js"; + +export interface AdrianCallbackHandlerOptions { + pairBuffer: EventPairBuffer; + contextTracker: AgentContextTracker; + hooks: HookRegistry; + config: AdrianConfig; +} + +export class AdrianCallbackHandler { + name = "AdrianCallbackHandler"; + private pairBuffer: EventPairBuffer; + private contextTracker: AgentContextTracker; + private hooks: HookRegistry; + private config: AdrianConfig; + private eventMap = new Map(); + private currentAgentId = "default"; + + constructor(options: AdrianCallbackHandlerOptions) { + this.pairBuffer = options.pairBuffer; + this.contextTracker = options.contextTracker; + this.hooks = options.hooks; + this.config = options.config; + } + + async handleChatModelStart(llm: Record, messages: unknown[][], runId: string, parentRunId?: string, extraParams?: Record): Promise { + const flatMessages = messages.flat().map(messageToChatMessage); + const metadata = extractMetadata(extraParams); + const agentId = deriveAgentId(metadata, flatMessages); + this.currentAgentId = agentId; + const systemPrompt = flatMessages.find((msg) => msg.role === "system")?.content ?? ""; + const userInstruction = [...flatMessages].reverse().find((msg) => msg.role === "human" || msg.role === "user")?.content ?? ""; + const parent = this.contextTracker.update(agentId, systemPrompt, userInstruction); + this.pairBuffer.onStart({ + eventType: "chat_model_start", + data: { model: extractModelName(llm), messages: flatMessages, metadata }, + runId: String(runId), + agentId, + parent: parent ?? this.contextTracker.getParent(agentId), + metadata, + parentRunId: parentRunId ? String(parentRunId) : "", + }); + } + + async handleLLMStart(llm: Record, prompts: string[], runId: string, parentRunId?: string, extraParams?: Record): Promise { + const flatMessages = prompts.map((content) => ({ role: "human", content })); + const metadata = extractMetadata(extraParams); + const agentId = deriveAgentId(metadata, flatMessages); + this.currentAgentId = agentId; + const parent = this.contextTracker.update(agentId, "", prompts[0] ?? ""); + this.pairBuffer.onStart({ + eventType: "chat_model_start", + data: { model: extractModelName(llm), messages: flatMessages, metadata }, + runId: String(runId), + agentId, + parent: parent ?? this.contextTracker.getParent(agentId), + metadata, + parentRunId: parentRunId ? String(parentRunId) : "", + }); + } + + async handleLLMEnd(output: unknown, runId: string): Promise { + const data = extractLlmEndData(output); + const pair = this.pairBuffer.onEnd({ + eventType: "llm_end", + data, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) { + if (data.toolCalls.length > 0) this.contextTracker.markDelegated(pair.agent.agentId); + await this.emitPair(pair); + } + } + + async handleToolStart(tool: Record, input: string, runId: string, parentRunId?: string, extraParams?: Record): Promise { + const metadata = extractMetadata(extraParams); + let agentId = this.currentAgentId; + if (metadata) { + const candidate = deriveAgentId(metadata); + if (this.contextTracker.hasContext(candidate)) agentId = candidate; + } + this.pairBuffer.onStart({ + eventType: "tool_start", + data: { + toolName: String(tool.name ?? tool.id ?? "unknown"), + toolCallId: typeof extraParams?.tool_call_id === "string" ? extraParams.tool_call_id : typeof extraParams?.toolCallId === "string" ? extraParams.toolCallId : null, + input: String(input ?? ""), + metadata, + }, + runId: String(runId), + agentId, + parent: this.contextTracker.getParent(agentId), + metadata, + agentContext: this.contextTracker.getContext(agentId), + parentRunId: parentRunId ? String(parentRunId) : "", + }); + } + + async handleToolEnd(output: unknown, runId: string): Promise { + const data: ToolEndData = { output: stringifyOutput(output) }; + const pair = this.pairBuffer.onEnd({ + eventType: "tool_end", + data, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) await this.emitPair(pair); + } + + async handleVerdict(verdict: Verdict): Promise { + const record = this.eventMap.get(verdict.eventId); + this.eventMap.delete(verdict.eventId); + if (!record) return; + const ctx: VerdictContext = { + eventId: verdict.eventId, + sessionId: verdict.sessionId, + eventType: record.eventType, + eventData: record.data, + runId: record.runId, + parentRunId: record.parentRunId, + policy: verdict.policy, + madCode: verdict.madCode, + hitl: verdict.hitl, + }; + await this.config.onVerdict?.(ctx); + const prefix = verdict.madCode.slice(0, 2); + if ((prefix === "M3" || prefix === "M4") && this.config.onBlock) await this.config.onBlock(ctx); + if (prefix === "M2" && this.config.onAudit) await this.config.onAudit(ctx); + } + + private async emitPair(pair: PairedEvent): Promise { + await this.hooks.emit(pair); + this.eventMap.set(pair.eventId, { + eventType: pair.pairType, + data: pair.data as unknown as EventData, + runId: pair.runId, + parentRunId: pair.parentRunId || null, + }); + await this.config.onEvent?.(pair.pairType, pair.data as unknown as EventData, pair.runId, pair.parentRunId || null, pair.eventId); + } + + private resolveSessionId(): string { + const cfg = currentConfig() ?? this.config; + if (!cfg.sessionId) throw new Error("session_id is not set, init() must be called before capturing events"); + return cfg.sessionId; + } + + private resolveInvocationId(): string { + return getInvocationId() ?? "no_invocation"; + } +} + +export function extractModelName(serialized: Record | null | undefined): string { + if (!serialized) return "unknown"; + if (typeof serialized.name === "string") return serialized.name; + if (Array.isArray(serialized.id) && serialized.id.length > 0) return String(serialized.id.at(-1)); + const kwargs = serialized.kwargs; + if (kwargs && typeof kwargs === "object" && "model_name" in kwargs) return String((kwargs as Record).model_name); + return "unknown"; +} + +function extractMetadata(extraParams?: Record): CallbackMetadata | null { + const raw = extraParams?.metadata; + if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) return null; + return raw as CallbackMetadata; +} + +function messageToChatMessage(message: unknown): ChatMessage { + const obj = message && typeof message === "object" ? message as Record : {}; + const role = String(obj.type ?? obj.role ?? "unknown"); + const content = obj.content; + return { role, content: typeof content === "string" ? content : JSON.stringify(content ?? "") }; +} + +function extractLlmEndData(output: unknown): LlmEndData { + const generations = (output as { generations?: unknown[][] })?.generations ?? []; + const first = generations[0]?.[0] as Record | undefined; + const text = typeof first?.text === "string" ? first.text : ""; + const message = first?.message as Record | undefined; + const rawCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : Array.isArray(message?.toolCalls) ? message.toolCalls : []; + const toolCalls: ToolCallRecord[] = rawCalls.map((call) => { + const obj = call && typeof call === "object" ? call as Record : {}; + return { id: String(obj.id ?? ""), name: String(obj.name ?? ""), args: (obj.args && typeof obj.args === "object" ? obj.args : {}) as ToolCallRecord["args"] }; + }); + const llmOutput = (output as { llmOutput?: Record; llm_output?: Record })?.llmOutput ?? (output as { llm_output?: Record })?.llm_output ?? {}; + const usageRaw = llmOutput.tokenUsage ?? llmOutput.token_usage; + const usageObj = usageRaw && typeof usageRaw === "object" ? usageRaw as Record : null; + return { + output: text, + toolCalls, + usage: usageObj ? { + promptTokens: Number(usageObj.promptTokens ?? usageObj.prompt_tokens ?? 0), + completionTokens: Number(usageObj.completionTokens ?? usageObj.completion_tokens ?? 0), + totalTokens: Number(usageObj.totalTokens ?? usageObj.total_tokens ?? 0), + } : null, + }; +} + +function stringifyOutput(output: unknown): string { + if (typeof output === "string") return output; + try { + return JSON.stringify(output); + } catch { + return String(output); + } +} diff --git a/sdk/typescript/src/handlers/jsonl.ts b/sdk/typescript/src/handlers/jsonl.ts new file mode 100644 index 0000000..25e3be5 --- /dev/null +++ b/sdk/typescript/src/handlers/jsonl.ts @@ -0,0 +1,36 @@ +import { mkdir, open, type FileHandle } from "node:fs/promises"; +import { dirname } from "node:path"; +import type { PairedEvent } from "../format/types.js"; +import type { EventHandler } from "../types.js"; + +export class JSONLHandler implements EventHandler { + readonly path: string; + private filePromise: Promise; + private chain: Promise = Promise.resolve(); + + constructor(path: string) { + this.path = path; + this.filePromise = this.openFile(path); + } + + async onPairedEvent(event: PairedEvent): Promise { + const line = JSON.stringify(event) + "\n"; + this.chain = this.chain.then(async () => { + const file = await this.filePromise; + await file.write(line); + await file.sync(); + }); + return this.chain; + } + + async close(): Promise { + await this.chain; + const file = await this.filePromise; + await file.close(); + } + + private async openFile(path: string): Promise { + await mkdir(dirname(path), { recursive: true }); + return open(path, "w"); + } +} diff --git a/sdk/typescript/src/hooks.ts b/sdk/typescript/src/hooks.ts new file mode 100644 index 0000000..759b172 --- /dev/null +++ b/sdk/typescript/src/hooks.ts @@ -0,0 +1,34 @@ +import type { PairedEvent } from "./format/types.js"; +import type { EventHandler } from "./types.js"; + +export class HookRegistry { + private handlers: EventHandler[] = []; + + register(handler: EventHandler): void { + this.handlers.push(handler); + } + + get size(): number { + return this.handlers.length; + } + + async emit(event: PairedEvent): Promise { + for (const handler of this.handlers) { + try { + await handler.onPairedEvent(event); + } catch (error) { + console.error("adrian handler failed", { handler: handler.constructor?.name, eventId: event.eventId, error }); + } + } + } + + async close(): Promise { + for (const handler of this.handlers) { + try { + await handler.close(); + } catch (error) { + console.error("adrian handler close failed", { handler: handler.constructor?.name, error }); + } + } + } +} diff --git a/sdk/typescript/src/identity.ts b/sdk/typescript/src/identity.ts new file mode 100644 index 0000000..19001f6 --- /dev/null +++ b/sdk/typescript/src/identity.ts @@ -0,0 +1,22 @@ +import type { CallbackMetadata, ChatMessage } from "./types.js"; + +const CHECKPOINT_NS_KEY = "langgraph_checkpoint_ns"; + +export function isLangGraphMetadata(metadata: CallbackMetadata): boolean { + return CHECKPOINT_NS_KEY in metadata; +} + +export function deriveLangGraphAgentId(metadata: CallbackMetadata): string | null { + const ns = metadata[CHECKPOINT_NS_KEY]; + if (typeof ns !== "string" || ns.length === 0) return null; + const nodeNames = ns.split("|").map((segment) => segment.split(":")[0]).filter(Boolean); + return nodeNames.length > 0 ? nodeNames.join("|") : null; +} + +export function deriveAgentId(metadata: CallbackMetadata | null, _messages?: ChatMessage[] | null): string { + if (metadata) { + const langGraphId = deriveLangGraphAgentId(metadata); + if (langGraphId !== null) return langGraphId; + } + return "default"; +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts new file mode 100644 index 0000000..0c08e60 --- /dev/null +++ b/sdk/typescript/src/index.ts @@ -0,0 +1,114 @@ +import { setConfig, type AdrianConfig, type InitOptions } from "./config.js"; +import { AgentContextTracker } from "./context.js"; +import { AdrianCallbackHandler } from "./handler.js"; +import { JSONLHandler } from "./handlers/jsonl.js"; +import { HookRegistry } from "./hooks.js"; +import { patchMcpAdapters, mcpServers } from "./mcp.js"; +import { EventPairBuffer } from "./pairing.js"; +import { RedactingHandler } from "./pii/index.js"; +import { envAwareResolveSessionId } from "./sessionPersistence.js"; +import { autoInstrument } from "./instrumentation/langchain.js"; +import { WebSocketClient } from "./ws.js"; +import type { EventHandler, McpServer } from "./types.js"; + +export const version = "1.0.0"; +export const __version__ = version; + +let hooks: HookRegistry | null = null; +let handler: AdrianCallbackHandler | null = null; +let wsClient: WebSocketClient | null = null; + +export async function init(options: InitOptions = {}): Promise { + const apiKey = options.apiKey ?? process.env.ADRIAN_API_KEY ?? null; + const logFile = process.env.ADRIAN_LOG_FILE ?? options.logFile ?? "events.jsonl"; + const wsUrl = process.env.ADRIAN_WS_URL ?? options.wsUrl ?? "ws://localhost:8080/ws"; + const sessionId = await envAwareResolveSessionId(options.sessionId ?? null); + const blockTimeout = Number(process.env.ADRIAN_BLOCK_TIMEOUT ?? options.blockTimeout ?? 30); + const replayBufferFrames = parseInt(process.env.ADRIAN_REPLAY_BUFFER_FRAMES ?? String(options.replayBufferFrames ?? 1000), 10); + + const config: AdrianConfig = { + apiKey, + logFile, + logLevel: options.logLevel ?? null, + sessionId, + wsUrl, + blockTimeout, + onEvent: options.onEvent ?? null, + onVerdict: options.onVerdict ?? null, + onBlock: options.onBlock ?? null, + onAudit: options.onAudit ?? null, + onDisconnect: options.onDisconnect ?? null, + onReconnect: options.onReconnect ?? null, + onMcpServer: chainMcpServerCallback(options.onMcpServer ?? null), + replayBufferFrames: Number.isFinite(replayBufferFrames) ? replayBufferFrames : 1000, + }; + setConfig(config); + + const handlerList: EventHandler[] = options.handlers ? [...options.handlers] : [new JSONLHandler(logFile)]; + if (!options.handlers && wsUrl) { + if (!apiKey) console.warn("ADRIAN wsUrl is set but no apiKey was provided; the server will reject the connection."); + wsClient = new WebSocketClient({ + url: wsUrl, + sessionId, + apiKey: apiKey ?? "", + onDisconnect: config.onDisconnect, + onReconnect: config.onReconnect, + onLoginAck: sendMcpInventory, + replayBufferFrames: config.replayBufferFrames, + }); + handlerList.push(wsClient); + } + + hooks = new HookRegistry(); + for (const eventHandler of handlerList.map((h) => new RedactingHandler(h))) hooks.register(eventHandler); + handler = new AdrianCallbackHandler({ pairBuffer: new EventPairBuffer(), contextTracker: new AgentContextTracker(), hooks, config }); + if (wsClient) { + wsClient.handler = handler; + wsClient.scheduleConnect(); + } + + await patchMcpAdapters(); + if (options.autoInstrument ?? true) await autoInstrument(getHandler, getWebSocketClient); +} + +export async function shutdown(): Promise { + await hooks?.close(); + hooks = null; + handler = null; + wsClient = null; + setConfig(null); +} + +export function getHandler(): AdrianCallbackHandler | null { + return handler; +} + +export function getWebSocketClient(): WebSocketClient | null { + return wsClient; +} + +async function sendMcpInventory(): Promise { + await wsClient?.sendMcpInventory(mcpServers()); +} + +function chainMcpServerCallback(userCallback: ((server: McpServer) => void | Promise) | null) { + return async (server: McpServer) => { + await sendMcpInventory(); + await userCallback?.(server); + }; +} + +export { AdrianCallbackHandler } from "./handler.js"; +export { JSONLHandler } from "./handlers/jsonl.js"; +export { HookRegistry } from "./hooks.js"; +export { EventPairBuffer } from "./pairing.js"; +export { AgentContextTracker, getInvocationId, runWithInvocationId } from "./context.js"; +export { deriveAgentId, deriveLangGraphAgentId } from "./identity.js"; +export { WebSocketClient, shouldHalt } from "./ws.js"; +export { mcpServers, registerMcpServer, registerMcpConnection } from "./mcp.js"; +export { resolveSessionId, envAwareResolveSessionId } from "./sessionPersistence.js"; +export * from "./config.js"; +export * from "./types.js"; +export * from "./format/types.js"; +export * from "./pii/index.js"; +export * from "./proto/schema.js"; diff --git a/sdk/typescript/src/instrumentation/langchain.ts b/sdk/typescript/src/instrumentation/langchain.ts new file mode 100644 index 0000000..8c1e25a --- /dev/null +++ b/sdk/typescript/src/instrumentation/langchain.ts @@ -0,0 +1,136 @@ +import { randomUUID } from "node:crypto"; +import type { AdrianCallbackHandler } from "../handler.js"; +import { runWithInvocationId } from "../context.js"; +import { shouldHalt, type WebSocketClient } from "../ws.js"; +import { currentConfig } from "../config.js"; + +let patched = false; +const CALLBACK_METHODS = ["invoke", "stream", "batch", "ainvoke", "astream"] as const; +const GRAPH_METHODS = ["invoke", "stream", "ainvoke", "astream"] as const; +const TOOL_NODE_METHODS = ["invoke", "ainvoke"] as const; + +export async function autoInstrument(getHandler: () => AdrianCallbackHandler | null, getWebSocketClient: () => WebSocketClient | null): Promise { + if (patched) return; + patched = true; + await Promise.allSettled([ + patchRunnable(getHandler), + patchBaseChatModel(getHandler), + patchLangGraph(getHandler), + patchToolNode(getHandler, getWebSocketClient), + ]); +} + +function injectCallbacks(config: unknown, handler: AdrianCallbackHandler | null): unknown { + if (!handler) return config ?? {}; + const next = { ...((config && typeof config === "object") ? config as Record : {}) }; + const callbacks = next.callbacks; + if (Array.isArray(callbacks)) { + if (!callbacks.some((cb) => cb?.constructor?.name === "AdrianCallbackHandler")) next.callbacks = [handler, ...callbacks]; + } else if (callbacks) { + next.callbacks = [handler, callbacks]; + } else { + next.callbacks = [handler]; + } + return next; +} + +async function patchRunnable(getHandler: () => AdrianCallbackHandler | null): Promise { + const mod = await importOptional("@langchain/core/runnables"); + const proto = (mod?.Runnable as { prototype?: Record; _adrianPatched?: boolean } | undefined)?.prototype; + if (!proto || (mod.Runnable as { _adrianPatched?: boolean })._adrianPatched) return; + for (const name of CALLBACK_METHODS) { + const original = proto[name]; + if (typeof original !== "function") continue; + proto[name] = function patchedInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { + return original.call(this, input, injectCallbacks(config, getHandler()), ...rest); + }; + } + (mod.Runnable as { _adrianPatched?: boolean })._adrianPatched = true; +} + +async function patchBaseChatModel(getHandler: () => AdrianCallbackHandler | null): Promise { + const mod = await importOptional("@langchain/core/language_models/chat_models"); + const proto = (mod?.BaseChatModel as { prototype?: Record; _adrianPatched?: boolean } | undefined)?.prototype; + if (!proto || (mod.BaseChatModel as { _adrianPatched?: boolean })._adrianPatched) return; + for (const name of CALLBACK_METHODS) { + const original = proto[name]; + if (typeof original !== "function") continue; + proto[name] = function patchedChatInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { + return original.call(this, input, injectCallbacks(config, getHandler()), ...rest); + }; + } + (mod.BaseChatModel as { _adrianPatched?: boolean })._adrianPatched = true; +} + +async function patchLangGraph(getHandler: () => AdrianCallbackHandler | null): Promise { + const mod = await importOptional("@langchain/langgraph"); + const graphClasses = [mod?.CompiledStateGraph, mod?.StateGraph, mod?.Pregel].filter(Boolean) as Array<{ prototype?: Record; _adrianPatched?: boolean }>; + for (const cls of graphClasses) { + const proto = cls.prototype; + if (!proto || cls._adrianPatched) continue; + for (const name of GRAPH_METHODS) { + const original = proto[name]; + if (typeof original !== "function") continue; + proto[name] = function patchedGraphInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { + const run = () => original.call(this, input, injectCallbacks(config, getHandler()), ...rest); + return runWithInvocationId(randomUUID(), run); + }; + } + cls._adrianPatched = true; + } +} + +async function patchToolNode(getHandler: () => AdrianCallbackHandler | null, getWebSocketClient: () => WebSocketClient | null): Promise { + const mod = await importOptional("@langchain/langgraph/prebuilt"); + const cls = mod?.ToolNode as { prototype?: Record; _adrianPatched?: boolean } | undefined; + const proto = cls?.prototype; + if (!cls || !proto || cls._adrianPatched) return; + for (const name of TOOL_NODE_METHODS) { + const original = proto[name]; + if (typeof original !== "function") continue; + proto[name] = async function patchedToolNodeInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { + const nextConfig = injectCallbacks(config, getHandler()); + const blockedResponse = await blockedToolNodeResponse(input, getWebSocketClient()); + if (blockedResponse) return blockedResponse; + return original.call(this, input, nextConfig, ...rest); + }; + } + cls._adrianPatched = true; +} + +export async function blockedToolNodeResponse(input: unknown, ws: WebSocketClient | null): Promise<{ messages: Array> } | null> { + if (!ws) return null; + const cfg = currentConfig(); + const policyReady = await ws.waitForPolicyReady(cfg?.blockTimeout ?? 30); + if (!policyReady || !ws.policyActive()) return null; + const toolCalls = extractToolCalls(input); + if (toolCalls.length === 0) return null; + if (toolCalls.some((call) => !call.id)) return buildBlockedResponse(toolCalls); + + const timeout = ws.blockTimeout(cfg?.blockTimeout ?? 30); + const verdicts = await Promise.all(toolCalls.map((call) => ws.waitForToolCallVerdict(call.id, timeout))); + if (verdicts.some((verdict) => !verdict || shouldHalt(verdict))) return buildBlockedResponse(toolCalls); + return null; +} + +export function extractToolCalls(input: unknown): Array<{ id: string; name: string }> { + const messages = Array.isArray(input) ? input : (input && typeof input === "object" ? (input as Record).messages : []); + if (!Array.isArray(messages)) return []; + for (const message of [...messages].reverse()) { + const calls = message && typeof message === "object" ? (message as Record).tool_calls ?? (message as Record).toolCalls : null; + if (Array.isArray(calls)) return calls.map((call) => ({ id: String((call as Record).id ?? ""), name: String((call as Record).name ?? "") })); + } + return []; +} + +export function buildBlockedResponse(toolCalls: Array<{ id: string; name: string }>): { messages: Array> } { + return { messages: toolCalls.map((call) => ({ content: "[BLOCKED by security policy]", tool_call_id: call.id, name: call.name, type: "tool" })) }; +} + +async function importOptional(specifier: string): Promise { + try { + return await import(specifier); + } catch { + return null; + } +} diff --git a/sdk/typescript/src/mcp.ts b/sdk/typescript/src/mcp.ts new file mode 100644 index 0000000..55256f4 --- /dev/null +++ b/sdk/typescript/src/mcp.ts @@ -0,0 +1,106 @@ +import { currentConfig, isInitialized } from "./config.js"; +import type { McpServer } from "./types.js"; + +const servers = new Map(); + +export function mcpServers(): McpServer[] { + return [...servers.values()]; +} + +export function resetMcpServers(): void { + servers.clear(); +} + +export function registerMcpServer(server: McpServer): void { + const previous = servers.get(server.name); + if (previous?.transport === server.transport && previous.endpoint === server.endpoint) return; + servers.set(server.name, server); + if (isInitialized()) void currentConfig()?.onMcpServer?.(server); +} + +export function registerMcpConnection(name: string, connection: unknown): void { + if (!name) return; + registerMcpServer(serverFromConnection(name, connection)); +} + +export async function patchMcpAdapters(): Promise { + await Promise.allSettled([patchLangchainMcpAdapters(), patchMcpTransports()]); +} + +function serverFromConnection(name: string, connection: unknown): McpServer { + if (!connection || typeof connection !== "object") return { name, transport: "unknown", endpoint: "" }; + const conn = connection as Record; + const transport = String(conn.transport ?? "unknown").toLowerCase(); + return { name, transport, endpoint: endpointFor(transport, conn) }; +} + +function endpointFor(transport: string, conn: Record): string { + if (transport === "stdio") { + const command = String(conn.command ?? ""); + const args = Array.isArray(conn.args) ? conn.args.map(String) : []; + return [command, ...args].filter(Boolean).join(" "); + } + if (["sse", "websocket", "streamable_http", "streamable-http", "http"].includes(transport)) return String(conn.url ?? ""); + return ""; +} + +async function patchLangchainMcpAdapters(): Promise { + const mod = await importOptional("langchain-mcp-adapters"); + const Client = mod?.MultiServerMCPClient ?? mod?.client?.MultiServerMCPClient; + if (!Client || Client._adrianMcpPatched) return; + const original = Client.prototype.constructor; + const originalInit = Client.prototype.__init__ ?? original; + if (typeof originalInit !== "function") return; + Client.prototype.__init__ = function patchedInit(...args: unknown[]) { + const result = originalInit.apply(this, args); + registerAllFromClient(this as Record); + return result; + }; + Client._adrianMcpPatched = true; +} + +async function patchMcpTransports(): Promise { + const targets: Array<[string, string, string]> = [ + ["@modelcontextprotocol/sdk/client/stdio.js", "stdio_client", "stdio"], + ["@modelcontextprotocol/sdk/client/sse.js", "sse_client", "sse"], + ["@modelcontextprotocol/sdk/client/websocket.js", "websocket_client", "websocket"], + ]; + for (const [specifier, attr, transport] of targets) { + const mod = await importOptional(specifier); + const original = mod?.[attr]; + if (!original || original._adrianMcpPatched) continue; + mod[attr] = function patchedTransport(...args: unknown[]) { + registerSynthesised(transport, endpointFromTransportArgs(transport, args)); + return original(...args); + }; + mod[attr]._adrianMcpPatched = true; + } +} + +function registerAllFromClient(client: Record): void { + const connections = client.connections; + if (!connections || typeof connections !== "object") return; + for (const [name, connection] of Object.entries(connections)) registerMcpConnection(name, connection); +} + +function registerSynthesised(transport: string, endpoint: string): void { + if (!endpoint && transport === "unknown") return; + if ([...servers.values()].some((server) => server.transport === transport && server.endpoint === endpoint)) return; + registerMcpServer({ name: endpoint ? `${transport}:${endpoint}` : transport, transport, endpoint }); +} + +function endpointFromTransportArgs(transport: string, args: unknown[]): string { + const first = args[0]; + if (transport === "stdio" && first && typeof first === "object") { + const params = first as Record; + return [String(params.command ?? ""), ...(Array.isArray(params.args) ? params.args.map(String) : [])].filter(Boolean).join(" "); + } + if (typeof first === "string") return first; + if (first instanceof URL) return first.toString(); + if (first && typeof first === "object" && "url" in first) return String((first as Record).url ?? ""); + return ""; +} + +async function importOptional(specifier: string): Promise { + try { return await import(specifier); } catch { return null; } +} diff --git a/sdk/typescript/src/pairing.ts b/sdk/typescript/src/pairing.ts new file mode 100644 index 0000000..53f0c29 --- /dev/null +++ b/sdk/typescript/src/pairing.ts @@ -0,0 +1,107 @@ +import { randomUUID } from "node:crypto"; +import type { AgentContext, PairedEvent, ParentContext } from "./format/types.js"; +import type { CallbackMetadata, ChatModelStartData, LlmEndData, ToolEndData, ToolStartData } from "./types.js"; + +interface StartEventRecord { + eventType: "chat_model_start" | "tool_start"; + data: ChatModelStartData | ToolStartData; + agentId: string; + parentRunId: string; + parent: ParentContext | null; + metadata: CallbackMetadata | null; + agentContext: AgentContext | null; +} + +export class EventPairBuffer { + private pending = new Map(); + + onStart(args: { + eventType: "chat_model_start" | "tool_start"; + data: ChatModelStartData | ToolStartData; + runId: string; + agentId: string; + parent: ParentContext | null; + metadata: CallbackMetadata | null; + agentContext?: AgentContext | null; + parentRunId?: string; + }): void { + this.pending.set(args.runId, { + eventType: args.eventType, + data: args.data, + agentId: args.agentId, + parentRunId: args.parentRunId ?? "", + parent: args.parent, + metadata: args.metadata, + agentContext: args.agentContext ?? null, + }); + } + + onEnd(args: { + eventType: "llm_end" | "tool_end"; + data: LlmEndData | ToolEndData; + runId: string; + invocationId: string; + sessionId: string; + }): PairedEvent | null { + const start = this.pending.get(args.runId); + this.pending.delete(args.runId); + if (!start) return null; + if (args.eventType === "llm_end" && start.eventType === "chat_model_start") { + return this.assembleLlmPair(start, args.data as LlmEndData, args.runId, args.invocationId, args.sessionId); + } + if (args.eventType === "tool_end" && start.eventType === "tool_start") { + return this.assembleToolPair(start, args.data as ToolEndData, args.runId, args.invocationId, args.sessionId); + } + return null; + } + + private assembleLlmPair(start: StartEventRecord, endData: LlmEndData, runId: string, invocationId: string, sessionId: string): PairedEvent { + const startData = start.data as ChatModelStartData; + const messages = startData.messages ?? []; + const systemPrompt = messages.find((msg) => msg.role === "system")?.content ?? ""; + const userInstruction = [...messages].reverse().find((msg) => msg.role === "human" || msg.role === "user")?.content ?? ""; + return { + eventId: randomUUID(), + invocationId, + sessionId, + runId, + parentRunId: start.parentRunId, + timestamp: new Date().toISOString(), + pairType: "llm", + agent: { agentId: start.agentId, systemPrompt, userInstruction }, + parent: start.parent, + data: { + kind: "llm", + model: startData.model ?? "unknown", + messages, + output: endData.output ?? "", + toolCalls: endData.toolCalls ?? [], + usage: endData.usage ?? null, + }, + metadata: start.metadata, + }; + } + + private assembleToolPair(start: StartEventRecord, endData: ToolEndData, runId: string, invocationId: string, sessionId: string): PairedEvent { + const startData = start.data as ToolStartData; + return { + eventId: randomUUID(), + invocationId, + sessionId, + runId, + parentRunId: start.parentRunId, + timestamp: new Date().toISOString(), + pairType: "tool", + agent: start.agentContext ?? { agentId: start.agentId, systemPrompt: "", userInstruction: "" }, + parent: start.parent, + data: { + kind: "tool", + toolName: startData.toolName ?? "unknown", + toolCallId: startData.toolCallId ?? null, + input: startData.input ?? "", + output: endData.output ?? "", + }, + metadata: start.metadata, + }; + } +} diff --git a/sdk/typescript/src/pii/engine.ts b/sdk/typescript/src/pii/engine.ts new file mode 100644 index 0000000..b580a47 --- /dev/null +++ b/sdk/typescript/src/pii/engine.ts @@ -0,0 +1,24 @@ +import { detect, type Detection, type PiiType } from "./patterns.js"; +import { applyStrategy, RedactionStrategy } from "./strategies.js"; + +export interface PiiConfig { + strategy?: RedactionStrategy; + enabledTypes?: ReadonlySet | PiiType[] | null; +} + +export interface RedactionResult { + text: string; + detections: Detection[]; +} + +export function redactText(text: string, config: PiiConfig = {}): RedactionResult { + if (!text) return { text, detections: [] }; + const enabledTypes = Array.isArray(config.enabledTypes) ? new Set(config.enabledTypes) : config.enabledTypes ?? null; + const detections = detect(text, enabledTypes); + if (detections.length === 0) return { text, detections }; + let result = text; + for (const detection of [...detections].reverse()) { + result = result.slice(0, detection.start) + applyStrategy(detection, config.strategy ?? RedactionStrategy.REPLACE) + result.slice(detection.end); + } + return { text: result, detections }; +} diff --git a/sdk/typescript/src/pii/index.ts b/sdk/typescript/src/pii/index.ts new file mode 100644 index 0000000..04b17c2 --- /dev/null +++ b/sdk/typescript/src/pii/index.ts @@ -0,0 +1,6 @@ +export { redactText } from "./engine.js"; +export type { PiiConfig, RedactionResult } from "./engine.js"; +export { PiiType, detect } from "./patterns.js"; +export type { Detection } from "./patterns.js"; +export { RedactionStrategy } from "./strategies.js"; +export { PiiRedactor, RedactingHandler } from "./redactor.js"; diff --git a/sdk/typescript/src/pii/patterns.ts b/sdk/typescript/src/pii/patterns.ts new file mode 100644 index 0000000..6c22718 --- /dev/null +++ b/sdk/typescript/src/pii/patterns.ts @@ -0,0 +1,65 @@ +export enum PiiType { + EMAIL = "EMAIL", + PHONE = "PHONE", + SSN = "SSN", + CREDIT_CARD = "CREDIT_CARD", + IP_ADDRESS = "IP_ADDRESS", + DATE_OF_BIRTH = "DATE_OF_BIRTH", + IBAN = "IBAN", + PASSPORT = "PASSPORT", + STREET_ADDRESS = "STREET_ADDRESS", + POSTAL_CODE = "POSTAL_CODE", + DRIVER_LICENSE = "DRIVER_LICENSE", + AWS_KEY = "AWS_KEY", +} + +export interface Detection { + piiType: PiiType; + start: number; + end: number; + text: string; +} + +const PATTERNS: Array<[PiiType, RegExp]> = [ + [PiiType.EMAIL, /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g], + [PiiType.PHONE, /(? | null): Detection[] { + const detections: Detection[] = []; + for (const [piiType, pattern] of PATTERNS) { + if (types && !types.has(piiType)) continue; + pattern.lastIndex = 0; + for (const match of text.matchAll(pattern)) { + const matched = match[0]; + const start = match.index ?? 0; + if (piiType === PiiType.CREDIT_CARD && !luhnCheck(matched.replace(/\D/g, ""))) continue; + detections.push({ piiType, start, end: start + matched.length, text: matched }); + } + } + return detections.sort((a, b) => a.start - b.start || b.end - a.end); +} + +function luhnCheck(digits: string): boolean { + let total = 0; + const reverse = [...digits].reverse(); + for (let i = 0; i < reverse.length; i += 1) { + let n = Number(reverse[i]); + if (i % 2 === 1) { + n *= 2; + if (n > 9) n -= 9; + } + total += n; + } + return digits.length >= 13 && total % 10 === 0; +} diff --git a/sdk/typescript/src/pii/redactor.ts b/sdk/typescript/src/pii/redactor.ts new file mode 100644 index 0000000..b59d559 --- /dev/null +++ b/sdk/typescript/src/pii/redactor.ts @@ -0,0 +1,82 @@ +import type { PairedEvent } from "../format/types.js"; +import type { EventHandler, JsonValue } from "../types.js"; +import { redactText, type PiiConfig } from "./engine.js"; +import { detect } from "./patterns.js"; + +export class PiiRedactor { + private config: PiiConfig; + constructor(config: PiiConfig = {}) { + this.config = config; + } + + redactEvent(event: PairedEvent, options: { inPlace?: boolean } = {}): PairedEvent { + const target = options.inPlace ? event : structuredClone(event); + target.agent.systemPrompt = this.redactString(target.agent.systemPrompt); + target.agent.userInstruction = this.redactString(target.agent.userInstruction); + if (target.parent) { + target.parent.systemPrompt = this.redactString(target.parent.systemPrompt); + target.parent.userInstruction = this.redactString(target.parent.userInstruction); + } + if (target.data.kind === "llm") { + for (const message of target.data.messages) message.content = this.redactString(message.content); + target.data.output = this.redactString(target.data.output); + for (const call of target.data.toolCalls) call.args = this.redactValue(call.args) as typeof call.args; + } else { + target.data.input = this.redactString(target.data.input); + target.data.output = this.redactString(target.data.output); + } + return target; + } + + eventHasPii(event: PairedEvent): boolean { + const enabledTypes = Array.isArray(this.config.enabledTypes) ? new Set(this.config.enabledTypes) : this.config.enabledTypes ?? null; + return this.iterEventText(event).some((text) => detect(text, enabledTypes).length > 0); + } + + private redactString(text: string): string { + return redactText(text, this.config).text; + } + + private redactValue(value: JsonValue): JsonValue { + if (typeof value === "string") return this.redactString(value); + if (Array.isArray(value)) return value.map((item) => this.redactValue(item)); + if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, this.redactValue(val)])); + return value; + } + + private iterEventText(event: PairedEvent): string[] { + const texts = [event.agent.systemPrompt, event.agent.userInstruction]; + if (event.parent) texts.push(event.parent.systemPrompt, event.parent.userInstruction); + if (event.data.kind === "llm") { + texts.push(...event.data.messages.map((msg) => msg.content), event.data.output); + for (const call of event.data.toolCalls) collectStrings(call.args, texts); + } else { + texts.push(event.data.input, event.data.output); + } + return texts; + } +} + +export class RedactingHandler implements EventHandler { + private inner: EventHandler; + private redactor: PiiRedactor; + constructor(inner: EventHandler, config: PiiConfig = {}) { + this.inner = inner; + this.redactor = new PiiRedactor(config); + } + + async onPairedEvent(event: PairedEvent): Promise { + const next = this.redactor.eventHasPii(event) ? this.redactor.redactEvent(event) : event; + await this.inner.onPairedEvent(next); + } + + async close(): Promise { + await this.inner.close(); + } +} + +function collectStrings(value: JsonValue, sink: string[]): void { + if (typeof value === "string") sink.push(value); + else if (Array.isArray(value)) value.forEach((item) => collectStrings(item, sink)); + else if (value && typeof value === "object") Object.values(value).forEach((item) => collectStrings(item, sink)); +} diff --git a/sdk/typescript/src/pii/strategies.ts b/sdk/typescript/src/pii/strategies.ts new file mode 100644 index 0000000..0e6b34e --- /dev/null +++ b/sdk/typescript/src/pii/strategies.ts @@ -0,0 +1,38 @@ +import { createHash } from "node:crypto"; +import { Detection, PiiType } from "./patterns.js"; + +export enum RedactionStrategy { + REPLACE = "replace", + MASK = "mask", + HASH = "hash", +} + +export function applyStrategy(detection: Detection, strategy: RedactionStrategy): string { + if (strategy === RedactionStrategy.HASH) { + const digest = createHash("sha256").update(detection.text).digest("hex").slice(0, 8); + return `[${detection.piiType}:${digest}]`; + } + if (strategy === RedactionStrategy.MASK) return mask(detection); + return `[${detection.piiType}_REDACTED]`; +} + +function mask(detection: Detection): string { + const text = detection.text; + switch (detection.piiType) { + case PiiType.EMAIL: { + const [local, domain] = text.split("@"); + const suffix = domain?.split(".").at(-1) ?? ""; + return `${local?.[0] ?? "*"}***@***.${suffix}`; + } + case PiiType.PHONE: + return `***-***-${text.replace(/\D/g, "").slice(-4)}`; + case PiiType.SSN: + return `***-**-${text.replace(/\D/g, "").slice(-4)}`; + case PiiType.CREDIT_CARD: + return `****-****-****-${text.replace(/\D/g, "").slice(-4)}`; + case PiiType.IP_ADDRESS: + return text.includes(":") ? `****:${text.split(":").at(-1) ?? ""}` : `***.***.***.${text.split(".").at(-1) ?? ""}`; + default: + return text.length <= 2 ? "*".repeat(text.length) : text[0] + "*".repeat(text.length - 2) + text.at(-1); + } +} diff --git a/sdk/typescript/src/proto/schema.ts b/sdk/typescript/src/proto/schema.ts new file mode 100644 index 0000000..f5d886b --- /dev/null +++ b/sdk/typescript/src/proto/schema.ts @@ -0,0 +1,161 @@ +import protobuf from "protobufjs"; +import type { PairedEvent } from "../format/types.js"; +import type { McpServer } from "../types.js"; + +export const SCHEMA_VERSION = 2; + +export enum PairTypeProto { + PAIR_TYPE_UNSPECIFIED = 0, + PAIR_TYPE_LLM = 1, + PAIR_TYPE_TOOL = 2, +} + +export enum Mode { + MODE_UNSPECIFIED = 0, + MODE_ALERT = 1, + MODE_HITL = 2, + MODE_BLOCK = 3, +} + +export interface PolicySnapshot { + mode: Mode; + policyM0: boolean; + policyM2: boolean; + policyM3: boolean; + policyM4: boolean; +} + +export interface HitlResponse { + continueExecution: boolean; +} + +export interface Verdict { + eventId: string; + sessionId: string; + madCode: string; + policy: PolicySnapshot; + hitl: HitlResponse | null; +} + +export interface LoginAck { + policy: PolicySnapshot; +} + +export type ClientFrame = + | { login: { sessionId: string; llmStack: { provider: string; model: string }; schemaVersion: number } } + | { pairedBatch: { events: PairedEvent[] } } + | { mcpInventory: { servers: McpServer[] } }; + +export type ServerFrame = + | { loginAck: LoginAck } + | { verdict: Verdict }; + +const protoSource = ` +syntax = "proto3"; +package adrian.core_api.v1; +enum PairType { PAIR_TYPE_UNSPECIFIED = 0; PAIR_TYPE_LLM = 1; PAIR_TYPE_TOOL = 2; } +message ChatMessage { string role = 1; string content = 2; } +message ToolCall { string name = 1; string args = 2; string id = 3; } +message TokenUsage { int32 prompt_tokens = 1; int32 completion_tokens = 2; int32 total_tokens = 3; } +message AgentContext { string agent_id = 1; string system_prompt = 2; string user_instruction = 3; } +message LlmPairData { string model = 1; repeated ChatMessage messages = 2; string output = 3; repeated ToolCall tool_calls = 4; TokenUsage usage = 5; } +message ToolPairData { string tool_name = 1; string tool_call_id = 2; string input = 3; string output = 4; } +message PairedEvent { string event_id = 1; string invocation_id = 2; string session_id = 3; string run_id = 4; string parent_run_id = 5; string timestamp = 6; PairType pair_type = 7; AgentContext agent = 8; AgentContext parent = 9; oneof data { LlmPairData llm = 10; ToolPairData tool = 11; } bytes metadata_json = 20; } +message PairedEventBatch { repeated PairedEvent events = 1; } +message McpServer { string name = 1; string transport = 2; string endpoint = 3; } +message McpInventory { repeated McpServer servers = 1; } +message LLMStack { string provider = 1; string model = 2; } +message SessionLogin { string session_id = 1; LLMStack llm_stack = 2; reserved 3; uint32 schema_version = 4; } +message ClientFrame { reserved 2; oneof frame { SessionLogin login = 1; PairedEventBatch paired_batch = 3; McpInventory mcp_inventory = 4; } } +enum Mode { MODE_UNSPECIFIED = 0; MODE_ALERT = 1; MODE_HITL = 2; MODE_BLOCK = 3; } +message PolicySnapshot { Mode mode = 1; bool policy_m0 = 2; bool policy_m2 = 3; bool policy_m3 = 4; bool policy_m4 = 5; } +message HitlResponse { bool continue_execution = 1; } +message LoginAck { PolicySnapshot policy = 1; } +message ServerFrame { oneof frame { LoginAck login_ack = 1; Verdict verdict = 2; } } +message Verdict { string event_id = 1; string session_id = 2; reserved 3; string mad_code = 4; reserved 5; PolicySnapshot policy = 6; HitlResponse hitl = 7; } +`; + +const root = protobuf.parse(protoSource, { keepCase: true }).root; +const ClientFrameType = root.lookupType("adrian.core_api.v1.ClientFrame"); +const ServerFrameType = root.lookupType("adrian.core_api.v1.ServerFrame"); + +export function encodeClientFrame(frame: ClientFrame): Uint8Array { + const message = toProtoClientFrame(frame); + const err = ClientFrameType.verify(message); + if (err) throw new Error(err); + return ClientFrameType.encode(ClientFrameType.create(message)).finish(); +} + +export function decodeServerFrame(bytes: Uint8Array): ServerFrame { + const decoded = ServerFrameType.toObject(ServerFrameType.decode(bytes), { defaults: true, bytes: Uint8Array }) as Record; + if (decoded.login_ack) return { loginAck: { policy: fromProtoPolicy((decoded.login_ack as Record).policy as Record) } }; + if (decoded.verdict) return { verdict: fromProtoVerdict(decoded.verdict as Record) }; + throw new Error("server frame did not contain login_ack or verdict"); +} + +export function pairedEventToProto(event: PairedEvent): Record { + const base: Record = { + event_id: event.eventId, + invocation_id: event.invocationId, + session_id: event.sessionId, + run_id: event.runId, + parent_run_id: event.parentRunId, + timestamp: event.timestamp, + pair_type: event.pairType === "llm" ? PairTypeProto.PAIR_TYPE_LLM : PairTypeProto.PAIR_TYPE_TOOL, + agent: agentToProto(event.agent), + parent: event.parent ? agentToProto(event.parent) : { agent_id: "", system_prompt: "", user_instruction: "" }, + }; + if (event.data.kind === "llm") { + base.llm = { + model: event.data.model, + messages: event.data.messages, + output: event.data.output, + tool_calls: event.data.toolCalls.map((call) => ({ name: call.name, args: JSON.stringify(call.args), id: call.id })), + usage: event.data.usage ? { + prompt_tokens: event.data.usage.promptTokens, + completion_tokens: event.data.usage.completionTokens, + total_tokens: event.data.usage.totalTokens, + } : undefined, + }; + } else { + base.tool = { tool_name: event.data.toolName, tool_call_id: event.data.toolCallId ?? "", input: event.data.input, output: event.data.output }; + } + if (event.metadata) base.metadata_json = new TextEncoder().encode(JSON.stringify(event.metadata)); + return base; +} + +function toProtoClientFrame(frame: ClientFrame): Record { + if ("login" in frame) { + return { login: { session_id: frame.login.sessionId, llm_stack: frame.login.llmStack, schema_version: frame.login.schemaVersion } }; + } + if ("pairedBatch" in frame) { + return { paired_batch: { events: frame.pairedBatch.events.map(pairedEventToProto) } }; + } + return { mcp_inventory: { servers: frame.mcpInventory.servers } }; +} + +function agentToProto(agent: { agentId: string; systemPrompt: string; userInstruction: string }): Record { + return { agent_id: agent.agentId, system_prompt: agent.systemPrompt, user_instruction: agent.userInstruction }; +} + +function fromProtoPolicy(policyRaw: Record | undefined): PolicySnapshot { + const policy = policyRaw ?? {}; + return { + mode: Number(policy.mode ?? 0) as Mode, + policyM0: Boolean(policy.policy_m0), + policyM2: Boolean(policy.policy_m2), + policyM3: Boolean(policy.policy_m3), + policyM4: Boolean(policy.policy_m4), + }; +} + +function fromProtoVerdict(raw: Record): Verdict { + const hitl = raw.hitl as Record | undefined; + return { + eventId: String(raw.event_id ?? ""), + sessionId: String(raw.session_id ?? ""), + madCode: String(raw.mad_code ?? ""), + policy: fromProtoPolicy(raw.policy as Record | undefined), + hitl: hitl ? { continueExecution: Boolean(hitl.continue_execution) } : null, + }; +} diff --git a/sdk/typescript/src/sessionPersistence.ts b/sdk/typescript/src/sessionPersistence.ts new file mode 100644 index 0000000..ec5e05c --- /dev/null +++ b/sdk/typescript/src/sessionPersistence.ts @@ -0,0 +1,54 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { resolve, sep } from "node:path"; + +const CONFIG_FILENAME = "config.json"; +const SESSION_KEY = "session_id"; + +export function cwdKey(cwd = process.cwd()): string { + return resolve(cwd).replaceAll("/", "-").replaceAll("\\", "-").replaceAll(":", "-"); +} + +export function configDir(cwd = process.cwd()): string { + return [homedir(), ".adrian", "projects", cwdKey(cwd)].join(sep); +} + +export function configPath(cwd = process.cwd()): string { + return [configDir(cwd), CONFIG_FILENAME].join(sep); +} + +export async function resolveSessionId(cwd = process.cwd()): Promise { + const existing = await readPersisted(cwd); + if (existing) return existing; + const next = randomUUID(); + await writePersisted(next, cwd); + return next; +} + +export async function envAwareResolveSessionId(explicit?: string | null, cwd = process.cwd()): Promise { + if (process.env.ADRIAN_SESSION_ID) return process.env.ADRIAN_SESSION_ID; + if (explicit) return explicit; + return resolveSessionId(cwd); +} + +async function readPersisted(cwd: string): Promise { + try { + const raw = await readFile(configPath(cwd), "utf8"); + const data = JSON.parse(raw) as Record; + const sessionId = data[SESSION_KEY]; + return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null; + } catch { + return null; + } +} + +async function writePersisted(sessionId: string, cwd: string): Promise { + try { + const path = configPath(cwd); + await mkdir(configDir(cwd), { recursive: true }); + await writeFile(path, JSON.stringify({ [SESSION_KEY]: sessionId }, null, 2) + "\n", "utf8"); + } catch { + // Persistence is best effort; init can still proceed with the generated id. + } +} diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/src/types.ts new file mode 100644 index 0000000..9a1ef23 --- /dev/null +++ b/sdk/typescript/src/types.ts @@ -0,0 +1,86 @@ +import type { PairedEvent } from "./format/types.js"; +import type { PolicySnapshot, HitlResponse } from "./proto/schema.js"; + +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; +export type MetadataValue = JsonPrimitive | string[]; +export type CallbackMetadata = Record; +export type ToolArgs = Record; + +export interface ChatMessage { + role: string; + content: string; +} + +export interface ToolCallRecord { + id: string; + name: string; + args: ToolArgs; +} + +export interface TokenUsage { + promptTokens: number; + completionTokens: number; + totalTokens: number; +} + +export interface ChatModelStartData { + model: string; + messages: ChatMessage[]; + metadata: CallbackMetadata | null; +} + +export interface LlmStartData { + model: string; + prompts: string[]; + metadata: CallbackMetadata | null; +} + +export interface LlmEndData { + output: string; + toolCalls: ToolCallRecord[]; + usage: TokenUsage | null; +} + +export interface ToolStartData { + toolName: string; + toolCallId: string | null; + input: string; + metadata: CallbackMetadata | null; +} + +export interface ToolEndData { + output: string; +} + +export type EventData = ChatModelStartData | LlmStartData | LlmEndData | ToolStartData | ToolEndData; + +export interface EventRecord { + eventType: string; + data: EventData; + runId: string; + parentRunId: string | null; +} + +export interface VerdictContext { + eventId: string; + sessionId: string; + eventType: string; + eventData: EventData; + runId: string; + parentRunId: string | null; + policy: PolicySnapshot; + madCode: string; + hitl: HitlResponse | null; +} + +export interface McpServer { + name: string; + transport: string; + endpoint: string; +} + +export interface EventHandler { + onPairedEvent(event: PairedEvent): Promise | void; + close(): Promise | void; +} diff --git a/sdk/typescript/src/ws.ts b/sdk/typescript/src/ws.ts new file mode 100644 index 0000000..3decbeb --- /dev/null +++ b/sdk/typescript/src/ws.ts @@ -0,0 +1,298 @@ +import WebSocket from "ws"; +import type { AdrianCallbackHandler } from "./handler.js"; +import type { PairedEvent } from "./format/types.js"; +import type { EventHandler, McpServer } from "./types.js"; +import { decodeServerFrame, encodeClientFrame, Mode, SCHEMA_VERSION, type PolicySnapshot, type Verdict } from "./proto/schema.js"; + +const INITIAL_BACKOFF_MS = 1000; +const MAX_BACKOFF_MS = 30_000; +const QUOTA_EXHAUSTED_CLOSE_CODE = 4003; +const QUOTA_RECONNECT_DELAY_MS = 60_000; +const MAX_RUN_ID_MAP = 1024; +const MAX_TOOL_CALL_MAP = 1024; +const MAX_VERDICT_CACHE = 1024; + +type VerdictWaiter = { resolve: (verdict: Verdict | null) => void; timer?: ReturnType }; +type LoginAckWaiter = { resolve: (acked: boolean) => void; timer?: ReturnType }; + +export class WebSocketClient implements EventHandler { + private url: string; + private sessionId: string; + private apiKey: string; + private onDisconnect?: ((reason: string) => void | Promise) | null; + private onReconnect?: (() => void | Promise) | null; + private onLoginAck?: (() => void | Promise) | null; + private ws: WebSocket | null = null; + private loggedIn = false; + private closing = false; + private replaying = false; + private hadConnection = false; + private provider = ""; + private model = ""; + private mode = Mode.MODE_UNSPECIFIED; + private policy: PolicySnapshot | null = null; + private replayBuffer: Uint8Array[] = []; + private replayLimit: number; + private droppedFrames = 0; + private runIdToEventId = new Map(); + private toolCallIdToEventId = new Map(); + private pendingVerdicts = new Map(); + private verdictCache = new Map(); + private loginAckWaiters = new Set(); + handler: AdrianCallbackHandler | null; + + constructor(options: { + url: string; + sessionId: string; + apiKey: string; + handler?: AdrianCallbackHandler | null; + onDisconnect?: ((reason: string) => void | Promise) | null; + onReconnect?: (() => void | Promise) | null; + onLoginAck?: (() => void | Promise) | null; + replayBufferFrames?: number; + }) { + this.url = options.url; + this.sessionId = options.sessionId; + this.apiKey = options.apiKey; + this.handler = options.handler ?? null; + this.onDisconnect = options.onDisconnect; + this.onReconnect = options.onReconnect; + this.onLoginAck = options.onLoginAck; + this.replayLimit = options.replayBufferFrames ?? 1000; + } + + scheduleConnect(): void { + void this.connectLoop(); + } + + async onPairedEvent(event: PairedEvent): Promise { + if (event.data.kind === "llm") { + if (!this.provider) this.provider = deriveProvider(event.data.model); + if (!this.model) this.model = event.data.model; + this.setLru(this.runIdToEventId, event.runId, event.eventId, MAX_RUN_ID_MAP); + for (const call of event.data.toolCalls) { + if (call.id) this.setLru(this.toolCallIdToEventId, call.id, event.eventId, MAX_TOOL_CALL_MAP); + } + } + await this.sendFrame(encodeClientFrame({ pairedBatch: { events: [event] } })); + } + + async sendMcpInventory(servers: McpServer[]): Promise { + if (servers.length === 0) return; + await this.sendFrame(encodeClientFrame({ mcpInventory: { servers } })); + } + + policyActive(): boolean { + return this.mode === Mode.MODE_BLOCK || this.mode === Mode.MODE_HITL; + } + + loginAcked(): boolean { + return this.loggedIn; + } + + async waitForPolicyReady(timeoutSeconds: number | null): Promise { + if (this.loggedIn) return true; + if (this.closing) return false; + return new Promise((resolve) => { + const waiter: LoginAckWaiter = { resolve }; + if (timeoutSeconds !== null) { + waiter.timer = setTimeout(() => { + this.loginAckWaiters.delete(waiter); + resolve(false); + }, timeoutSeconds * 1000); + } + this.loginAckWaiters.add(waiter); + }); + } + + blockTimeout(defaultSeconds: number): number | null { + if (this.mode === Mode.MODE_HITL) return null; + if (this.mode === Mode.MODE_BLOCK) return defaultSeconds; + return 0; + } + + async waitForToolCallVerdict(toolCallId: string, timeoutSeconds: number | null): Promise { + const eventId = this.toolCallIdToEventId.get(toolCallId); + if (!eventId) return null; + return this.waitForVerdict(eventId, timeoutSeconds); + } + + async waitForVerdict(eventId: string, timeoutSeconds: number | null): Promise { + const cached = this.verdictCache.get(eventId); + if (cached) return cached; + return new Promise((resolve) => { + const entry: VerdictWaiter = { resolve }; + if (timeoutSeconds !== null) { + entry.timer = setTimeout(() => this.resolveVerdict(eventId, null), timeoutSeconds * 1000); + } + const waiters = this.pendingVerdicts.get(eventId) ?? []; + waiters.push(entry); + this.pendingVerdicts.set(eventId, waiters); + }); + } + + async close(): Promise { + this.closing = true; + this.ws?.close(); + for (const [eventId] of this.pendingVerdicts) this.resolveVerdict(eventId, null); + this.resolveLoginAckWaiters(false); + } + + private async connectLoop(): Promise { + let backoff = INITIAL_BACKOFF_MS; + while (!this.closing) { + try { + await this.connectOnce(); + backoff = INITIAL_BACKOFF_MS; + await this.waitForClose(); + } catch { + // Connection errors are retried with backoff. + } + if (this.closing) return; + const delay = this.wsCloseCode() === QUOTA_EXHAUSTED_CLOSE_CODE ? QUOTA_RECONNECT_DELAY_MS : backoff; + await sleep(delay); + backoff = Math.min(backoff * 2, MAX_BACKOFF_MS); + } + } + + private async connectOnce(): Promise { + await new Promise((resolve, reject) => { + const ws = new WebSocket(this.url, { headers: { Authorization: `Bearer ${this.apiKey}` } }); + this.ws = ws; + ws.binaryType = "arraybuffer"; + ws.once("open", () => { + void this.sendRaw(encodeClientFrame({ login: { sessionId: this.sessionId, llmStack: { provider: this.provider, model: this.model }, schemaVersion: SCHEMA_VERSION } })); + resolve(); + }); + ws.on("message", (data) => void this.handleMessage(data)); + ws.once("error", reject); + ws.once("close", () => { + this.loggedIn = false; + this.replaying = false; + this.policy = null; + this.mode = Mode.MODE_UNSPECIFIED; + this.resolveLoginAckWaiters(false); + if (!this.closing) void this.onDisconnect?.("recv_loop_exit"); + }); + }); + } + + private waitForClose(): Promise { + const ws = this.ws; + if (!ws || ws.readyState === WebSocket.CLOSED) return Promise.resolve(); + return new Promise((resolve) => ws.once("close", () => resolve())); + } + + private wsCloseCode(): number | null { + return null; + } + + private async handleMessage(data: WebSocket.RawData): Promise { + const bytes = data instanceof Buffer ? data : Buffer.from(data as ArrayBuffer); + let frame: ReturnType; + try { + frame = decodeServerFrame(bytes); + } catch { + return; + } + if ("loginAck" in frame) { + this.policy = frame.loginAck.policy; + this.mode = frame.loginAck.policy.mode; + this.loggedIn = true; + this.resolveLoginAckWaiters(true); + if (this.hadConnection) await this.onReconnect?.(); + this.hadConnection = true; + await this.drainReplayBuffer(); + await this.onLoginAck?.(); + return; + } + const verdict = frame.verdict; + this.resolveVerdict(verdict.eventId, verdict); + await this.handler?.handleVerdict(verdict); + } + + private async sendFrame(frame: Uint8Array): Promise { + if (!this.loggedIn || this.replaying || !this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.bufferFrame(frame); + return; + } + try { + await this.sendRaw(frame); + } catch { + this.bufferFrame(frame); + await this.onDisconnect?.("send_failure"); + } + } + + private async sendRaw(frame: Uint8Array): Promise { + const ws = this.ws; + if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("websocket is not open"); + await new Promise((resolve, reject) => ws.send(frame, { binary: true }, (err) => err ? reject(err) : resolve())); + } + + private async drainReplayBuffer(): Promise { + this.replaying = true; + try { + while (this.replayBuffer.length > 0 && this.ws?.readyState === WebSocket.OPEN) { + const frame = this.replayBuffer.shift(); + if (frame) await this.sendRaw(frame); + } + } finally { + this.replaying = false; + } + this.droppedFrames = 0; + } + + private bufferFrame(frame: Uint8Array): void { + if (this.replayLimit <= 0) return; + if (this.replayBuffer.length >= this.replayLimit) { + this.replayBuffer.shift(); + this.droppedFrames += 1; + } + this.replayBuffer.push(frame); + } + + private resolveVerdict(eventId: string, verdict: Verdict | null): void { + if (verdict) this.setLru(this.verdictCache, eventId, verdict, MAX_VERDICT_CACHE); + const waiters = this.pendingVerdicts.get(eventId); + if (!waiters) return; + this.pendingVerdicts.delete(eventId); + for (const entry of waiters) { + if (entry.timer) clearTimeout(entry.timer); + entry.resolve(verdict); + } + } + + private resolveLoginAckWaiters(acked: boolean): void { + for (const waiter of this.loginAckWaiters) { + if (waiter.timer) clearTimeout(waiter.timer); + waiter.resolve(acked); + } + this.loginAckWaiters.clear(); + } + + private setLru(map: Map, key: string, value: V, limit: number): void { + if (map.has(key)) map.delete(key); + map.set(key, value); + while (map.size > limit) map.delete(map.keys().next().value as string); + } +} + +export function shouldHalt(verdict: Verdict): boolean { + if (verdict.hitl) return !verdict.hitl.continueExecution; + const prefix = verdict.madCode.slice(0, 2); + if (prefix === "M0") return verdict.policy.policyM0; + if (prefix === "M2") return verdict.policy.policyM2; + if (prefix === "M3") return verdict.policy.policyM3; + if (prefix === "M4") return verdict.policy.policyM4; + return false; +} + +function deriveProvider(modelClassName: string): string { + const key = modelClassName.toLowerCase(); + return ({ chatanthropic: "anthropic", chatopenai: "openai", chatgooglegenai: "google", chatcohere: "cohere", chatmistralai: "mistral" } as Record)[key] ?? key; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/sdk/typescript/tests/jsonl.test.ts b/sdk/typescript/tests/jsonl.test.ts new file mode 100644 index 0000000..9c10c51 --- /dev/null +++ b/sdk/typescript/tests/jsonl.test.ts @@ -0,0 +1,28 @@ +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { JSONLHandler } from "../src/handlers/jsonl.js"; +import type { PairedEvent } from "../src/format/types.js"; + +it("writes paired events as jsonl", async () => { + const dir = await mkdtemp(join(tmpdir(), "adrian-ts-")); + const path = join(dir, "events.jsonl"); + const handler = new JSONLHandler(path); + const event: PairedEvent = { + eventId: "evt", + invocationId: "inv", + sessionId: "sess", + runId: "run", + parentRunId: "", + timestamp: new Date(0).toISOString(), + pairType: "tool", + agent: { agentId: "agent", systemPrompt: "", userInstruction: "" }, + parent: null, + data: { kind: "tool", toolName: "search", toolCallId: null, input: "x", output: "y" }, + metadata: null, + }; + await handler.onPairedEvent(event); + await handler.close(); + expect(await readFile(path, "utf8")).toContain('"eventId":"evt"'); +}); diff --git a/sdk/typescript/tests/langchain.test.ts b/sdk/typescript/tests/langchain.test.ts new file mode 100644 index 0000000..ed609c9 --- /dev/null +++ b/sdk/typescript/tests/langchain.test.ts @@ -0,0 +1,79 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { setConfig } from "../src/config.js"; +import { blockedToolNodeResponse } from "../src/instrumentation/langchain.js"; +import { Mode, type Verdict } from "../src/proto/schema.js"; +import type { WebSocketClient } from "../src/ws.js"; + +function config(): Parameters[0] { + return { + apiKey: null, + logFile: "events.jsonl", + logLevel: null, + sessionId: "sess", + wsUrl: null, + blockTimeout: 5, + onEvent: null, + onVerdict: null, + onBlock: null, + onAudit: null, + onDisconnect: null, + onReconnect: null, + onMcpServer: null, + replayBufferFrames: 1000, + }; +} + +function verdict(eventId: string, halt: boolean): Verdict { + return { + eventId, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, + hitl: null, + }; +} + +describe("ToolNode policy gating", () => { + afterEach(() => setConfig(null)); + + it("waits for policy readiness and evaluates every tool call verdict", async () => { + setConfig(config()); + const waitedFor: string[] = []; + let waitedForPolicy = false; + const ws = { + waitForPolicyReady: async () => { + waitedForPolicy = true; + return true; + }, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => { + waitedFor.push(toolCallId); + return verdict(`event-${toolCallId}`, toolCallId === "call-2"); + }, + } as unknown as WebSocketClient; + + const response = await blockedToolNodeResponse({ + messages: [{ tool_calls: [{ id: "call-1", name: "search" }, { id: "call-2", name: "write" }] }], + }, ws); + + expect(waitedForPolicy).toBe(true); + expect(waitedFor).toEqual(["call-1", "call-2"]); + expect(response?.messages).toHaveLength(2); + expect(response?.messages[0]?.content).toContain("BLOCKED"); + }); + + it("does not block when all correlated verdicts allow execution", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => verdict(`event-${toolCallId}`, false), + } as unknown as WebSocketClient; + + await expect(blockedToolNodeResponse({ + messages: [{ toolCalls: [{ id: "call-1", name: "search" }, { id: "call-2", name: "write" }] }], + }, ws)).resolves.toBeNull(); + }); +}); diff --git a/sdk/typescript/tests/pairing.test.ts b/sdk/typescript/tests/pairing.test.ts new file mode 100644 index 0000000..9e50a06 --- /dev/null +++ b/sdk/typescript/tests/pairing.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { EventPairBuffer } from "../src/pairing.js"; + +it("pairs chat model start and llm end events", () => { + const buffer = new EventPairBuffer(); + buffer.onStart({ + eventType: "chat_model_start", + data: { model: "ChatOpenAI", messages: [{ role: "system", content: "sys" }, { role: "human", content: "hi" }], metadata: null }, + runId: "run-1", + agentId: "agent", + parent: null, + metadata: null, + }); + const pair = buffer.onEnd({ eventType: "llm_end", data: { output: "hello", toolCalls: [], usage: null }, runId: "run-1", invocationId: "inv", sessionId: "sess" }); + expect(pair?.pairType).toBe("llm"); + expect(pair?.agent.systemPrompt).toBe("sys"); + expect(pair?.agent.userInstruction).toBe("hi"); +}); diff --git a/sdk/typescript/tests/pii.test.ts b/sdk/typescript/tests/pii.test.ts new file mode 100644 index 0000000..ac6e45f --- /dev/null +++ b/sdk/typescript/tests/pii.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { redactText, RedactionStrategy } from "../src/pii/index.js"; + +it("redacts email addresses", () => { + const result = redactText("email me at test@example.com"); + expect(result.text).toContain("[EMAIL_REDACTED]"); +}); + +it("can mask phone numbers", () => { + const result = redactText("call 415-555-1234", { strategy: RedactionStrategy.MASK }); + expect(result.text).toContain("***-***-1234"); +}); diff --git a/sdk/typescript/tests/proto.test.ts b/sdk/typescript/tests/proto.test.ts new file mode 100644 index 0000000..9776de1 --- /dev/null +++ b/sdk/typescript/tests/proto.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { encodeClientFrame, SCHEMA_VERSION } from "../src/proto/schema.js"; +import type { PairedEvent } from "../src/format/types.js"; + +it("encodes login and paired event frames", () => { + const login = encodeClientFrame({ login: { sessionId: "sess", llmStack: { provider: "openai", model: "gpt" }, schemaVersion: SCHEMA_VERSION } }); + expect(login.length).toBeGreaterThan(0); + + const event: PairedEvent = { + eventId: "evt", + invocationId: "inv", + sessionId: "sess", + runId: "run", + parentRunId: "", + timestamp: new Date(0).toISOString(), + pairType: "llm", + agent: { agentId: "agent", systemPrompt: "", userInstruction: "" }, + parent: null, + data: { kind: "llm", model: "ChatOpenAI", messages: [], output: "ok", toolCalls: [], usage: null }, + metadata: null, + }; + const batch = encodeClientFrame({ pairedBatch: { events: [event] } }); + expect(batch.length).toBeGreaterThan(0); +}); diff --git a/sdk/typescript/tests/session.test.ts b/sdk/typescript/tests/session.test.ts new file mode 100644 index 0000000..2ba7bf5 --- /dev/null +++ b/sdk/typescript/tests/session.test.ts @@ -0,0 +1,6 @@ +import { describe, expect, it } from "vitest"; +import { cwdKey } from "../src/sessionPersistence.js"; + +it("encodes cwd into a flat key", () => { + expect(cwdKey("/tmp/adrian")).toContain("-tmp-adrian"); +}); diff --git a/sdk/typescript/tests/ws.test.ts b/sdk/typescript/tests/ws.test.ts new file mode 100644 index 0000000..fd87b63 --- /dev/null +++ b/sdk/typescript/tests/ws.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import type { PairedEvent } from "../src/format/types.js"; +import { Mode, type Verdict } from "../src/proto/schema.js"; +import { WebSocketClient } from "../src/ws.js"; + +function client(): WebSocketClient { + return new WebSocketClient({ url: "ws://localhost:0", sessionId: "sess", apiKey: "key", replayBufferFrames: 10 }); +} + +function verdict(eventId: string): Verdict { + return { + eventId, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: true, policyM4: false }, + hitl: null, + }; +} + +function llmEvent(eventId: string): PairedEvent { + return { + eventId, + invocationId: "inv", + sessionId: "sess", + runId: "run", + parentRunId: "", + timestamp: new Date(0).toISOString(), + pairType: "llm", + agent: { agentId: "agent", systemPrompt: "", userInstruction: "" }, + parent: null, + data: { kind: "llm", model: "ChatOpenAI", messages: [], output: "", toolCalls: [{ id: "tool-1", name: "search", args: {} }], usage: null }, + metadata: null, + }; +} + +describe("WebSocketClient verdict waiting", () => { + it("replays a verdict that arrives before a waiter is registered", async () => { + const ws = client(); + const early = verdict("evt-1"); + (ws as unknown as { resolveVerdict: (eventId: string, verdict: Verdict) => void }).resolveVerdict("evt-1", early); + + await expect(ws.waitForVerdict("evt-1", 1)).resolves.toBe(early); + }); + + it("resolves every waiter registered for the same event", async () => { + const ws = client(); + const expected = verdict("evt-2"); + const first = ws.waitForVerdict("evt-2", 1); + const second = ws.waitForVerdict("evt-2", 1); + + (ws as unknown as { resolveVerdict: (eventId: string, verdict: Verdict) => void }).resolveVerdict("evt-2", expected); + + await expect(Promise.all([first, second])).resolves.toEqual([expected, expected]); + }); + + it("uses cached event verdicts for correlated tool calls", async () => { + const ws = client(); + await ws.onPairedEvent(llmEvent("evt-3")); + const expected = verdict("evt-3"); + (ws as unknown as { resolveVerdict: (eventId: string, verdict: Verdict) => void }).resolveVerdict("evt-3", expected); + + await expect(ws.waitForToolCallVerdict("tool-1", 1)).resolves.toBe(expected); + }); +}); diff --git a/sdk/typescript/tsconfig.build.json b/sdk/typescript/tsconfig.build.json new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/sdk/typescript/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["tests/**/*.ts"] +} diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json new file mode 100644 index 0000000..a930282 --- /dev/null +++ b/sdk/typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} From 7d36b3ffd29158df79bc208a9f2c28d7dcea8519 Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Sun, 24 May 2026 14:10:02 +0530 Subject: [PATCH 02/10] supprt openai, vercel --- sdk/typescript/README.md | 78 +++++- sdk/typescript/package-lock.json | 10 +- sdk/typescript/package.json | 10 +- sdk/typescript/src/format/types.ts | 4 +- sdk/typescript/src/handler.ts | 48 +++- sdk/typescript/src/index.ts | 2 + sdk/typescript/src/instrumentation/common.ts | 155 ++++++++++++ sdk/typescript/src/instrumentation/openai.ts | 228 +++++++++++++++++ sdk/typescript/src/instrumentation/vercel.ts | 160 ++++++++++++ sdk/typescript/src/pairing.ts | 2 + sdk/typescript/src/types.ts | 8 + sdk/typescript/tests/openai.test.ts | 251 +++++++++++++++++++ sdk/typescript/tests/vercel.test.ts | 144 +++++++++++ 13 files changed, 1095 insertions(+), 5 deletions(-) create mode 100644 sdk/typescript/src/instrumentation/common.ts create mode 100644 sdk/typescript/src/instrumentation/openai.ts create mode 100644 sdk/typescript/src/instrumentation/vercel.ts create mode 100644 sdk/typescript/tests/openai.test.ts create mode 100644 sdk/typescript/tests/vercel.test.ts diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 55193b8..ba4379f 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -1,6 +1,6 @@ # @secureagentics/adrian -TypeScript SDK for Adrian multi-agent event capture in Node.js LangChain.js / LangGraph.js applications. +TypeScript SDK for Adrian multi-agent event capture in Node.js LangChain.js, LangGraph.js, OpenAI SDK, and Vercel AI SDK applications. ```ts import { init } from "@secureagentics/adrian"; @@ -10,6 +10,82 @@ await init({ apiKey: process.env.ADRIAN_API_KEY }); The SDK mirrors the Python package: it pairs LLM/tool callbacks into `PairedEvent` objects, redacts PII, writes JSONL locally, streams protobuf frames to the Adrian WebSocket endpoint, tracks MCP servers, and applies BLOCK/HITL tool gating when LangGraph ToolNode instrumentation is available. +## OpenAI SDK + +```ts +import OpenAI from "openai"; +import { init, instrumentOpenAI } from "@secureagentics/adrian"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const openai = instrumentOpenAI(new OpenAI()); +await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Hello" }], +}); +``` + +`instrumentOpenAI` captures `chat.completions.create` and `responses.create` calls. Streaming chat completions are captured when the returned async iterable is consumed. + +OpenAI returns tool call requests, but your app executes the tools. Wrap that local execution with `captureOpenAIToolCall` to emit Adrian tool events: + +```ts +import { captureOpenAIToolCall } from "@secureagentics/adrian"; + +for (const toolCall of assistantMessage.tool_calls ?? []) { + const toolResult = await captureOpenAIToolCall(toolCall, () => + runTool(toolCall.function.name, toolCall.function.arguments), + ); + + messages.push({ + role: "tool", + tool_call_id: toolCall.id, + content: toolResult, + }); +} +``` + +## Vercel AI SDK + +```ts +import * as ai from "ai"; +import { init, instrumentVercelAI } from "@secureagentics/adrian"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const adrianAI = instrumentVercelAI(ai); +await adrianAI.generateText({ + model, + prompt: "Hello", +}); +``` + +`instrumentVercelAI` wraps `generateText`, `streamText`, `generateObject`, and `streamObject`. Stream results are emitted after the Vercel result promises such as `text`, `toolCalls`, and `usage` settle. + +If you pass Vercel AI SDK tools with `execute` functions, wrap the tools object before passing it to `generateText`: + +```ts +import { instrumentVercelAITools } from "@secureagentics/adrian"; + +const result = await adrianAI.generateText({ + model, + prompt: "Use the weather tool.", + tools: instrumentVercelAITools(tools), +}); +``` + +If your app executes Vercel AI SDK tool calls manually, wrap that execution with `captureVercelAIToolCall` or the shorter `captureAITool` alias: + +```ts +import { captureAITool } from "@secureagentics/adrian"; + +for (const toolCall of result.toolCalls ?? []) { + const toolResult = await captureAITool(toolCall, () => + runTool(toolCall.toolName, toolCall.args), + ); +} +``` + ## Environment - `ADRIAN_API_KEY` diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index c321d7c..ff0c625 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -21,7 +21,9 @@ }, "peerDependencies": { "@langchain/core": ">=0.3.0", - "@langchain/langgraph": ">=0.2.0" + "@langchain/langgraph": ">=0.2.0", + "ai": ">=4.0.0", + "openai": ">=4.0.0" }, "peerDependenciesMeta": { "@langchain/core": { @@ -29,6 +31,12 @@ }, "@langchain/langgraph": { "optional": true + }, + "ai": { + "optional": true + }, + "openai": { + "optional": true } } }, diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 61cc108..4995175 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -37,15 +37,23 @@ "prompt-injection" ], "peerDependencies": { + "ai": ">=4.0.0", "@langchain/core": ">=0.3.0", - "@langchain/langgraph": ">=0.2.0" + "@langchain/langgraph": ">=0.2.0", + "openai": ">=4.0.0" }, "peerDependenciesMeta": { + "ai": { + "optional": true + }, "@langchain/core": { "optional": true }, "@langchain/langgraph": { "optional": true + }, + "openai": { + "optional": true } }, "dependencies": { diff --git a/sdk/typescript/src/format/types.ts b/sdk/typescript/src/format/types.ts index 5018f99..cc262cf 100644 --- a/sdk/typescript/src/format/types.ts +++ b/sdk/typescript/src/format/types.ts @@ -1,4 +1,4 @@ -import type { CallbackMetadata, ChatMessage, TokenUsage, ToolCallRecord } from "../types.js"; +import type { CallbackMetadata, ChatMessage, ErrorData, TokenUsage, ToolCallRecord } from "../types.js"; export interface AgentContext { agentId: string; @@ -19,6 +19,7 @@ export interface LlmPairData { output: string; toolCalls: ToolCallRecord[]; usage: TokenUsage | null; + error?: ErrorData; } export interface ToolPairData { @@ -27,6 +28,7 @@ export interface ToolPairData { toolCallId: string | null; input: string; output: string; + error?: ErrorData; } export type PairType = "llm" | "tool"; diff --git a/sdk/typescript/src/handler.ts b/sdk/typescript/src/handler.ts index 9681787..c31fb4a 100644 --- a/sdk/typescript/src/handler.ts +++ b/sdk/typescript/src/handler.ts @@ -5,7 +5,7 @@ import type { PairedEvent } from "./format/types.js"; import { HookRegistry } from "./hooks.js"; import { deriveAgentId } from "./identity.js"; import { EventPairBuffer } from "./pairing.js"; -import type { CallbackMetadata, ChatMessage, EventData, EventRecord, LlmEndData, ToolCallRecord, ToolEndData, VerdictContext } from "./types.js"; +import type { CallbackMetadata, ChatMessage, ErrorData, EventData, EventRecord, LlmEndData, ToolCallRecord, ToolEndData, VerdictContext } from "./types.js"; import type { Verdict } from "./proto/schema.js"; export interface AdrianCallbackHandlerOptions { @@ -82,6 +82,18 @@ export class AdrianCallbackHandler { } } + async handleLLMError(error: unknown, runId: string): Promise { + const errorData = normalizeError(error); + const pair = this.pairBuffer.onEnd({ + eventType: "llm_end", + data: { output: errorOutput(errorData), toolCalls: [], usage: null, error: errorData }, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) await this.emitPair(pair); + } + async handleToolStart(tool: Record, input: string, runId: string, parentRunId?: string, extraParams?: Record): Promise { const metadata = extractMetadata(extraParams); let agentId = this.currentAgentId; @@ -118,6 +130,18 @@ export class AdrianCallbackHandler { if (pair) await this.emitPair(pair); } + async handleToolError(error: unknown, runId: string): Promise { + const errorData = normalizeError(error); + const pair = this.pairBuffer.onEnd({ + eventType: "tool_end", + data: { output: errorOutput(errorData), error: errorData }, + runId: String(runId), + invocationId: this.resolveInvocationId(), + sessionId: this.resolveSessionId(), + }); + if (pair) await this.emitPair(pair); + } + async handleVerdict(verdict: Verdict): Promise { const record = this.eventMap.get(verdict.eventId); this.eventMap.delete(verdict.eventId); @@ -184,6 +208,7 @@ function messageToChatMessage(message: unknown): ChatMessage { } function extractLlmEndData(output: unknown): LlmEndData { + if (isLlmEndData(output)) return output; const generations = (output as { generations?: unknown[][] })?.generations ?? []; const first = generations[0]?.[0] as Record | undefined; const text = typeof first?.text === "string" ? first.text : ""; @@ -207,6 +232,27 @@ function extractLlmEndData(output: unknown): LlmEndData { }; } +function isLlmEndData(output: unknown): output is LlmEndData { + if (!output || typeof output !== "object") return false; + const obj = output as Record; + return typeof obj.output === "string" && Array.isArray(obj.toolCalls) && ("usage" in obj); +} + +function normalizeError(error: unknown): ErrorData { + if (error instanceof Error) { + return { + name: error.name || "Error", + message: error.message, + ...(error.stack ? { stack: error.stack } : {}), + }; + } + return { name: "Error", message: stringifyOutput(error) }; +} + +function errorOutput(error: ErrorData): string { + return `[ERROR] ${error.name}: ${error.message}`; +} + function stringifyOutput(output: unknown): string { if (typeof output === "string") return output; try { diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 0c08e60..4d8ed74 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -112,3 +112,5 @@ export * from "./types.js"; export * from "./format/types.js"; export * from "./pii/index.js"; export * from "./proto/schema.js"; +export * from "./instrumentation/openai.js"; +export * from "./instrumentation/vercel.js"; diff --git a/sdk/typescript/src/instrumentation/common.ts b/sdk/typescript/src/instrumentation/common.ts new file mode 100644 index 0000000..9963e50 --- /dev/null +++ b/sdk/typescript/src/instrumentation/common.ts @@ -0,0 +1,155 @@ +import { randomUUID } from "node:crypto"; +import type { AdrianCallbackHandler } from "../handler.js"; +import { runWithInvocationId } from "../context.js"; +import type { CallbackMetadata, ChatMessage, LlmEndData, TokenUsage, ToolArgs, ToolCallRecord } from "../types.js"; + +export interface LlmCaptureInput { + model: string; + messages: ChatMessage[]; + metadata?: CallbackMetadata | null; + parentRunId?: string; +} + +export async function captureLlmCall( + getHandler: () => AdrianCallbackHandler | null, + input: LlmCaptureInput, + execute: () => Promise, + extractOutput: (result: T) => LlmEndData | Promise, +): Promise { + const handler = getHandler(); + if (!handler) return execute(); + + const runId = randomUUID(); + return runWithInvocationId(randomUUID(), async () => { + await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata ?? null }); + try { + const result = await execute(); + await handler.handleLLMEnd(await extractOutput(result), runId); + return result; + } catch (error) { + await handler.handleLLMError(error, runId); + throw error; + } + }); +} + +export function captureLlmAsyncIterable( + getHandler: () => AdrianCallbackHandler | null, + input: LlmCaptureInput, + iterable: AsyncIterable, + aggregate: (chunk: T) => void, + extractOutput: () => LlmEndData, +): AsyncIterable { + const handler = getHandler(); + if (!handler) return iterable; + + const runId = randomUUID(); + const invocationId = randomUUID(); + + async function* wrapped(): AsyncGenerator { + await handler?.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata ?? null }); + yield* runWithInvocationId(invocationId, async function* () { + let emitted = false; + let failed = false; + try { + for await (const chunk of iterable) { + aggregate(chunk); + yield chunk; + } + emitted = true; + await handler?.handleLLMEnd(await extractOutput(), runId); + } catch (error) { + failed = true; + await handler?.handleLLMError(error, runId); + throw error; + } finally { + if (!emitted && !failed) await handler?.handleLLMEnd(await extractOutput(), runId); + } + }); + } + + return wrapped(); +} + +export function normalizeMessages(input: unknown): ChatMessage[] { + if (typeof input === "string") return [{ role: "user", content: input }]; + if (!Array.isArray(input)) return []; + return input.map((message) => { + const obj = message && typeof message === "object" ? message as Record : {}; + return { + role: String(obj.role ?? "unknown"), + content: stringifyContent(obj.content ?? obj.text ?? ""), + }; + }); +} + +export function messagesFromPromptLike(args: Record): ChatMessage[] { + const messages = normalizeMessages(args.messages); + if (messages.length > 0) return prependSystem(args.system, messages); + if (typeof args.prompt === "string") return prependSystem(args.system, [{ role: "user", content: args.prompt }]); + if (typeof args.input === "string") return prependSystem(args.system, [{ role: "user", content: args.input }]); + return prependSystem(args.system, []); +} + +export function stringifyContent(value: unknown): string { + if (typeof value === "string") return value; + if (Array.isArray(value)) { + return value.map((part) => { + if (typeof part === "string") return part; + if (part && typeof part === "object") { + const obj = part as Record; + if (typeof obj.text === "string") return obj.text; + if (typeof obj.content === "string") return obj.content; + } + return stringifyJson(part); + }).join(""); + } + return stringifyJson(value); +} + +export function normalizeUsage(usage: unknown, promptKeys = ["promptTokens", "prompt_tokens", "input_tokens"], completionKeys = ["completionTokens", "completion_tokens", "output_tokens"]): TokenUsage | null { + if (!usage || typeof usage !== "object") return null; + const obj = usage as Record; + const promptTokens = numberFromKeys(obj, promptKeys); + const completionTokens = numberFromKeys(obj, completionKeys); + const totalTokens = numberFromKeys(obj, ["totalTokens", "total_tokens"]) ?? ((promptTokens ?? 0) + (completionTokens ?? 0)); + if (promptTokens === null && completionTokens === null && totalTokens === 0) return null; + return { promptTokens: promptTokens ?? 0, completionTokens: completionTokens ?? 0, totalTokens }; +} + +export function parseToolArgs(value: unknown): ToolArgs { + if (!value) return {}; + if (typeof value === "object" && !Array.isArray(value)) return value as ToolArgs; + if (typeof value !== "string") return {}; + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as ToolArgs : {}; + } catch { + return {}; + } +} + +export function emptyLlmEnd(output = "", toolCalls: ToolCallRecord[] = [], usage: TokenUsage | null = null): LlmEndData { + return { output, toolCalls, usage }; +} + +function prependSystem(system: unknown, messages: ChatMessage[]): ChatMessage[] { + return typeof system === "string" && system.length > 0 ? [{ role: "system", content: system }, ...messages] : messages; +} + +function numberFromKeys(obj: Record, keys: string[]): number | null { + for (const key of keys) { + const value = obj[key]; + if (typeof value === "number" && Number.isFinite(value)) return value; + } + return null; +} + +function stringifyJson(value: unknown): string { + if (value === null || value === undefined) return ""; + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} diff --git a/sdk/typescript/src/instrumentation/openai.ts b/sdk/typescript/src/instrumentation/openai.ts new file mode 100644 index 0000000..89258c5 --- /dev/null +++ b/sdk/typescript/src/instrumentation/openai.ts @@ -0,0 +1,228 @@ +import { randomUUID } from "node:crypto"; +import { runWithInvocationId } from "../context.js"; +import { getHandler } from "../index.js"; +import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "../types.js"; +import { + captureLlmAsyncIterable, + captureLlmCall, + emptyLlmEnd, + messagesFromPromptLike, + normalizeMessages, + normalizeUsage, + parseToolArgs, + stringifyContent, +} from "./common.js"; + +export interface OpenAIInstrumentationOptions { + metadata?: CallbackMetadata | null; +} + +export interface OpenAIToolCallLike { + id: string; + type?: string; + function?: { + name?: string; + arguments?: string; + }; + name?: string; + arguments?: string; +} + +export interface OpenAIToolCallCaptureOptions { + metadata?: CallbackMetadata | null; + parentRunId?: string; +} + +export function instrumentOpenAI(client: T, options: OpenAIInstrumentationOptions = {}): T { + return new Proxy(client, { + get(target, prop, receiver) { + if (prop === "chat") return instrumentChat(Reflect.get(target, prop, receiver), options); + if (prop === "responses") return instrumentResponses(Reflect.get(target, prop, receiver), options); + return Reflect.get(target, prop, receiver); + }, + }); +} + +export const withAdrianOpenAI = instrumentOpenAI; + +export async function captureOpenAIToolCall( + toolCall: OpenAIToolCallLike, + execute: () => T | Promise, + options: OpenAIToolCallCaptureOptions = {}, +): Promise { + const handler = getHandler(); + if (!handler) return execute(); + + const runId = randomUUID(); + const toolName = String(toolCall.function?.name ?? toolCall.name ?? "unknown"); + const toolCallId = String(toolCall.id ?? ""); + const input = String(toolCall.function?.arguments ?? toolCall.arguments ?? ""); + const metadata = integrationMetadata(options.metadata, "openai.tool_call"); + + return runWithInvocationId(randomUUID(), async () => { + await handler.handleToolStart({ name: toolName }, input, runId, options.parentRunId, { metadata, tool_call_id: toolCallId }); + try { + const result = await execute(); + await handler.handleToolEnd(result, runId); + return result; + } catch (error) { + await handler.handleToolError(error, runId); + throw error; + } + }); +} + +function instrumentChat(chat: unknown, options: OpenAIInstrumentationOptions): unknown { + if (!chat || typeof chat !== "object") return chat; + return new Proxy(chat as Record, { + get(target, prop, receiver) { + if (prop === "completions") return instrumentChatCompletions(Reflect.get(target, prop, receiver), options); + return Reflect.get(target, prop, receiver); + }, + }); +} + +function instrumentChatCompletions(completions: unknown, options: OpenAIInstrumentationOptions): unknown { + if (!completions || typeof completions !== "object") return completions; + return new Proxy(completions as Record, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop !== "create" || typeof value !== "function") return value; + return async function adrianOpenAIChatCreate(this: unknown, body: Record = {}, ...rest: unknown[]) { + const model = String(body.model ?? "openai"); + const metadata = integrationMetadata(options.metadata, "openai.chat.completions"); + const messages = normalizeMessages(body.messages); + if (body.stream === true) { + const result = await Promise.resolve(value.call(target, body, ...rest)); + if (isAsyncIterable(result)) return captureChatCompletionStream(model, messages, metadata, result); + return result; + } + return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractChatCompletion); + }; + }, + }); +} + +function instrumentResponses(responses: unknown, options: OpenAIInstrumentationOptions): unknown { + if (!responses || typeof responses !== "object") return responses; + return new Proxy(responses as Record, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop !== "create" || typeof value !== "function") return value; + return async function adrianOpenAIResponsesCreate(this: unknown, body: Record = {}, ...rest: unknown[]) { + const model = String(body.model ?? "openai"); + const metadata = integrationMetadata(options.metadata, "openai.responses"); + const messages = messagesFromPromptLike({ input: body.input }); + if (body.stream === true) { + const result = await Promise.resolve(value.call(target, body, ...rest)); + if (isAsyncIterable(result)) return captureResponseStream(model, messages, metadata, result); + return result; + } + return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractResponse); + }; + }, + }); +} + +function captureChatCompletionStream(model: string, messages: ReturnType, metadata: CallbackMetadata | null, stream: AsyncIterable): AsyncIterable { + let output = ""; + let usage: LlmEndData["usage"] = null; + const toolCallParts = new Map(); + return captureLlmAsyncIterable(getHandler, { model, messages, metadata }, stream, (chunk) => { + const obj = chunk && typeof chunk === "object" ? chunk as Record : {}; + usage = normalizeUsage(obj.usage) ?? usage; + const choices = Array.isArray(obj.choices) ? obj.choices : []; + for (const choice of choices) { + const delta = (choice as Record).delta as Record | undefined; + output += stringifyContent(delta?.content); + const calls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : Array.isArray(delta?.toolCalls) ? delta.toolCalls : []; + for (const rawCall of calls) { + const call = rawCall as Record; + const index = typeof call.index === "number" ? call.index : toolCallParts.size; + const fn = call.function as Record | undefined; + const current = toolCallParts.get(index) ?? { id: "", name: "", args: "" }; + toolCallParts.set(index, { + id: typeof call.id === "string" ? call.id : current.id, + name: typeof fn?.name === "string" ? fn.name : current.name, + args: current.args + (typeof fn?.arguments === "string" ? fn.arguments : ""), + }); + } + } + }, () => emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage)); +} + +function captureResponseStream(model: string, messages: ReturnType, metadata: CallbackMetadata | null, stream: AsyncIterable): AsyncIterable { + let output = ""; + let usage: LlmEndData["usage"] = null; + const toolCallParts = new Map(); + return captureLlmAsyncIterable(getHandler, { model, messages, metadata }, stream, (chunk) => { + const obj = chunk && typeof chunk === "object" ? chunk as Record : {}; + usage = normalizeUsage(obj.usage) ?? usage; + if (obj.type === "response.output_text.delta" && typeof obj.delta === "string") output += obj.delta; + if (typeof obj.output_text === "string") output += obj.output_text; + collectResponseStreamToolCall(obj, toolCallParts); + }, () => emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage)); +} + +function extractChatCompletion(result: unknown): LlmEndData { + if (isAsyncIterable(result)) return emptyLlmEnd(); + const obj = result && typeof result === "object" ? result as Record : {}; + const choices = Array.isArray(obj.choices) ? obj.choices : []; + const first = choices[0] && typeof choices[0] === "object" ? choices[0] as Record : {}; + const message = first.message && typeof first.message === "object" ? first.message as Record : {}; + const toolCalls = normalizeOpenAIToolCalls(message.tool_calls ?? message.toolCalls); + return emptyLlmEnd(stringifyContent(message.content), toolCalls, normalizeUsage(obj.usage)); +} + +function extractResponse(result: unknown): LlmEndData { + if (isAsyncIterable(result)) return emptyLlmEnd(); + const obj = result && typeof result === "object" ? result as Record : {}; + return emptyLlmEnd(typeof obj.output_text === "string" ? obj.output_text : stringifyContent(obj.output), normalizeResponseToolCalls(obj.output), normalizeUsage(obj.usage)); +} + +function normalizeOpenAIToolCalls(raw: unknown): ToolCallRecord[] { + if (!Array.isArray(raw)) return []; + return raw.map((call) => { + const obj = call && typeof call === "object" ? call as Record : {}; + const fn = obj.function && typeof obj.function === "object" ? obj.function as Record : {}; + return { id: String(obj.id ?? ""), name: String(fn.name ?? obj.name ?? ""), args: parseToolArgs(fn.arguments ?? obj.arguments ?? obj.args) }; + }); +} + +function normalizeResponseToolCalls(raw: unknown): ToolCallRecord[] { + if (!Array.isArray(raw)) return []; + return raw.flatMap((item) => { + const obj = item && typeof item === "object" ? item as Record : {}; + if (obj.type === "message" && Array.isArray(obj.content)) { + return obj.content.flatMap((part) => normalizeResponseToolCalls([part])); + } + if (obj.type !== "function_call" && obj.type !== "tool_call") return []; + return [{ id: String(obj.call_id ?? obj.id ?? ""), name: String(obj.name ?? ""), args: parseToolArgs(obj.arguments) }]; + }); +} + +function collectResponseStreamToolCall(obj: Record, toolCallParts: Map): void { + const item = obj.item && typeof obj.item === "object" ? obj.item as Record : null; + if (item && (item.type === "function_call" || item.type === "tool_call")) { + const key = String(item.id ?? item.call_id ?? obj.output_index ?? toolCallParts.size); + const current = toolCallParts.get(key) ?? { id: "", name: "", args: "" }; + toolCallParts.set(key, { + id: String(item.call_id ?? item.id ?? current.id), + name: String(item.name ?? current.name), + args: typeof item.arguments === "string" ? item.arguments : current.args, + }); + } + + if (obj.type !== "response.function_call_arguments.delta" || typeof obj.delta !== "string") return; + const key = String(obj.item_id ?? obj.output_index ?? toolCallParts.size); + const current = toolCallParts.get(key) ?? { id: "", name: "", args: "" }; + toolCallParts.set(key, { ...current, args: current.args + obj.delta }); +} + +function integrationMetadata(metadata: CallbackMetadata | null | undefined, operation: string): CallbackMetadata { + return { ...(metadata ?? {}), adrianIntegration: "openai", operation }; +} + +function isAsyncIterable(value: unknown): value is AsyncIterable { + return Boolean(value && typeof value === "object" && Symbol.asyncIterator in value); +} diff --git a/sdk/typescript/src/instrumentation/vercel.ts b/sdk/typescript/src/instrumentation/vercel.ts new file mode 100644 index 0000000..f70b1d3 --- /dev/null +++ b/sdk/typescript/src/instrumentation/vercel.ts @@ -0,0 +1,160 @@ +import { randomUUID } from "node:crypto"; +import { runWithInvocationId } from "../context.js"; +import { getHandler } from "../index.js"; +import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "../types.js"; +import { captureLlmCall, emptyLlmEnd, messagesFromPromptLike, normalizeUsage, parseToolArgs, stringifyContent } from "./common.js"; + +export interface VercelAIInstrumentationOptions { + metadata?: CallbackMetadata | null; +} + +export interface VercelAIToolCallLike { + toolCallId?: string; + id?: string; + toolName?: string; + name?: string; + args?: unknown; +} + +export interface VercelAIToolCallCaptureOptions { + metadata?: CallbackMetadata | null; + parentRunId?: string; +} + +type VercelToolExecute = (args: unknown, options?: unknown, ...rest: unknown[]) => unknown; + +const VERCEL_METHODS = new Set(["generateText", "streamText", "generateObject", "streamObject"]); + +export function instrumentVercelAI>(ai: T, options: VercelAIInstrumentationOptions = {}): T { + return new Proxy(ai, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (!VERCEL_METHODS.has(String(prop)) || typeof value !== "function") return value; + return function adrianVercelAI(this: unknown, args: Record = {}, ...rest: unknown[]) { + return captureVercelCall(String(prop), () => value.call(this, args, ...rest), args, options); + }; + }, + }); +} + +export const withAdrianVercelAI = instrumentVercelAI; + +export function instrumentVercelAITools>(tools: T, options: VercelAIToolCallCaptureOptions = {}): T { + return Object.fromEntries(Object.entries(tools).map(([toolName, toolDef]) => { + if (!toolDef || typeof toolDef !== "object") return [toolName, toolDef]; + const execute = (toolDef as { execute?: unknown }).execute; + if (typeof execute !== "function") return [toolName, toolDef]; + + return [toolName, { + ...(toolDef as Record), + execute(this: unknown, args: unknown, executionOptions?: unknown, ...rest: unknown[]) { + const toolCallId = extractToolCallId(executionOptions); + return captureVercelAIToolCall({ + toolCallId, + toolName, + args, + }, () => (execute as VercelToolExecute).call(this, args, executionOptions, ...rest), options); + }, + }]; + })) as T; +} + +export async function captureVercelAIToolCall( + toolCall: VercelAIToolCallLike, + execute: () => T | Promise, + options: VercelAIToolCallCaptureOptions = {}, +): Promise { + const handler = getHandler(); + if (!handler) return execute(); + + const runId = randomUUID(); + const toolName = String(toolCall.toolName ?? toolCall.name ?? "unknown"); + const toolCallId = String(toolCall.toolCallId ?? toolCall.id ?? ""); + const input = stringifyContent(toolCall.args); + const metadata = integrationMetadata(options.metadata, "vercel-ai.tool_call"); + + return runWithInvocationId(randomUUID(), async () => { + await handler.handleToolStart({ name: toolName }, input, runId, options.parentRunId, { metadata, toolCallId }); + try { + const result = await execute(); + await handler.handleToolEnd(result, runId); + return result; + } catch (error) { + await handler.handleToolError(error, runId); + throw error; + } + }); +} + +export const captureAITool = captureVercelAIToolCall; + +function captureVercelCall(operation: string, execute: () => T, args: Record, options: VercelAIInstrumentationOptions): unknown { + const model = extractModelName(args.model); + const metadata = integrationMetadata(options.metadata, operation); + const result = execute(); + + if (operation.startsWith("stream")) { + void Promise.resolve(result).then((resolved) => emitVercelStreamResult(model, args, metadata, resolved)).catch(() => undefined); + return result; + } + + return Promise.resolve(result).then((resolved) => captureLlmCall(getHandler, { model, messages: messagesFromPromptLike(args), metadata }, async () => resolved, extractVercelResult)); +} + +async function emitVercelStreamResult(model: string, args: Record, metadata: CallbackMetadata, result: unknown): Promise { + await captureLlmCall(getHandler, { model, messages: messagesFromPromptLike(args), metadata }, async () => result, async (streamResult) => { + const obj = streamResult && typeof streamResult === "object" ? streamResult as Record : {}; + const [text, toolCalls, usage] = await Promise.all([ + resolveMaybe(obj.text, ""), + resolveMaybe(obj.toolCalls, []), + resolveMaybe(obj.usage, null), + ]); + return emptyLlmEnd(typeof text === "string" ? text : stringifyContent(text), normalizeVercelToolCalls(toolCalls), normalizeVercelUsage(usage)); + }); +} + +function extractVercelResult(result: unknown): LlmEndData { + const obj = result && typeof result === "object" ? result as Record : {}; + const output = typeof obj.text === "string" ? obj.text : typeof obj.object !== "undefined" ? stringifyContent(obj.object) : ""; + return emptyLlmEnd(output, normalizeVercelToolCalls(obj.toolCalls), normalizeVercelUsage(obj.usage)); +} + +function normalizeVercelToolCalls(raw: unknown): ToolCallRecord[] { + if (!Array.isArray(raw)) return []; + return raw.map((call) => { + const obj = call && typeof call === "object" ? call as Record : {}; + return { + id: String(obj.toolCallId ?? obj.id ?? ""), + name: String(obj.toolName ?? obj.name ?? ""), + args: parseToolArgs(obj.args), + }; + }); +} + +function normalizeVercelUsage(usage: unknown): LlmEndData["usage"] { + return normalizeUsage(usage, ["promptTokens", "inputTokens"], ["completionTokens", "outputTokens"]); +} + +async function resolveMaybe(value: unknown, fallback: unknown): Promise { + if (value === undefined || value === null) return fallback; + return Promise.resolve(value); +} + +function extractModelName(model: unknown): string { + if (typeof model === "string") return model; + if (model && typeof model === "object") { + const obj = model as Record; + return String(obj.modelId ?? obj.model ?? obj.id ?? obj.name ?? "vercel-ai"); + } + return "vercel-ai"; +} + +function extractToolCallId(executionOptions: unknown): string { + if (!executionOptions || typeof executionOptions !== "object") return ""; + const obj = executionOptions as Record; + return String(obj.toolCallId ?? obj.id ?? ""); +} + +function integrationMetadata(metadata: CallbackMetadata | null | undefined, operation: string): CallbackMetadata { + return { ...(metadata ?? {}), adrianIntegration: "vercel-ai", operation }; +} diff --git a/sdk/typescript/src/pairing.ts b/sdk/typescript/src/pairing.ts index 53f0c29..9e70f3d 100644 --- a/sdk/typescript/src/pairing.ts +++ b/sdk/typescript/src/pairing.ts @@ -77,6 +77,7 @@ export class EventPairBuffer { output: endData.output ?? "", toolCalls: endData.toolCalls ?? [], usage: endData.usage ?? null, + error: endData.error, }, metadata: start.metadata, }; @@ -100,6 +101,7 @@ export class EventPairBuffer { toolCallId: startData.toolCallId ?? null, input: startData.input ?? "", output: endData.output ?? "", + error: endData.error, }, metadata: start.metadata, }; diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/src/types.ts index 9a1ef23..c8c0ceb 100644 --- a/sdk/typescript/src/types.ts +++ b/sdk/typescript/src/types.ts @@ -24,6 +24,12 @@ export interface TokenUsage { totalTokens: number; } +export interface ErrorData { + name: string; + message: string; + stack?: string; +} + export interface ChatModelStartData { model: string; messages: ChatMessage[]; @@ -40,6 +46,7 @@ export interface LlmEndData { output: string; toolCalls: ToolCallRecord[]; usage: TokenUsage | null; + error?: ErrorData; } export interface ToolStartData { @@ -51,6 +58,7 @@ export interface ToolStartData { export interface ToolEndData { output: string; + error?: ErrorData; } export type EventData = ChatModelStartData | LlmStartData | LlmEndData | ToolStartData | ToolEndData; diff --git a/sdk/typescript/tests/openai.test.ts b/sdk/typescript/tests/openai.test.ts new file mode 100644 index 0000000..0e26df0 --- /dev/null +++ b/sdk/typescript/tests/openai.test.ts @@ -0,0 +1,251 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { captureOpenAIToolCall, init, instrumentOpenAI, shutdown } from "../src/index.js"; +import type { EventData } from "../src/types.js"; + +describe("OpenAI instrumentation", () => { + afterEach(async () => { + await shutdown(); + }); + + it("captures chat completion calls as paired LLM events", async () => { + const events: EventData[] = []; + const client = instrumentOpenAI({ + chat: { + completions: { + create: async (_body: Record) => ({ + choices: [{ + message: { + content: "hello", + tool_calls: [{ + id: "call-1", + function: { name: "search", arguments: "{\"query\":\"docs\"}" }, + }], + }, + }], + usage: { prompt_tokens: 3, completion_tokens: 4, total_tokens: 7 }, + }), + }, + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await client.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "system", content: "be brief" }, { role: "user", content: "hi" }], + }); + + expect(result.choices[0]?.message.content).toBe("hello"); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + output: "hello", + usage: { promptTokens: 3, completionTokens: 4, totalTokens: 7 }, + }); + expect("toolCalls" in events[0] && events[0].toolCalls[0]).toMatchObject({ id: "call-1", name: "search", args: { query: "docs" } }); + }); + + it("captures responses API calls", async () => { + const events: EventData[] = []; + const client = instrumentOpenAI({ + responses: { + create: async (_body: Record) => ({ + output_text: "done", + output: [{ type: "function_call", call_id: "call-2", name: "lookup", arguments: "{\"id\":42}" }], + usage: { input_tokens: 5, output_tokens: 6, total_tokens: 11 }, + }), + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + await client.responses.create({ model: "gpt-4.1", input: "run lookup" }); + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4.1", + output: "done", + toolCalls: [{ id: "call-2", name: "lookup", args: { id: 42 } }], + }); + }); + + it("captures camelCase chat tool calls", async () => { + const events: EventData[] = []; + const client = instrumentOpenAI({ + chat: { + completions: { + create: async (_body: Record) => ({ + choices: [{ + message: { + content: null, + toolCalls: [{ + id: "call-3", + name: "search", + args: { query: "camel" }, + }], + }, + }], + }), + }, + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "use search" }] }); + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + toolCalls: [{ id: "call-3", name: "search", args: { query: "camel" } }], + }); + }); + + it("captures responses API streaming tool calls and text", async () => { + const events: EventData[] = []; + async function* stream() { + yield { type: "response.output_text.delta", delta: "The answer " }; + yield { type: "response.output_item.added", item: { id: "item-1", type: "function_call", call_id: "call-4", name: "lookup" } }; + yield { type: "response.function_call_arguments.delta", item_id: "item-1", delta: "{\"id\"" }; + yield { type: "response.function_call_arguments.delta", item_id: "item-1", delta: ":7}" }; + yield { type: "response.output_text.delta", delta: "is ready." }; + } + const client = instrumentOpenAI({ + responses: { + create: async (_body: Record) => stream(), + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await client.responses.create({ model: "gpt-4.1", input: "lookup id 7", stream: true }); + for await (const _chunk of result) { + // consume the stream so Adrian can emit the paired event + } + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4.1", + output: "The answer is ready.", + toolCalls: [{ id: "call-4", name: "lookup", args: { id: 7 } }], + }); + }); + + it("emits partial stream data when the consumer stops early", async () => { + const events: EventData[] = []; + async function* stream() { + yield { choices: [{ delta: { content: "first " } }] }; + yield { choices: [{ delta: { content: "second" } }] }; + } + const client = instrumentOpenAI({ + chat: { + completions: { + create: async (_body: Record) => stream(), + }, + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "stream" }], stream: true }); + for await (const _chunk of result) { + break; + } + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + output: "first ", + }); + }); + + it("captures local OpenAI tool execution as a tool event", async () => { + const events: Array<{ type: string; data: EventData }> = []; + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + const result = await captureOpenAIToolCall({ + id: "call-weather", + type: "function", + function: { name: "get_weather", arguments: "{\"city\":\"San Francisco\"}" }, + }, async () => ({ temperatureF: 58, condition: "cloudy" })); + + expect(result).toEqual({ temperatureF: 58, condition: "cloudy" }); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "get_weather", + toolCallId: "call-weather", + input: "{\"city\":\"San Francisco\"}", + output: "{\"temperatureF\":58,\"condition\":\"cloudy\"}", + }, + }); + }); + + it("captures local OpenAI tool execution errors as tool events", async () => { + const events: Array<{ type: string; data: EventData }> = []; + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + await expect(captureOpenAIToolCall({ + id: "call-weather", + type: "function", + function: { name: "get_weather", arguments: "{\"city\":\"San Francisco\"}" }, + }, async () => { + throw new Error("weather API unavailable"); + })).rejects.toThrow("weather API unavailable"); + + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "get_weather", + toolCallId: "call-weather", + output: "[ERROR] Error: weather API unavailable", + error: { name: "Error", message: "weather API unavailable" }, + }, + }); + }); + + it("captures OpenAI request errors as LLM events", async () => { + const events: Array<{ type: string; data: EventData }> = []; + const client = instrumentOpenAI({ + chat: { + completions: { + create: async (_body: Record) => { + throw new Error("rate limited"); + }, + }, + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + await expect(client.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hi" }], + })).rejects.toThrow("rate limited"); + + expect(events[0]).toMatchObject({ + type: "llm", + data: { + kind: "llm", + model: "gpt-4o-mini", + output: "[ERROR] Error: rate limited", + error: { name: "Error", message: "rate limited" }, + }, + }); + }); +}); diff --git a/sdk/typescript/tests/vercel.test.ts b/sdk/typescript/tests/vercel.test.ts new file mode 100644 index 0000000..00cc887 --- /dev/null +++ b/sdk/typescript/tests/vercel.test.ts @@ -0,0 +1,144 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { captureAITool, captureVercelAIToolCall, init, instrumentVercelAI, instrumentVercelAITools, shutdown } from "../src/index.js"; +import type { EventData } from "../src/types.js"; + +describe("Vercel AI SDK instrumentation", () => { + afterEach(async () => { + await shutdown(); + }); + + it("captures generateText calls as paired LLM events", async () => { + const events: EventData[] = []; + const ai = instrumentVercelAI({ + generateText: async (_args: Record) => ({ + text: "hello from vercel", + toolCalls: [{ toolCallId: "tool-1", toolName: "search", args: { query: "adrian" } }], + usage: { promptTokens: 8, completionTokens: 9, totalTokens: 17 }, + }), + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await ai.generateText({ + model: { modelId: "openai/gpt-4o-mini" }, + system: "be brief", + prompt: "hello", + }); + + expect(result.text).toBe("hello from vercel"); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + kind: "llm", + model: "openai/gpt-4o-mini", + output: "hello from vercel", + toolCalls: [{ id: "tool-1", name: "search", args: { query: "adrian" } }], + usage: { promptTokens: 8, completionTokens: 9, totalTokens: 17 }, + }); + }); + + it("emits streamText events when the result promises settle", async () => { + const events: EventData[] = []; + const ai = instrumentVercelAI({ + streamText: (_args: Record) => ({ + text: Promise.resolve("streamed"), + toolCalls: Promise.resolve([]), + usage: Promise.resolve({ inputTokens: 2, outputTokens: 3, totalTokens: 5 }), + }), + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + const result = await ai.streamText({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }] }); + await result.text; + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o", + output: "streamed", + usage: { promptTokens: 2, completionTokens: 3, totalTokens: 5 }, + }); + }); + + it("captures local Vercel AI tool execution as a tool event", async () => { + const events: Array<{ type: string; data: EventData }> = []; + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + const result = await captureVercelAIToolCall({ + toolCallId: "tool-weather", + toolName: "getWeather", + args: { city: "San Francisco" }, + }, async () => ({ temperatureF: 58, condition: "cloudy" })); + + expect(result).toEqual({ temperatureF: 58, condition: "cloudy" }); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "getWeather", + toolCallId: "tool-weather", + input: "{\"city\":\"San Francisco\"}", + output: "{\"temperatureF\":58,\"condition\":\"cloudy\"}", + }, + }); + }); + + it("captures local Vercel AI tool errors as tool events", async () => { + const events: Array<{ type: string; data: EventData }> = []; + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + await expect(captureAITool({ + toolCallId: "tool-weather", + toolName: "getWeather", + args: { city: "San Francisco" }, + }, async () => { + throw new Error("weather API unavailable"); + })).rejects.toThrow("weather API unavailable"); + + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "getWeather", + toolCallId: "tool-weather", + output: "[ERROR] Error: weather API unavailable", + error: { name: "Error", message: "weather API unavailable" }, + }, + }); + }); + + it("wraps Vercel AI SDK tool execute functions", async () => { + const events: Array<{ type: string; data: EventData }> = []; + const tools = instrumentVercelAITools({ + getWeather: { + description: "Get current weather for a city.", + execute: async ({ city }: { city: string }, _options?: unknown) => ({ city, temperatureF: 58 }), + }, + }); + + await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + const result = await tools.getWeather.execute({ city: "San Francisco" }, { toolCallId: "tool-weather" }); + + expect(result).toEqual({ city: "San Francisco", temperatureF: 58 }); + expect(events[0]).toMatchObject({ + type: "tool", + data: { + kind: "tool", + toolName: "getWeather", + toolCallId: "tool-weather", + input: "{\"city\":\"San Francisco\"}", + output: "{\"city\":\"San Francisco\",\"temperatureF\":58}", + }, + }); + }); +}); From 14c73285411f937ef4a91c6f45565c1e09f7fa0c Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Tue, 2 Jun 2026 12:49:53 +0530 Subject: [PATCH 03/10] feat(sdk/typescript): split SDK into provider packages with shared core capture registry --- CONTRIBUTING.md | 11 ++ backend/data/adrian.db | Bin 0 -> 172032 bytes sdk/python/uv.lock | 102 +++++++++- sdk/typescript/README.md | 184 ++++++++++++------ sdk/typescript/package-lock.json | 138 ++++++++++--- sdk/typescript/package.json | 65 +------ sdk/typescript/packages/core/README.md | 63 ++++++ sdk/typescript/packages/core/package.json | 53 +++++ .../core/src/capture}/common.ts | 0 .../packages/core/src/capture/index.ts | 1 + .../{ => packages/core}/src/config.ts | 1 - .../{ => packages/core}/src/context.ts | 0 .../{ => packages/core}/src/format/types.ts | 0 .../{ => packages/core}/src/handler.ts | 0 .../{ => packages/core}/src/handlers/jsonl.ts | 0 .../{ => packages/core}/src/hooks.ts | 0 .../{ => packages/core}/src/identity.ts | 0 .../{ => packages/core}/src/index.ts | 25 +-- sdk/typescript/{ => packages/core}/src/mcp.ts | 0 .../{ => packages/core}/src/pairing.ts | 0 .../{ => packages/core}/src/pii/engine.ts | 0 .../{ => packages/core}/src/pii/index.ts | 0 .../{ => packages/core}/src/pii/patterns.ts | 0 .../{ => packages/core}/src/pii/redactor.ts | 0 .../{ => packages/core}/src/pii/strategies.ts | 0 .../{ => packages/core}/src/proto/schema.ts | 0 sdk/typescript/packages/core/src/registry.ts | 18 ++ .../core}/src/sessionPersistence.ts | 0 .../{ => packages/core}/src/types.ts | 0 sdk/typescript/{ => packages/core}/src/ws.ts | 0 .../{ => packages/core}/tests/jsonl.test.ts | 0 .../{ => packages/core}/tests/pairing.test.ts | 0 .../{ => packages/core}/tests/pii.test.ts | 0 .../{ => packages/core}/tests/proto.test.ts | 0 .../{ => packages/core}/tests/session.test.ts | 0 .../{ => packages/core}/tests/ws.test.ts | 0 .../{ => packages/core}/tsconfig.build.json | 0 sdk/typescript/packages/core/tsconfig.json | 8 + sdk/typescript/packages/langchain/README.md | 57 ++++++ .../packages/langchain/package.json | 57 ++++++ .../langchain/src/index.ts} | 60 ++++-- .../langchain}/tests/langchain.test.ts | 7 +- .../packages/langchain/tsconfig.build.json | 12 ++ .../packages/langchain/tsconfig.json | 8 + sdk/typescript/packages/openai/README.md | 70 +++++++ sdk/typescript/packages/openai/package.json | 52 +++++ .../openai/src/index.ts} | 53 +++-- .../openai}/tests/openai.test.ts | 61 ++++-- .../packages/openai/tsconfig.build.json | 12 ++ sdk/typescript/packages/openai/tsconfig.json | 8 + sdk/typescript/packages/vercel/README.md | 92 +++++++++ sdk/typescript/packages/vercel/package.json | 52 +++++ .../vercel/src/index.ts} | 94 ++++++--- .../vercel}/tests/vercel.test.ts | 65 +++++-- .../packages/vercel/tsconfig.build.json | 12 ++ sdk/typescript/packages/vercel/tsconfig.json | 8 + sdk/typescript/tsconfig.base.json | 13 +- sdk/typescript/tsconfig.json | 18 -- 58 files changed, 1198 insertions(+), 282 deletions(-) create mode 100644 backend/data/adrian.db create mode 100644 sdk/typescript/packages/core/README.md create mode 100644 sdk/typescript/packages/core/package.json rename sdk/typescript/{src/instrumentation => packages/core/src/capture}/common.ts (100%) create mode 100644 sdk/typescript/packages/core/src/capture/index.ts rename sdk/typescript/{ => packages/core}/src/config.ts (98%) rename sdk/typescript/{ => packages/core}/src/context.ts (100%) rename sdk/typescript/{ => packages/core}/src/format/types.ts (100%) rename sdk/typescript/{ => packages/core}/src/handler.ts (100%) rename sdk/typescript/{ => packages/core}/src/handlers/jsonl.ts (100%) rename sdk/typescript/{ => packages/core}/src/hooks.ts (100%) rename sdk/typescript/{ => packages/core}/src/identity.ts (100%) rename sdk/typescript/{ => packages/core}/src/index.ts (84%) rename sdk/typescript/{ => packages/core}/src/mcp.ts (100%) rename sdk/typescript/{ => packages/core}/src/pairing.ts (100%) rename sdk/typescript/{ => packages/core}/src/pii/engine.ts (100%) rename sdk/typescript/{ => packages/core}/src/pii/index.ts (100%) rename sdk/typescript/{ => packages/core}/src/pii/patterns.ts (100%) rename sdk/typescript/{ => packages/core}/src/pii/redactor.ts (100%) rename sdk/typescript/{ => packages/core}/src/pii/strategies.ts (100%) rename sdk/typescript/{ => packages/core}/src/proto/schema.ts (100%) create mode 100644 sdk/typescript/packages/core/src/registry.ts rename sdk/typescript/{ => packages/core}/src/sessionPersistence.ts (100%) rename sdk/typescript/{ => packages/core}/src/types.ts (100%) rename sdk/typescript/{ => packages/core}/src/ws.ts (100%) rename sdk/typescript/{ => packages/core}/tests/jsonl.test.ts (100%) rename sdk/typescript/{ => packages/core}/tests/pairing.test.ts (100%) rename sdk/typescript/{ => packages/core}/tests/pii.test.ts (100%) rename sdk/typescript/{ => packages/core}/tests/proto.test.ts (100%) rename sdk/typescript/{ => packages/core}/tests/session.test.ts (100%) rename sdk/typescript/{ => packages/core}/tests/ws.test.ts (100%) rename sdk/typescript/{ => packages/core}/tsconfig.build.json (100%) create mode 100644 sdk/typescript/packages/core/tsconfig.json create mode 100644 sdk/typescript/packages/langchain/README.md create mode 100644 sdk/typescript/packages/langchain/package.json rename sdk/typescript/{src/instrumentation/langchain.ts => packages/langchain/src/index.ts} (76%) rename sdk/typescript/{ => packages/langchain}/tests/langchain.test.ts (90%) create mode 100644 sdk/typescript/packages/langchain/tsconfig.build.json create mode 100644 sdk/typescript/packages/langchain/tsconfig.json create mode 100644 sdk/typescript/packages/openai/README.md create mode 100644 sdk/typescript/packages/openai/package.json rename sdk/typescript/{src/instrumentation/openai.ts => packages/openai/src/index.ts} (88%) rename sdk/typescript/{ => packages/openai}/tests/openai.test.ts (80%) create mode 100644 sdk/typescript/packages/openai/tsconfig.build.json create mode 100644 sdk/typescript/packages/openai/tsconfig.json create mode 100644 sdk/typescript/packages/vercel/README.md create mode 100644 sdk/typescript/packages/vercel/package.json rename sdk/typescript/{src/instrumentation/vercel.ts => packages/vercel/src/index.ts} (70%) rename sdk/typescript/{ => packages/vercel}/tests/vercel.test.ts (68%) create mode 100644 sdk/typescript/packages/vercel/tsconfig.build.json create mode 100644 sdk/typescript/packages/vercel/tsconfig.json delete mode 100644 sdk/typescript/tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06b377e..f1c977a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,17 @@ source .venv/bin/activate pre-commit install # wires the git hook ``` +The TypeScript SDK lives at `sdk/typescript/` as an npm workspace with four packages (`core`, `openai`, `vercel`, `langchain`). From that directory: + +```sh +cd sdk/typescript +npm install +npm run build +npm test +``` + +Provider packages include `@secureagentics/adrian` as a dependency and re-export `init`, `shutdown`, and other core APIs — users install one package (e.g. `@secureagentics/adrian-openai openai`) and import everything from it. See [`sdk/typescript/README.md`](sdk/typescript/README.md) for usage examples. + After `pre-commit install`, every `git commit` runs the configured hooks on staged files: `ruff format`, `ruff check --fix`, `basedpyright` on `sdk/python/adrian/`, plus the standard whitespace / YAML / TOML checks. Hooks that diff --git a/backend/data/adrian.db b/backend/data/adrian.db new file mode 100644 index 0000000000000000000000000000000000000000..f17b681f76becc1716bc262f864d0e2606cfaf49 GIT binary patch literal 172032 zcmeI4O>7)TcE@MIB!^sb50G3EAh*qF5o3=*kV}978w9DY?&=Tr z^k}%zENJRKu;-)e)vH&(diAQh`cXGO*wA`Jthc*Or6*=Xr$b|7q3?@gC=?o_|G!E9 z=P!YNnczR@XV~>U=<8VM(x3n1q#+#togwp``0q}golVVrJ#%jA=do{Mzl!`~>XWHA zBUdJV8JU~>x3SO0vSZmGWw?GU2wZ$ABD^&p4rwiw+|xgAQ2&vY{a%~>mdPE`>gjUE z{5*TG`eIaASO`Da=_%C)>9iYKO(Q!0JhQe{S>3LP+p9m=sEGWkxD*rVOH++^_FCWE zuDny(5`Vb0etmW8miT_O>xN*Ury|4y+&d=QEjwqw-ZULL1yxOxtSJ4 zZlkZkG&j`|HWa6qGl#WEN6{kD7W-<+4$zW0Y)dvsOYSIIa)-6-cT}ZERG9{qz=2rO zd)<0ZYm%kJ;;qzTGqtF0FTT67cztED{xK!pYTu=tW64J=@u)CAA3nIjd`Q_fJj{GQ z?eQP;n%{ev1uoYyeT7NO%*mL5J#IUj>mTe?Org5g)qAo|NXzVpEyTTPxK5bL8Vc9- z43srVPoazz>bqX^xn+qj9wcJPi!ThCttK&9GCyCuczOuoj4AwrG8Ykw3*iTB-fQYT zS?OrvY?7tw9XGV=dXT zTa~vfTb0eV%1zPqOiNOy5iV)qxmMYz&`^1^V#YI5X{}56JTolrP|Uie1%=iR~#{a#SYoM54l5 zwDmIqt+R@n>Zu=nO4C*y!_Rz8TV=^dXD6aUA`yP9v5Dx>@A)z5nTDRCfLZ99f>xV` zJ>Jmeiid{G;V`(idUI{{nzc$h%)Cl`G1A+_(V}B4S)Pmt*Aqd0=;^+k@%)S&6vI(L z5W-Kc+emfRtuY@$-H*CjMe{5?hCG+HXRSS za>uF`yfe=P#cbY)$hXVQ{(3$8Z&wI)>K``KrZikIjU|)g5#g#Z$mw)hVy-li9DBMb z^Gki)ypw$S%&hU{^s%_Fgb^SB0w4eaAOHd&00JNY0w4eaAOHd*A`mn5;{Ja`bfFdm zKmY_l00ck)1V8`;KmY_l00fQ)0o?x|k8yw%AOHd&00JNY0w4eaAOHd&00JN|N&?9L zA0=Jr1_2NN0T2KI5C8!X009sH0T2Lz<3Rw=|BuHwzzPrm0T2KI5C8!X009sH0T2KI z5Evx^-2ab~E_8zc2!H?xfB*=900@8p2!H?xfWYw}fcyXBF%GZ-1V8`;KmY_l00ck) z1V8`;KmY_rNdSNUKT5jL4FVtl0w4eaAOHd&00JNY0w4ea$AbW#{~wQWfE6GB0w4ea zAOHd&00JNY0w4eaATUY-xc?s|UFZe@5C8!X009sH0T2KI5C8!X0D_DDA0=Jr1_2NN0T2KI5C8!X z009sH0T2Lz<3RxT|Hoq-UBk(o!Ln&Zi`4J6&9n3M=XS zrF0?vG5-Gl+cvPU7X&~61V8`;KmY_l00ck)1V8`;j!ppi|3_zr8Xy1yAOHd&00JNY z0w4eaAOHd&@NFY78~^Xnnb40zvwQJ>ivQ)Qv$Lt0uV>Cp{XF(f>{pRrOnox-X5`Am z|4#hN#4pD_3;lR3JFtI`YiFXu{CxPU^F5{7Amk2d^>p+7rM0cf>UKrkUj4yFMKrD* zNw}wrOEHnYG}U-_y|ybKZ5uy-xV3(Lb?cV+e&yCOlhlc>Yweb7*Wi+yH@3yiosA7s zOxdMIIzpmoh`Eb8-F97T5JQA(u$$kiyj|I(+Z(lPo0`Z>1KSsYP{r@!gfh>nn@(j}yy@ zR{L%unT#b5R$q(?3k%^VJIp0I?S@vO>xN*Ury|4y$0=Bwb8EKrhPLTWG3I2n`u$x zHu~XmQypPLae6s(Sc`NNEfQ_9ua@ipEt$i%WP`Nij-n-ZSj&D#<(_odH|>EK)`u>> z5E0&*4~MjtO77{OH?$s+mHl3u{g&Cp(`Cv0eDUJxA%rug@C%Pt;w)et++f~mUNzrO zdpy&;=J!k1HcUpz>}lFD2}C@zE_uwl^Dd$Q8eM5nWKY0RKGY9V4rG|0tQ$yZ>16w(JlPJYtvyt=CJ@oWx@5r+?oUgt9@n=Iy2gNs0TS_x=w_-* zqc+=H>N2_4(Yi#Z8z#^7DPO?z6uYV!anDn>mZ>`o+hQG!@OW@VKSz9=n5@EBG0*=Iij>Hwkvz4IU@Cpfz0dr>&fCToQ`f)LMZX zY)6B&)n?kwtv0K;oM@6}m2{m=wxa8I+g+7yDtPt&mNxh4MyR%@w04Qyv8n~{%rikT zn>Qlz?Q(!#&;HxxXHNaYW*&L>n;ee_SA{`NM>oIBl_rv7PZwo=sgKk9_$lLF9?$;= zo@k&D1V8`;KmY_l00ck)1V8`;KmY_z1cBL5EA&#R7>a#3{a@367Wt3qYUD}ewaLF7 z`_IXrPQEu*on0EsoccQS+fzRZ-HH7(6)duD=MxcO{q>+jiDs=sk4U?B=&7BYas7BR zc3_@5G!~=6)pOy8)Aqrpvaf1AxzXO0ol_M1+LG@~(=Kv}vqwl=&61-g`RKjZqr$m! z;e(p_T!G8jO5X6C!P%F8-y=->@aHKnYt`Vm>D@O3jNZ zeJrLmbfb=^ihagpJg9rdXP}qQ4|=L|msI!K?b~$N`M2*qoH-X2%ICum*X_YJo}|m4 z?~{FcMn_tz*4lNhT=0#xQ^uv)sU1DNc1qmC%}PIfmnLS2de1nRr}+U1tDVHbhp$G3 z^XJ1)?lWI-G&tYRdAz~7?so^1y8kHF$a}F;O1$G@9>0n%zc907Sh@&1khc;>Jg3`q z9;sT*)X4PN4rYCE>9b@9o|d~aV;pB{Dym$w(kDDtn+{~5WG#A%9cSZ=`yZ-WhrN~_ zcHGM=EGLu>eR_9?s5HN#OMXOZGzrB%{iV+%HFB3ekgVP}f!p18^_v}l4$Qi-)zIV? zr`JxH2%1B~J{-ut77^Y#9~2TDH+te}309^ZC_2^WK>S^xMGs?~$~hl(-z%6#%Ho{ieS| zi7E3xcrhoa=K=XM%vN*9g}%M978sg>qHA@nrdZ1u3lg+~-DxWT)VzW$5L8J^+ikJ5 zv7T1H@5{2HX=!@9Mb`<7fNKIbXqr*0Ml-P3GDyp<2lS>@#~=gl@^=O;nB2+a4FC4Z zXD`#dn)&cg?U`V@h-(DE7bEVTP zMAD<*_glAVvSrI&F6oA-=t&jGmTh7>D6Az~@M1@tlD!gUS1-`A6SrR8raAAo#{OY35VR?yH zo^K#|>5{3+vvCt?`U z2@n7Q5C8!X009sH0T2KI5C8!X7)Sv3{{!)$5ClK~1V8`;KmY_l00ck)1V8`;P6Pq` z`+p~57|{t3009sH0T2KI5C8!X009sH0T38S0MGvi;z1z@fB*=900@8p2!H?xfB*=9 z00^820=WM_5yOa1fB*=900@8p2!H?xfB*=900@Asp8pTTgF+Ah0T2KI z5C8!X009sH0T2KI5I7M8aQ}ZIh7p|r0T2KI5C8!X009sH0T2KI5CDOJ1dROuSnMxC z@jG+>K3ARn_3Y-UzdyAy^EWeZ(2ID100@8p2!H?xfB*=9z#ovn!)PqJ6IvAXt7)ZH z&X=mCRJEKbrEFEmL;W3 zsVYH>3EjQrA%2>)KWR4 zj>eMGrDD!tGRggV>jY9+f3|DMKxpuq?^d zic&4FR8xg|u}Ei3u}qz{q^7EhqUNe~>L__K5=*4wrF_A#WZbeu%2zWao0U@elBA|` zb;FXhTBC0gsgG*nB$iT zLvWbWk#R$Cn8S;)#Q4tiFlP(q{r_)5@!!V(A^unKKZ$qZH{*r){M`S{{pMu29(n-+ zAOHd&00JNY0w4eaAOHd&@Ej0$b^K~LD5)prFLd&3rh^KN)R(ECLL(a`+P~0D5Qzj8 z8aW%2eG9ELiiy63R%XL^|3Wj_VC?&tsb_y3=RS%5Br00@8p2!H?xfB*=9 q00@8p2t4-$@bCXU_bvbcAOHd&00JNY0w4eaAOHd&00Peef&T+rmV0vm literal 0 HcmV?d00001 diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 2c18e6e..5d29efe 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.12" [[package]] name = "adrian-sdk-oss" -version = "1.0.2" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "langchain-core" }, @@ -18,6 +18,7 @@ dev = [ { name = "langchain-mcp-adapters" }, { name = "langgraph" }, { name = "langgraph-prebuilt" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -31,6 +32,7 @@ requires-dist = [ { name = "langchain-mcp-adapters", marker = "extra == 'dev'", specifier = ">=0.2.2" }, { name = "langgraph", marker = "extra == 'dev'", specifier = "==1.1.2" }, { name = "langgraph-prebuilt", marker = "extra == 'dev'", specifier = "==1.0.8" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.0" }, { name = "protobuf", specifier = ">=5.29.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, @@ -149,6 +151,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -380,6 +391,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -426,6 +455,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.13" @@ -626,6 +664,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "nodejs-wheel-binaries" version = "24.15.0" @@ -743,6 +790,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -752,6 +808,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "protobuf" version = "7.34.1" @@ -946,6 +1018,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "python-discovery" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1273,6 +1358,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, ] +[[package]] +name = "virtualenv" +version = "21.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, +] + [[package]] name = "websockets" version = "16.0" diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index ba4379f..0e24d2c 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -1,105 +1,173 @@ -# @secureagentics/adrian +# Adrian TypeScript SDK -TypeScript SDK for Adrian multi-agent event capture in Node.js LangChain.js, LangGraph.js, OpenAI SDK, and Vercel AI SDK applications. +Monorepo for the Adrian TypeScript SDK. Pick the package for your framework — the core SDK is installed automatically. + +## Packages + +| Package | npm name | Install | Import | +|---|---|---|---| +| OpenAI | `@secureagentics/adrian-openai` | `npm install @secureagentics/adrian-openai openai` | `import { init, adrian, captureTool } from "@secureagentics/adrian-openai"` | +| Vercel AI | `@secureagentics/adrian-vercel` | `npm install @secureagentics/adrian-vercel ai` | `import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"` | +| LangChain | `@secureagentics/adrian-langchain` | `npm install @secureagentics/adrian-langchain @langchain/core @langchain/langgraph` | `import { init, adrian } from "@secureagentics/adrian-langchain"` | +| Core only | `@secureagentics/adrian` | `npm install @secureagentics/adrian` | `import { init, shutdown } from "@secureagentics/adrian"` | + +Provider packages depend on `@secureagentics/adrian` and re-export `init`, `shutdown`, and other core APIs — one install, one import. + +## Two-step setup + +1. **`init()`** — starts the event pipeline (JSONL, WebSocket, PII redaction). +2. **`adrian()`** — connects your framework to Adrian. + +Both come from the same provider package: ```ts -import { init } from "@secureagentics/adrian"; +// OpenAI +import { init, adrian, captureTool } from "@secureagentics/adrian-openai"; + +// Vercel AI +import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; + +// LangChain / LangGraph +import { init, adrian } from "@secureagentics/adrian-langchain"; await init({ apiKey: process.env.ADRIAN_API_KEY }); ``` -The SDK mirrors the Python package: it pairs LLM/tool callbacks into `PairedEvent` objects, redacts PII, writes JSONL locally, streams protobuf frames to the Adrian WebSocket endpoint, tracks MCP servers, and applies BLOCK/HITL tool gating when LangGraph ToolNode instrumentation is available. +## Unified API + +All provider packages export the **same function names**: + +| Export | OpenAI | Vercel AI | LangChain | +|---|---|---|---| +| `init` / `shutdown` | Re-exported from core | Re-exported from core | Re-exported from core | +| `adrian(...)` | Wrap an OpenAI client | Wrap an AI module or tools object | Enable callbacks (no arguments) | +| `adrianTools(...)` | — | Wrap tool definitions for `generateText` | — | +| `captureTool(...)` | Capture manual tool execution | Capture manual tool execution | — | + +Shared option types (same names in every provider package): + +| Type | Purpose | +|---|---| +| `AdrianOptions` | Optional metadata when wrapping a client or module | +| `ToolCallLike` | Shape of a tool call passed to `captureTool` | +| `ToolCaptureOptions` | Optional metadata when capturing tool execution | -## OpenAI SDK +## Examples + +### OpenAI + +```bash +npm install @secureagentics/adrian-openai openai +``` ```ts import OpenAI from "openai"; -import { init, instrumentOpenAI } from "@secureagentics/adrian"; +import { init, shutdown, adrian, captureTool } from "@secureagentics/adrian-openai"; await init({ apiKey: process.env.ADRIAN_API_KEY }); -const openai = instrumentOpenAI(new OpenAI()); -await openai.chat.completions.create({ +const openai = adrian(new OpenAI()); + +const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Hello" }], }); -``` - -`instrumentOpenAI` captures `chat.completions.create` and `responses.create` calls. Streaming chat completions are captured when the returned async iterable is consumed. -OpenAI returns tool call requests, but your app executes the tools. Wrap that local execution with `captureOpenAIToolCall` to emit Adrian tool events: - -```ts -import { captureOpenAIToolCall } from "@secureagentics/adrian"; - -for (const toolCall of assistantMessage.tool_calls ?? []) { - const toolResult = await captureOpenAIToolCall(toolCall, () => - runTool(toolCall.function.name, toolCall.function.arguments), - ); - - messages.push({ - role: "tool", - tool_call_id: toolCall.id, - content: toolResult, - }); +for (const toolCall of response.choices[0].message.tool_calls ?? []) { + await captureTool(toolCall, () => runTool(toolCall.function.name, toolCall.function.arguments)); } + +await shutdown(); ``` -## Vercel AI SDK +### Vercel AI SDK + +```bash +npm install @secureagentics/adrian-vercel ai +``` ```ts import * as ai from "ai"; -import { init, instrumentVercelAI } from "@secureagentics/adrian"; +import { init, shutdown, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; await init({ apiKey: process.env.ADRIAN_API_KEY }); -const adrianAI = instrumentVercelAI(ai); -await adrianAI.generateText({ +const monitored = adrian(ai); + +const result = await monitored.generateText({ model, - prompt: "Hello", + prompt: "What's the weather in London?", + tools: adrianTools(tools), }); + +for (const toolCall of result.toolCalls ?? []) { + await captureTool(toolCall, () => runTool(toolCall.toolName, toolCall.args)); +} + +await shutdown(); ``` -`instrumentVercelAI` wraps `generateText`, `streamText`, `generateObject`, and `streamObject`. Stream results are emitted after the Vercel result promises such as `text`, `toolCalls`, and `usage` settle. +### LangChain / LangGraph -If you pass Vercel AI SDK tools with `execute` functions, wrap the tools object before passing it to `generateText`: +```bash +npm install @secureagentics/adrian-langchain @langchain/core @langchain/langgraph +``` ```ts -import { instrumentVercelAITools } from "@secureagentics/adrian"; +import { init, shutdown, adrian } from "@secureagentics/adrian-langchain"; -const result = await adrianAI.generateText({ - model, - prompt: "Use the weather tool.", - tools: instrumentVercelAITools(tools), -}); +await init({ apiKey: process.env.ADRIAN_API_KEY }); +await adrian(); + +// Your normal LangChain / LangGraph code runs here. + +await shutdown(); ``` -If your app executes Vercel AI SDK tool calls manually, wrap that execution with `captureVercelAIToolCall` or the shorter `captureAITool` alias: +## Using multiple providers + +Install each provider package you need. Core is deduplicated to a single copy: + +```bash +npm install @secureagentics/adrian-openai @secureagentics/adrian-vercel openai ai +``` ```ts -import { captureAITool } from "@secureagentics/adrian"; +import { init, adrian as adrianOpenAI } from "@secureagentics/adrian-openai"; +import { adrian as adrianVercel } from "@secureagentics/adrian-vercel"; +import { adrian as adrianLangChain } from "@secureagentics/adrian-langchain"; -for (const toolCall of result.toolCalls ?? []) { - const toolResult = await captureAITool(toolCall, () => - runTool(toolCall.toolName, toolCall.args), - ); -} +await init({ apiKey: process.env.ADRIAN_API_KEY }); +await adrianLangChain(); + +const openai = adrianOpenAI(new OpenAI()); +const monitored = adrianVercel(vercelAi); ``` -## Environment +## Environment variables -- `ADRIAN_API_KEY` -- `ADRIAN_LOG_FILE` -- `ADRIAN_WS_URL` -- `ADRIAN_SESSION_ID` -- `ADRIAN_BLOCK_TIMEOUT` -- `ADRIAN_REPLAY_BUFFER_FRAMES` +| Variable | Description | +|---|---| +| `ADRIAN_API_KEY` | API key from the Adrian dashboard | +| `ADRIAN_LOG_FILE` | Local JSONL log path (default: `events.jsonl`) | +| `ADRIAN_WS_URL` | WebSocket endpoint (default: `ws://localhost:8080/ws`) | +| `ADRIAN_SESSION_ID` | Session identifier for grouping events | +| `ADRIAN_BLOCK_TIMEOUT` | Seconds to wait for a BLOCK/HITL verdict | +| `ADRIAN_REPLAY_BUFFER_FRAMES` | WebSocket replay buffer size | -## Manual Callback Wiring +## Development -```ts -import { init, getHandler } from "@secureagentics/adrian"; +```bash +npm install +npm run build +npm test +``` -await init({ autoInstrument: false }); -const handler = getHandler(); +Build or test a single package: + +```bash +npm run build -w @secureagentics/adrian +npm test -w @secureagentics/adrian-openai ``` + +Per-package docs: [core](./packages/core) · [openai](./packages/openai) · [vercel](./packages/vercel) · [langchain](./packages/langchain) diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index ff0c625..38f334c 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -1,43 +1,20 @@ { - "name": "@secureagentics/adrian", + "name": "@secureagentics/adrian-workspace", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@secureagentics/adrian", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "protobufjs": "^8.4.2", - "ws": "^8.20.1" - }, + "name": "@secureagentics/adrian-workspace", + "workspaces": [ + "packages/*" + ], "devDependencies": { "@types/node": "^25.9.1", "@types/ws": "^8.18.1", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.7" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.0", - "@langchain/langgraph": ">=0.2.0", - "ai": ">=4.0.0", - "openai": ">=4.0.0" - }, - "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "@langchain/langgraph": { - "optional": true - }, - "ai": { - "optional": true - }, - "openai": { - "optional": true - } } }, "node_modules/@emnapi/core": { @@ -1198,6 +1175,22 @@ "win32" ] }, + "node_modules/@secureagentics/adrian": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@secureagentics/adrian-langchain": { + "resolved": "packages/langchain", + "link": true + }, + "node_modules/@secureagentics/adrian-openai": { + "resolved": "packages/openai", + "link": true + }, + "node_modules/@secureagentics/adrian-vercel": { + "resolved": "packages/vercel", + "link": true + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2707,6 +2700,95 @@ "optional": true } } + }, + "packages/core": { + "name": "@secureagentics/adrian", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } + }, + "packages/langchain": { + "name": "@secureagentics/adrian-langchain", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@secureagentics/adrian": "1.0.0" + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.0", + "@langchain/langgraph": ">=0.2.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "@langchain/langgraph": { + "optional": true + } + } + }, + "packages/openai": { + "name": "@secureagentics/adrian-openai", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@secureagentics/adrian": "1.0.0" + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + }, + "peerDependencies": { + "openai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "packages/vercel": { + "name": "@secureagentics/adrian-vercel", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@secureagentics/adrian": "1.0.0" + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + }, + "peerDependencies": { + "ai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "ai": { + "optional": true + } + } } } } diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 4995175..03f3bbb 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,64 +1,13 @@ { - "name": "@secureagentics/adrian", - "version": "1.0.0", - "description": "Multi-agent security monitoring SDK for LangChain.js / LangGraph.js.", - "license": "Apache-2.0", - "author": "Secure Agentics ", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist", - "README.md" + "name": "@secureagentics/adrian-workspace", + "private": true, + "workspaces": [ + "packages/*" ], "scripts": { - "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "test": "vitest run" - }, - "keywords": [ - "langchain", - "langgraph", - "ai", - "agents", - "security", - "monitoring", - "observability", - "llm", - "multi-agent", - "prompt-injection" - ], - "peerDependencies": { - "ai": ">=4.0.0", - "@langchain/core": ">=0.3.0", - "@langchain/langgraph": ">=0.2.0", - "openai": ">=4.0.0" - }, - "peerDependenciesMeta": { - "ai": { - "optional": true - }, - "@langchain/core": { - "optional": true - }, - "@langchain/langgraph": { - "optional": true - }, - "openai": { - "optional": true - } - }, - "dependencies": { - "protobufjs": "^8.4.2", - "ws": "^8.20.1" + "build": "npm run build --workspaces --if-present", + "typecheck": "npm run typecheck --workspaces --if-present", + "test": "npm run test --workspaces --if-present" }, "devDependencies": { "@types/node": "^25.9.1", diff --git a/sdk/typescript/packages/core/README.md b/sdk/typescript/packages/core/README.md new file mode 100644 index 0000000..d971aca --- /dev/null +++ b/sdk/typescript/packages/core/README.md @@ -0,0 +1,63 @@ +# @secureagentics/adrian + +Core TypeScript SDK for Adrian multi-agent security monitoring in Node.js. + +Handles the event pipeline: callback handler, event pairing, PII redaction, JSONL logging, WebSocket streaming, MCP inventory, and BLOCK/HITL tool gating. + +## Install + +You usually do **not** need to install this package directly. Provider packages install it automatically: + +```bash +npm install @secureagentics/adrian-openai openai # includes @secureagentics/adrian +``` + +Install core on its own only if you are wiring callbacks manually or building a custom integration: + +```bash +npm install @secureagentics/adrian +``` + +## Quick start (via a provider package) + +```ts +import { init, shutdown, adrian } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); +const openai = adrian(new OpenAI()); +await shutdown(); +``` + +## Core exports + +| Export | Description | +|---|---| +| `init(options?)` | Initialise the SDK | +| `shutdown()` | Flush handlers and tear down | +| `getHandler()` | Access the callback handler for manual wiring | +| `getWebSocketClient()` | Access the WebSocket client | +| `AdrianCallbackHandler` | LangChain-compatible callback class | +| `JSONLHandler` | Local JSONL event sink | + +## Environment + +- `ADRIAN_API_KEY` +- `ADRIAN_LOG_FILE` +- `ADRIAN_WS_URL` +- `ADRIAN_SESSION_ID` +- `ADRIAN_BLOCK_TIMEOUT` +- `ADRIAN_REPLAY_BUFFER_FRAMES` + +## Manual callback wiring + +```ts +import { init, getHandler } from "@secureagentics/adrian"; + +await init(); +const handler = getHandler(); +// Pass handler into your framework's callback system. +``` + +## Subpath export + +`@secureagentics/adrian/capture` exposes shared LLM capture helpers used internally by the OpenAI and Vercel provider packages. Most apps should use `adrian()` from a provider package instead. diff --git a/sdk/typescript/packages/core/package.json b/sdk/typescript/packages/core/package.json new file mode 100644 index 0000000..336e160 --- /dev/null +++ b/sdk/typescript/packages/core/package.json @@ -0,0 +1,53 @@ +{ + "name": "@secureagentics/adrian", + "version": "1.0.0", + "description": "Core SDK for Adrian multi-agent security monitoring in Node.js.", + "license": "Apache-2.0", + "author": "Secure Agentics ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./capture": { + "types": "./dist/capture/index.d.ts", + "import": "./dist/capture/index.js", + "require": "./dist/capture/index.cjs" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts src/capture/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "ai", + "agents", + "security", + "monitoring", + "observability", + "llm", + "multi-agent", + "prompt-injection" + ], + "dependencies": { + "protobufjs": "^8.4.2", + "ws": "^8.20.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/src/instrumentation/common.ts b/sdk/typescript/packages/core/src/capture/common.ts similarity index 100% rename from sdk/typescript/src/instrumentation/common.ts rename to sdk/typescript/packages/core/src/capture/common.ts diff --git a/sdk/typescript/packages/core/src/capture/index.ts b/sdk/typescript/packages/core/src/capture/index.ts new file mode 100644 index 0000000..e9db7b2 --- /dev/null +++ b/sdk/typescript/packages/core/src/capture/index.ts @@ -0,0 +1 @@ +export * from "./common.js"; diff --git a/sdk/typescript/src/config.ts b/sdk/typescript/packages/core/src/config.ts similarity index 98% rename from sdk/typescript/src/config.ts rename to sdk/typescript/packages/core/src/config.ts index 0e6a2e1..129f7e7 100644 --- a/sdk/typescript/src/config.ts +++ b/sdk/typescript/packages/core/src/config.ts @@ -36,7 +36,6 @@ export interface InitOptions { apiKey?: string | null; logFile?: string; handlers?: import("./types.js").EventHandler[] | null; - autoInstrument?: boolean; logLevel?: string | null; wsUrl?: string | null; sessionId?: string | null; diff --git a/sdk/typescript/src/context.ts b/sdk/typescript/packages/core/src/context.ts similarity index 100% rename from sdk/typescript/src/context.ts rename to sdk/typescript/packages/core/src/context.ts diff --git a/sdk/typescript/src/format/types.ts b/sdk/typescript/packages/core/src/format/types.ts similarity index 100% rename from sdk/typescript/src/format/types.ts rename to sdk/typescript/packages/core/src/format/types.ts diff --git a/sdk/typescript/src/handler.ts b/sdk/typescript/packages/core/src/handler.ts similarity index 100% rename from sdk/typescript/src/handler.ts rename to sdk/typescript/packages/core/src/handler.ts diff --git a/sdk/typescript/src/handlers/jsonl.ts b/sdk/typescript/packages/core/src/handlers/jsonl.ts similarity index 100% rename from sdk/typescript/src/handlers/jsonl.ts rename to sdk/typescript/packages/core/src/handlers/jsonl.ts diff --git a/sdk/typescript/src/hooks.ts b/sdk/typescript/packages/core/src/hooks.ts similarity index 100% rename from sdk/typescript/src/hooks.ts rename to sdk/typescript/packages/core/src/hooks.ts diff --git a/sdk/typescript/src/identity.ts b/sdk/typescript/packages/core/src/identity.ts similarity index 100% rename from sdk/typescript/src/identity.ts rename to sdk/typescript/packages/core/src/identity.ts diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/packages/core/src/index.ts similarity index 84% rename from sdk/typescript/src/index.ts rename to sdk/typescript/packages/core/src/index.ts index 4d8ed74..265aa1d 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/packages/core/src/index.ts @@ -6,8 +6,8 @@ import { HookRegistry } from "./hooks.js"; import { patchMcpAdapters, mcpServers } from "./mcp.js"; import { EventPairBuffer } from "./pairing.js"; import { RedactingHandler } from "./pii/index.js"; +import { getHandler, getWebSocketClient, setRuntime } from "./registry.js"; import { envAwareResolveSessionId } from "./sessionPersistence.js"; -import { autoInstrument } from "./instrumentation/langchain.js"; import { WebSocketClient } from "./ws.js"; import type { EventHandler, McpServer } from "./types.js"; @@ -15,8 +15,6 @@ export const version = "1.0.0"; export const __version__ = version; let hooks: HookRegistry | null = null; -let handler: AdrianCallbackHandler | null = null; -let wsClient: WebSocketClient | null = null; export async function init(options: InitOptions = {}): Promise { const apiKey = options.apiKey ?? process.env.ADRIAN_API_KEY ?? null; @@ -45,6 +43,7 @@ export async function init(options: InitOptions = {}): Promise { setConfig(config); const handlerList: EventHandler[] = options.handlers ? [...options.handlers] : [new JSONLHandler(logFile)]; + let wsClient: WebSocketClient | null = null; if (!options.handlers && wsUrl) { if (!apiKey) console.warn("ADRIAN wsUrl is set but no apiKey was provided; the server will reject the connection."); wsClient = new WebSocketClient({ @@ -61,34 +60,25 @@ export async function init(options: InitOptions = {}): Promise { hooks = new HookRegistry(); for (const eventHandler of handlerList.map((h) => new RedactingHandler(h))) hooks.register(eventHandler); - handler = new AdrianCallbackHandler({ pairBuffer: new EventPairBuffer(), contextTracker: new AgentContextTracker(), hooks, config }); + const handler = new AdrianCallbackHandler({ pairBuffer: new EventPairBuffer(), contextTracker: new AgentContextTracker(), hooks, config }); + setRuntime(handler, wsClient); if (wsClient) { wsClient.handler = handler; wsClient.scheduleConnect(); } await patchMcpAdapters(); - if (options.autoInstrument ?? true) await autoInstrument(getHandler, getWebSocketClient); } export async function shutdown(): Promise { await hooks?.close(); hooks = null; - handler = null; - wsClient = null; + setRuntime(null, null); setConfig(null); } -export function getHandler(): AdrianCallbackHandler | null { - return handler; -} - -export function getWebSocketClient(): WebSocketClient | null { - return wsClient; -} - async function sendMcpInventory(): Promise { - await wsClient?.sendMcpInventory(mcpServers()); + await getWebSocketClient()?.sendMcpInventory(mcpServers()); } function chainMcpServerCallback(userCallback: ((server: McpServer) => void | Promise) | null) { @@ -107,10 +97,9 @@ export { deriveAgentId, deriveLangGraphAgentId } from "./identity.js"; export { WebSocketClient, shouldHalt } from "./ws.js"; export { mcpServers, registerMcpServer, registerMcpConnection } from "./mcp.js"; export { resolveSessionId, envAwareResolveSessionId } from "./sessionPersistence.js"; +export { getHandler, getWebSocketClient } from "./registry.js"; export * from "./config.js"; export * from "./types.js"; export * from "./format/types.js"; export * from "./pii/index.js"; export * from "./proto/schema.js"; -export * from "./instrumentation/openai.js"; -export * from "./instrumentation/vercel.js"; diff --git a/sdk/typescript/src/mcp.ts b/sdk/typescript/packages/core/src/mcp.ts similarity index 100% rename from sdk/typescript/src/mcp.ts rename to sdk/typescript/packages/core/src/mcp.ts diff --git a/sdk/typescript/src/pairing.ts b/sdk/typescript/packages/core/src/pairing.ts similarity index 100% rename from sdk/typescript/src/pairing.ts rename to sdk/typescript/packages/core/src/pairing.ts diff --git a/sdk/typescript/src/pii/engine.ts b/sdk/typescript/packages/core/src/pii/engine.ts similarity index 100% rename from sdk/typescript/src/pii/engine.ts rename to sdk/typescript/packages/core/src/pii/engine.ts diff --git a/sdk/typescript/src/pii/index.ts b/sdk/typescript/packages/core/src/pii/index.ts similarity index 100% rename from sdk/typescript/src/pii/index.ts rename to sdk/typescript/packages/core/src/pii/index.ts diff --git a/sdk/typescript/src/pii/patterns.ts b/sdk/typescript/packages/core/src/pii/patterns.ts similarity index 100% rename from sdk/typescript/src/pii/patterns.ts rename to sdk/typescript/packages/core/src/pii/patterns.ts diff --git a/sdk/typescript/src/pii/redactor.ts b/sdk/typescript/packages/core/src/pii/redactor.ts similarity index 100% rename from sdk/typescript/src/pii/redactor.ts rename to sdk/typescript/packages/core/src/pii/redactor.ts diff --git a/sdk/typescript/src/pii/strategies.ts b/sdk/typescript/packages/core/src/pii/strategies.ts similarity index 100% rename from sdk/typescript/src/pii/strategies.ts rename to sdk/typescript/packages/core/src/pii/strategies.ts diff --git a/sdk/typescript/src/proto/schema.ts b/sdk/typescript/packages/core/src/proto/schema.ts similarity index 100% rename from sdk/typescript/src/proto/schema.ts rename to sdk/typescript/packages/core/src/proto/schema.ts diff --git a/sdk/typescript/packages/core/src/registry.ts b/sdk/typescript/packages/core/src/registry.ts new file mode 100644 index 0000000..191c0bb --- /dev/null +++ b/sdk/typescript/packages/core/src/registry.ts @@ -0,0 +1,18 @@ +import type { AdrianCallbackHandler } from "./handler.js"; +import type { WebSocketClient } from "./ws.js"; + +let handler: AdrianCallbackHandler | null = null; +let wsClient: WebSocketClient | null = null; + +export function getHandler(): AdrianCallbackHandler | null { + return handler; +} + +export function getWebSocketClient(): WebSocketClient | null { + return wsClient; +} + +export function setRuntime(nextHandler: AdrianCallbackHandler | null, nextWsClient: WebSocketClient | null): void { + handler = nextHandler; + wsClient = nextWsClient; +} diff --git a/sdk/typescript/src/sessionPersistence.ts b/sdk/typescript/packages/core/src/sessionPersistence.ts similarity index 100% rename from sdk/typescript/src/sessionPersistence.ts rename to sdk/typescript/packages/core/src/sessionPersistence.ts diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/packages/core/src/types.ts similarity index 100% rename from sdk/typescript/src/types.ts rename to sdk/typescript/packages/core/src/types.ts diff --git a/sdk/typescript/src/ws.ts b/sdk/typescript/packages/core/src/ws.ts similarity index 100% rename from sdk/typescript/src/ws.ts rename to sdk/typescript/packages/core/src/ws.ts diff --git a/sdk/typescript/tests/jsonl.test.ts b/sdk/typescript/packages/core/tests/jsonl.test.ts similarity index 100% rename from sdk/typescript/tests/jsonl.test.ts rename to sdk/typescript/packages/core/tests/jsonl.test.ts diff --git a/sdk/typescript/tests/pairing.test.ts b/sdk/typescript/packages/core/tests/pairing.test.ts similarity index 100% rename from sdk/typescript/tests/pairing.test.ts rename to sdk/typescript/packages/core/tests/pairing.test.ts diff --git a/sdk/typescript/tests/pii.test.ts b/sdk/typescript/packages/core/tests/pii.test.ts similarity index 100% rename from sdk/typescript/tests/pii.test.ts rename to sdk/typescript/packages/core/tests/pii.test.ts diff --git a/sdk/typescript/tests/proto.test.ts b/sdk/typescript/packages/core/tests/proto.test.ts similarity index 100% rename from sdk/typescript/tests/proto.test.ts rename to sdk/typescript/packages/core/tests/proto.test.ts diff --git a/sdk/typescript/tests/session.test.ts b/sdk/typescript/packages/core/tests/session.test.ts similarity index 100% rename from sdk/typescript/tests/session.test.ts rename to sdk/typescript/packages/core/tests/session.test.ts diff --git a/sdk/typescript/tests/ws.test.ts b/sdk/typescript/packages/core/tests/ws.test.ts similarity index 100% rename from sdk/typescript/tests/ws.test.ts rename to sdk/typescript/packages/core/tests/ws.test.ts diff --git a/sdk/typescript/tsconfig.build.json b/sdk/typescript/packages/core/tsconfig.build.json similarity index 100% rename from sdk/typescript/tsconfig.build.json rename to sdk/typescript/packages/core/tsconfig.build.json diff --git a/sdk/typescript/packages/core/tsconfig.json b/sdk/typescript/packages/core/tsconfig.json new file mode 100644 index 0000000..6140ae3 --- /dev/null +++ b/sdk/typescript/packages/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/langchain/README.md b/sdk/typescript/packages/langchain/README.md new file mode 100644 index 0000000..004b1d2 --- /dev/null +++ b/sdk/typescript/packages/langchain/README.md @@ -0,0 +1,57 @@ +# @secureagentics/adrian-langchain + +LangChain.js and LangGraph.js instrumentation for Adrian security monitoring. Includes `@secureagentics/adrian` as a dependency — no separate core install needed. + +## Install + +```bash +npm install @secureagentics/adrian-langchain @langchain/core @langchain/langgraph +``` + +```ts +import { init, adrian } from "@secureagentics/adrian-langchain"; +``` + +## Usage + +```ts +import { init, shutdown, adrian } from "@secureagentics/adrian-langchain"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); +await adrian(); + +// Your normal LangChain / LangGraph code runs here. + +await shutdown(); +``` + +After `adrian()`: + +- LangChain runnables and chat models receive the Adrian callback handler automatically. +- LangGraph graphs are wrapped with invocation tracking. +- ToolNode execution waits for BLOCK/HITL verdicts from the Adrian WebSocket. + +Unlike the OpenAI and Vercel packages, LangChain does not wrap a client object. Call `adrian()` once after `init()` to patch the framework globally. + +## Advanced: custom handler wiring + +```ts +import { init, getHandler, getWebSocketClient, adrianWith } from "@secureagentics/adrian-langchain"; + +await init(); +await adrianWith(getHandler, getWebSocketClient); +``` + +## API + +| Export | Description | +|---|---| +| `init(options?)` | Initialise Adrian (re-exported from core) | +| `shutdown()` | Tear down Adrian (re-exported from core) | +| `adrian()` | Enable LangChain / LangGraph instrumentation | +| `adrianWith(getHandler, getWebSocketClient)` | Enable instrumentation with custom handler accessors | +| `blockedToolNodeResponse(input, ws)` | Check ToolNode input against policy (used internally) | +| `extractToolCalls(input)` | Extract tool calls from graph state | +| `buildBlockedResponse(toolCalls)` | Build a blocked tool response message | + +See the [workspace README](../README.md) for environment variables and multi-provider setup. diff --git a/sdk/typescript/packages/langchain/package.json b/sdk/typescript/packages/langchain/package.json new file mode 100644 index 0000000..eb0c153 --- /dev/null +++ b/sdk/typescript/packages/langchain/package.json @@ -0,0 +1,57 @@ +{ + "name": "@secureagentics/adrian-langchain", + "version": "1.0.0", + "description": "LangChain.js and LangGraph.js instrumentation for Adrian security monitoring.", + "license": "Apache-2.0", + "author": "Secure Agentics ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "langchain", + "langgraph", + "ai", + "agents", + "security", + "monitoring" + ], + "dependencies": { + "@secureagentics/adrian": "^1.0.0" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.0", + "@langchain/langgraph": ">=0.2.0" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "@langchain/langgraph": { + "optional": true + } + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/src/instrumentation/langchain.ts b/sdk/typescript/packages/langchain/src/index.ts similarity index 76% rename from sdk/typescript/src/instrumentation/langchain.ts rename to sdk/typescript/packages/langchain/src/index.ts index 8c1e25a..0015615 100644 --- a/sdk/typescript/src/instrumentation/langchain.ts +++ b/sdk/typescript/packages/langchain/src/index.ts @@ -1,25 +1,38 @@ import { randomUUID } from "node:crypto"; -import type { AdrianCallbackHandler } from "../handler.js"; -import { runWithInvocationId } from "../context.js"; -import { shouldHalt, type WebSocketClient } from "../ws.js"; -import { currentConfig } from "../config.js"; +import { currentConfig, getHandler, getWebSocketClient, runWithInvocationId, shouldHalt } from "@secureagentics/adrian"; +import type { AdrianCallbackHandler, WebSocketClient } from "@secureagentics/adrian"; let patched = false; const CALLBACK_METHODS = ["invoke", "stream", "batch", "ainvoke", "astream"] as const; const GRAPH_METHODS = ["invoke", "stream", "ainvoke", "astream"] as const; const TOOL_NODE_METHODS = ["invoke", "ainvoke"] as const; -export async function autoInstrument(getHandler: () => AdrianCallbackHandler | null, getWebSocketClient: () => WebSocketClient | null): Promise { +/** Enable Adrian instrumentation for LangChain.js and LangGraph.js. */ +export async function adrian(): Promise { + return adrianWith(getHandler, getWebSocketClient); +} + +export async function adrianWith( + getHandlerFn: () => AdrianCallbackHandler | null, + getWebSocketClientFn: () => WebSocketClient | null, +): Promise { if (patched) return; patched = true; await Promise.allSettled([ - patchRunnable(getHandler), - patchBaseChatModel(getHandler), - patchLangGraph(getHandler), - patchToolNode(getHandler, getWebSocketClient), + patchRunnable(getHandlerFn), + patchBaseChatModel(getHandlerFn), + patchLangGraph(getHandlerFn), + patchToolNode(getHandlerFn, getWebSocketClientFn), ]); } +/** @deprecated Use adrian instead */ +export const instrumentLangChain = adrian; +/** @deprecated Use adrianWith instead */ +export const instrumentLangChainWith = adrianWith; +/** @deprecated Use adrian instead */ +export const autoInstrument = adrian; + function injectCallbacks(config: unknown, handler: AdrianCallbackHandler | null): unknown { if (!handler) return config ?? {}; const next = { ...((config && typeof config === "object") ? config as Record : {}) }; @@ -34,7 +47,7 @@ function injectCallbacks(config: unknown, handler: AdrianCallbackHandler | null) return next; } -async function patchRunnable(getHandler: () => AdrianCallbackHandler | null): Promise { +async function patchRunnable(getHandlerFn: () => AdrianCallbackHandler | null): Promise { const mod = await importOptional("@langchain/core/runnables"); const proto = (mod?.Runnable as { prototype?: Record; _adrianPatched?: boolean } | undefined)?.prototype; if (!proto || (mod.Runnable as { _adrianPatched?: boolean })._adrianPatched) return; @@ -42,13 +55,13 @@ async function patchRunnable(getHandler: () => AdrianCallbackHandler | null): Pr const original = proto[name]; if (typeof original !== "function") continue; proto[name] = function patchedInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - return original.call(this, input, injectCallbacks(config, getHandler()), ...rest); + return original.call(this, input, injectCallbacks(config, getHandlerFn()), ...rest); }; } (mod.Runnable as { _adrianPatched?: boolean })._adrianPatched = true; } -async function patchBaseChatModel(getHandler: () => AdrianCallbackHandler | null): Promise { +async function patchBaseChatModel(getHandlerFn: () => AdrianCallbackHandler | null): Promise { const mod = await importOptional("@langchain/core/language_models/chat_models"); const proto = (mod?.BaseChatModel as { prototype?: Record; _adrianPatched?: boolean } | undefined)?.prototype; if (!proto || (mod.BaseChatModel as { _adrianPatched?: boolean })._adrianPatched) return; @@ -56,13 +69,13 @@ async function patchBaseChatModel(getHandler: () => AdrianCallbackHandler | null const original = proto[name]; if (typeof original !== "function") continue; proto[name] = function patchedChatInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - return original.call(this, input, injectCallbacks(config, getHandler()), ...rest); + return original.call(this, input, injectCallbacks(config, getHandlerFn()), ...rest); }; } (mod.BaseChatModel as { _adrianPatched?: boolean })._adrianPatched = true; } -async function patchLangGraph(getHandler: () => AdrianCallbackHandler | null): Promise { +async function patchLangGraph(getHandlerFn: () => AdrianCallbackHandler | null): Promise { const mod = await importOptional("@langchain/langgraph"); const graphClasses = [mod?.CompiledStateGraph, mod?.StateGraph, mod?.Pregel].filter(Boolean) as Array<{ prototype?: Record; _adrianPatched?: boolean }>; for (const cls of graphClasses) { @@ -72,7 +85,7 @@ async function patchLangGraph(getHandler: () => AdrianCallbackHandler | null): P const original = proto[name]; if (typeof original !== "function") continue; proto[name] = function patchedGraphInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - const run = () => original.call(this, input, injectCallbacks(config, getHandler()), ...rest); + const run = () => original.call(this, input, injectCallbacks(config, getHandlerFn()), ...rest); return runWithInvocationId(randomUUID(), run); }; } @@ -80,7 +93,7 @@ async function patchLangGraph(getHandler: () => AdrianCallbackHandler | null): P } } -async function patchToolNode(getHandler: () => AdrianCallbackHandler | null, getWebSocketClient: () => WebSocketClient | null): Promise { +async function patchToolNode(getHandlerFn: () => AdrianCallbackHandler | null, getWebSocketClientFn: () => WebSocketClient | null): Promise { const mod = await importOptional("@langchain/langgraph/prebuilt"); const cls = mod?.ToolNode as { prototype?: Record; _adrianPatched?: boolean } | undefined; const proto = cls?.prototype; @@ -89,8 +102,8 @@ async function patchToolNode(getHandler: () => AdrianCallbackHandler | null, get const original = proto[name]; if (typeof original !== "function") continue; proto[name] = async function patchedToolNodeInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - const nextConfig = injectCallbacks(config, getHandler()); - const blockedResponse = await blockedToolNodeResponse(input, getWebSocketClient()); + const nextConfig = injectCallbacks(config, getHandlerFn()); + const blockedResponse = await blockedToolNodeResponse(input, getWebSocketClientFn()); if (blockedResponse) return blockedResponse; return original.call(this, input, nextConfig, ...rest); }; @@ -134,3 +147,14 @@ async function importOptional(specifier: string): Promise { return null; } } + +export { + init, + shutdown, + getHandler, + getWebSocketClient, + version, + __version__, +} from "@secureagentics/adrian"; + +export type { EventData, InitOptions } from "@secureagentics/adrian"; diff --git a/sdk/typescript/tests/langchain.test.ts b/sdk/typescript/packages/langchain/tests/langchain.test.ts similarity index 90% rename from sdk/typescript/tests/langchain.test.ts rename to sdk/typescript/packages/langchain/tests/langchain.test.ts index ed609c9..8ba0764 100644 --- a/sdk/typescript/tests/langchain.test.ts +++ b/sdk/typescript/packages/langchain/tests/langchain.test.ts @@ -1,8 +1,7 @@ import { afterEach, describe, expect, it } from "vitest"; -import { setConfig } from "../src/config.js"; -import { blockedToolNodeResponse } from "../src/instrumentation/langchain.js"; -import { Mode, type Verdict } from "../src/proto/schema.js"; -import type { WebSocketClient } from "../src/ws.js"; +import { init, shutdown, type EventData } from "@secureagentics/adrian"; +import { setConfig, Mode, type Verdict, type WebSocketClient } from "@secureagentics/adrian"; +import { blockedToolNodeResponse } from "../src/index.js"; function config(): Parameters[0] { return { diff --git a/sdk/typescript/packages/langchain/tsconfig.build.json b/sdk/typescript/packages/langchain/tsconfig.build.json new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/sdk/typescript/packages/langchain/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/langchain/tsconfig.json b/sdk/typescript/packages/langchain/tsconfig.json new file mode 100644 index 0000000..6140ae3 --- /dev/null +++ b/sdk/typescript/packages/langchain/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/openai/README.md b/sdk/typescript/packages/openai/README.md new file mode 100644 index 0000000..2e5e732 --- /dev/null +++ b/sdk/typescript/packages/openai/README.md @@ -0,0 +1,70 @@ +# @secureagentics/adrian-openai + +OpenAI SDK instrumentation for Adrian security monitoring. Includes `@secureagentics/adrian` as a dependency — no separate core install needed. + +## Install + +```bash +npm install @secureagentics/adrian-openai openai +``` + +```ts +import { init, adrian, captureTool } from "@secureagentics/adrian-openai"; +``` + +## Usage + +```ts +import OpenAI from "openai"; +import { init, shutdown, adrian, captureTool } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const openai = adrian(new OpenAI()); + +await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Hello" }], +}); + +await shutdown(); +``` + +`adrian()` wraps `chat.completions.create`, `responses.create`, and their streaming variants. Events are paired, PII-redacted, and streamed to Adrian automatically. + +## Tool execution capture + +OpenAI returns tool call requests; your app executes the tools. Wrap that execution with `captureTool`: + +```ts +for (const toolCall of assistantMessage.tool_calls ?? []) { + const toolResult = await captureTool(toolCall, () => + runTool(toolCall.function.name, toolCall.function.arguments), + ); + + messages.push({ + role: "tool", + tool_call_id: toolCall.id, + content: toolResult, + }); +} +``` + +## API + +| Export | Description | +|---|---| +| `init(options?)` | Initialise Adrian (re-exported from core) | +| `shutdown()` | Tear down Adrian (re-exported from core) | +| `adrian(client, options?)` | Wrap an OpenAI client | +| `captureTool(toolCall, execute, options?)` | Capture manual tool execution | + +### Types + +| Type | Fields | +|---|---| +| `AdrianOptions` | `metadata?` | +| `ToolCallLike` | `id`, `function.name`, `function.arguments`, … | +| `ToolCaptureOptions` | `metadata?`, `parentRunId?` | + +See the [workspace README](../README.md) for environment variables and multi-provider setup. diff --git a/sdk/typescript/packages/openai/package.json b/sdk/typescript/packages/openai/package.json new file mode 100644 index 0000000..40cd21c --- /dev/null +++ b/sdk/typescript/packages/openai/package.json @@ -0,0 +1,52 @@ +{ + "name": "@secureagentics/adrian-openai", + "version": "1.0.0", + "description": "OpenAI SDK instrumentation for Adrian security monitoring.", + "license": "Apache-2.0", + "author": "Secure Agentics ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "openai", + "ai", + "agents", + "security", + "monitoring" + ], + "dependencies": { + "@secureagentics/adrian": "^1.0.0" + }, + "peerDependencies": { + "openai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/src/instrumentation/openai.ts b/sdk/typescript/packages/openai/src/index.ts similarity index 88% rename from sdk/typescript/src/instrumentation/openai.ts rename to sdk/typescript/packages/openai/src/index.ts index 89258c5..c4a6c88 100644 --- a/sdk/typescript/src/instrumentation/openai.ts +++ b/sdk/typescript/packages/openai/src/index.ts @@ -1,7 +1,6 @@ import { randomUUID } from "node:crypto"; -import { runWithInvocationId } from "../context.js"; -import { getHandler } from "../index.js"; -import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "../types.js"; +import { getHandler, runWithInvocationId } from "@secureagentics/adrian"; +import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "@secureagentics/adrian"; import { captureLlmAsyncIterable, captureLlmCall, @@ -11,13 +10,13 @@ import { normalizeUsage, parseToolArgs, stringifyContent, -} from "./common.js"; +} from "@secureagentics/adrian/capture"; -export interface OpenAIInstrumentationOptions { +export interface AdrianOptions { metadata?: CallbackMetadata | null; } -export interface OpenAIToolCallLike { +export interface ToolCallLike { id: string; type?: string; function?: { @@ -28,12 +27,13 @@ export interface OpenAIToolCallLike { arguments?: string; } -export interface OpenAIToolCallCaptureOptions { +export interface ToolCaptureOptions { metadata?: CallbackMetadata | null; parentRunId?: string; } -export function instrumentOpenAI(client: T, options: OpenAIInstrumentationOptions = {}): T { +/** Wrap an OpenAI client so Adrian captures LLM and tool events. */ +export function adrian(client: T, options: AdrianOptions = {}): T { return new Proxy(client, { get(target, prop, receiver) { if (prop === "chat") return instrumentChat(Reflect.get(target, prop, receiver), options); @@ -43,12 +43,11 @@ export function instrumentOpenAI(client: T, options: OpenAIIns }); } -export const withAdrianOpenAI = instrumentOpenAI; - -export async function captureOpenAIToolCall( - toolCall: OpenAIToolCallLike, +/** Wrap manual OpenAI tool execution so Adrian captures tool events. */ +export async function captureTool( + toolCall: ToolCallLike, execute: () => T | Promise, - options: OpenAIToolCallCaptureOptions = {}, + options: ToolCaptureOptions = {}, ): Promise { const handler = getHandler(); if (!handler) return execute(); @@ -72,7 +71,18 @@ export async function captureOpenAIToolCall( }); } -function instrumentChat(chat: unknown, options: OpenAIInstrumentationOptions): unknown { +/** @deprecated Use adrian instead */ +export const instrumentOpenAI = adrian; +/** @deprecated Use adrian instead */ +export const instrument = adrian; +/** @deprecated Use adrian instead */ +export const withAdrianOpenAI = adrian; +/** @deprecated Use captureTool instead */ +export const captureOpenAITool = captureTool; +/** @deprecated Use captureTool instead */ +export const captureOpenAIToolCall = captureTool; + +function instrumentChat(chat: unknown, options: AdrianOptions): unknown { if (!chat || typeof chat !== "object") return chat; return new Proxy(chat as Record, { get(target, prop, receiver) { @@ -82,7 +92,7 @@ function instrumentChat(chat: unknown, options: OpenAIInstrumentationOptions): u }); } -function instrumentChatCompletions(completions: unknown, options: OpenAIInstrumentationOptions): unknown { +function instrumentChatCompletions(completions: unknown, options: AdrianOptions): unknown { if (!completions || typeof completions !== "object") return completions; return new Proxy(completions as Record, { get(target, prop, receiver) { @@ -103,7 +113,7 @@ function instrumentChatCompletions(completions: unknown, options: OpenAIInstrume }); } -function instrumentResponses(responses: unknown, options: OpenAIInstrumentationOptions): unknown { +function instrumentResponses(responses: unknown, options: AdrianOptions): unknown { if (!responses || typeof responses !== "object") return responses; return new Proxy(responses as Record, { get(target, prop, receiver) { @@ -226,3 +236,14 @@ function integrationMetadata(metadata: CallbackMetadata | null | undefined, oper function isAsyncIterable(value: unknown): value is AsyncIterable { return Boolean(value && typeof value === "object" && Symbol.asyncIterator in value); } + +export { + init, + shutdown, + getHandler, + getWebSocketClient, + version, + __version__, +} from "@secureagentics/adrian"; + +export type { EventData, InitOptions } from "@secureagentics/adrian"; diff --git a/sdk/typescript/tests/openai.test.ts b/sdk/typescript/packages/openai/tests/openai.test.ts similarity index 80% rename from sdk/typescript/tests/openai.test.ts rename to sdk/typescript/packages/openai/tests/openai.test.ts index 0e26df0..2596e48 100644 --- a/sdk/typescript/tests/openai.test.ts +++ b/sdk/typescript/packages/openai/tests/openai.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; -import { captureOpenAIToolCall, init, instrumentOpenAI, shutdown } from "../src/index.js"; -import type { EventData } from "../src/types.js"; +import { init, shutdown, type EventData } from "@secureagentics/adrian"; +import { captureTool, adrian } from "../src/index.js"; describe("OpenAI instrumentation", () => { afterEach(async () => { @@ -9,7 +9,7 @@ describe("OpenAI instrumentation", () => { it("captures chat completion calls as paired LLM events", async () => { const events: EventData[] = []; - const client = instrumentOpenAI({ + const client = adrian({ chat: { completions: { create: async (_body: Record) => ({ @@ -28,7 +28,7 @@ describe("OpenAI instrumentation", () => { }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); const result = await client.chat.completions.create({ @@ -49,7 +49,7 @@ describe("OpenAI instrumentation", () => { it("captures responses API calls", async () => { const events: EventData[] = []; - const client = instrumentOpenAI({ + const client = adrian({ responses: { create: async (_body: Record) => ({ output_text: "done", @@ -59,7 +59,7 @@ describe("OpenAI instrumentation", () => { }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); await client.responses.create({ model: "gpt-4.1", input: "run lookup" }); @@ -74,7 +74,7 @@ describe("OpenAI instrumentation", () => { it("captures camelCase chat tool calls", async () => { const events: EventData[] = []; - const client = instrumentOpenAI({ + const client = adrian({ chat: { completions: { create: async (_body: Record) => ({ @@ -93,7 +93,7 @@ describe("OpenAI instrumentation", () => { }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "use search" }] }); @@ -114,13 +114,13 @@ describe("OpenAI instrumentation", () => { yield { type: "response.function_call_arguments.delta", item_id: "item-1", delta: ":7}" }; yield { type: "response.output_text.delta", delta: "is ready." }; } - const client = instrumentOpenAI({ + const client = adrian({ responses: { create: async (_body: Record) => stream(), }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); const result = await client.responses.create({ model: "gpt-4.1", input: "lookup id 7", stream: true }); @@ -142,7 +142,7 @@ describe("OpenAI instrumentation", () => { yield { choices: [{ delta: { content: "first " } }] }; yield { choices: [{ delta: { content: "second" } }] }; } - const client = instrumentOpenAI({ + const client = adrian({ chat: { completions: { create: async (_body: Record) => stream(), @@ -150,7 +150,7 @@ describe("OpenAI instrumentation", () => { }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); const result = await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "stream" }], stream: true }); @@ -167,11 +167,11 @@ describe("OpenAI instrumentation", () => { it("captures local OpenAI tool execution as a tool event", async () => { const events: Array<{ type: string; data: EventData }> = []; - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { events.push({ type, data }); } }); - const result = await captureOpenAIToolCall({ + const result = await captureTool({ id: "call-weather", type: "function", function: { name: "get_weather", arguments: "{\"city\":\"San Francisco\"}" }, @@ -193,11 +193,11 @@ describe("OpenAI instrumentation", () => { it("captures local OpenAI tool execution errors as tool events", async () => { const events: Array<{ type: string; data: EventData }> = []; - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { events.push({ type, data }); } }); - await expect(captureOpenAIToolCall({ + await expect(captureTool({ id: "call-weather", type: "function", function: { name: "get_weather", arguments: "{\"city\":\"San Francisco\"}" }, @@ -219,7 +219,7 @@ describe("OpenAI instrumentation", () => { it("captures OpenAI request errors as LLM events", async () => { const events: Array<{ type: string; data: EventData }> = []; - const client = instrumentOpenAI({ + const client = adrian({ chat: { completions: { create: async (_body: Record) => { @@ -229,7 +229,7 @@ describe("OpenAI instrumentation", () => { }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { events.push({ type, data }); } }); @@ -248,4 +248,29 @@ describe("OpenAI instrumentation", () => { }, }); }); + + it("wraps OpenAI client via adrian()", async () => { + const events: EventData[] = []; + const client = adrian({ + chat: { + completions: { + create: async (_body: Record) => ({ + choices: [{ message: { content: "hello" } }], + }), + }, + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + + await client.chat.completions.create({ + model: "gpt-4o", + messages: [{ role: "user", content: "hi" }], + }); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ model: "gpt-4o", output: "hello" }); + }); }); diff --git a/sdk/typescript/packages/openai/tsconfig.build.json b/sdk/typescript/packages/openai/tsconfig.build.json new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/sdk/typescript/packages/openai/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/openai/tsconfig.json b/sdk/typescript/packages/openai/tsconfig.json new file mode 100644 index 0000000..6140ae3 --- /dev/null +++ b/sdk/typescript/packages/openai/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/vercel/README.md b/sdk/typescript/packages/vercel/README.md new file mode 100644 index 0000000..7f906e3 --- /dev/null +++ b/sdk/typescript/packages/vercel/README.md @@ -0,0 +1,92 @@ +# @secureagentics/adrian-vercel + +Vercel AI SDK instrumentation for Adrian security monitoring. Includes `@secureagentics/adrian` as a dependency — no separate core install needed. + +## Install + +```bash +npm install @secureagentics/adrian-vercel ai +``` + +```ts +import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; +``` + +## Usage + +```ts +import * as ai from "ai"; +import { init, shutdown, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const ai = adrian(ai); + +await ai.generateText({ + model, + prompt: "Hello", +}); + +await shutdown(); +``` + +`adrian()` wraps `generateText`, `streamText`, `generateObject`, and `streamObject`. + +## Tools + +Pass wrapped tool definitions to `generateText`: + +```ts +const result = await ai.generateText({ + model, + prompt: "Use the weather tool.", + tools: adrianTools(tools), +}); +``` + +`adrian()` also accepts a plain tools object directly (auto-detected when every key has `execute` or `description`): + +```ts +const tools = adrian({ + getWeather: { + description: "Get weather for a city", + execute: async ({ city }) => ({ city, temp: 72 }), + }, +}); + +await tools.getWeather.execute({ city: "London" }); +``` + +## Manual tool capture + +When you execute Vercel AI tool calls yourself: + +```ts +for (const toolCall of result.toolCalls ?? []) { + await captureTool(toolCall, () => runTool(toolCall.toolName, toolCall.args)); +} +``` + +## API + + +| Export | Description | +| ------------------------------------------ | ---------------------------------------------- | +| `init(options?)` | Initialise Adrian (re-exported from core) | +| `shutdown()` | Tear down Adrian (re-exported from core) | +| `adrian(target, options?)` | Wrap a Vercel AI module or tools object | +| `adrianTools(tools, options?)` | Wrap tool definitions passed to `generateText` | +| `captureTool(toolCall, execute, options?)` | Capture manual tool execution | + + +### Types + + +| Type | Fields | +| -------------------- | ----------------------------------- | +| `AdrianOptions` | `metadata?` | +| `ToolCallLike` | `toolCallId`, `toolName`, `args`, … | +| `ToolCaptureOptions` | `metadata?`, `parentRunId?` | + + +See the [workspace README](../README.md) for environment variables and multi-provider setup. diff --git a/sdk/typescript/packages/vercel/package.json b/sdk/typescript/packages/vercel/package.json new file mode 100644 index 0000000..3f3a2a8 --- /dev/null +++ b/sdk/typescript/packages/vercel/package.json @@ -0,0 +1,52 @@ +{ + "name": "@secureagentics/adrian-vercel", + "version": "1.0.0", + "description": "Vercel AI SDK instrumentation for Adrian security monitoring.", + "license": "Apache-2.0", + "author": "Secure Agentics ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "keywords": [ + "vercel-ai", + "ai", + "agents", + "security", + "monitoring" + ], + "dependencies": { + "@secureagentics/adrian": "^1.0.0" + }, + "peerDependencies": { + "ai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "ai": { + "optional": true + } + }, + "devDependencies": { + "@secureagentics/adrian": "file:../core", + "@types/node": "^25.9.1", + "tsup": "^8.5.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7" + } +} diff --git a/sdk/typescript/src/instrumentation/vercel.ts b/sdk/typescript/packages/vercel/src/index.ts similarity index 70% rename from sdk/typescript/src/instrumentation/vercel.ts rename to sdk/typescript/packages/vercel/src/index.ts index f70b1d3..fafacec 100644 --- a/sdk/typescript/src/instrumentation/vercel.ts +++ b/sdk/typescript/packages/vercel/src/index.ts @@ -1,14 +1,13 @@ import { randomUUID } from "node:crypto"; -import { runWithInvocationId } from "../context.js"; -import { getHandler } from "../index.js"; -import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "../types.js"; -import { captureLlmCall, emptyLlmEnd, messagesFromPromptLike, normalizeUsage, parseToolArgs, stringifyContent } from "./common.js"; +import { getHandler, runWithInvocationId } from "@secureagentics/adrian"; +import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "@secureagentics/adrian"; +import { captureLlmCall, emptyLlmEnd, messagesFromPromptLike, normalizeUsage, parseToolArgs, stringifyContent } from "@secureagentics/adrian/capture"; -export interface VercelAIInstrumentationOptions { +export interface AdrianOptions { metadata?: CallbackMetadata | null; } -export interface VercelAIToolCallLike { +export interface ToolCallLike { toolCallId?: string; id?: string; toolName?: string; @@ -16,7 +15,7 @@ export interface VercelAIToolCallLike { args?: unknown; } -export interface VercelAIToolCallCaptureOptions { +export interface ToolCaptureOptions { metadata?: CallbackMetadata | null; parentRunId?: string; } @@ -25,21 +24,27 @@ type VercelToolExecute = (args: unknown, options?: unknown, ...rest: unknown[]) const VERCEL_METHODS = new Set(["generateText", "streamText", "generateObject", "streamObject"]); -export function instrumentVercelAI>(ai: T, options: VercelAIInstrumentationOptions = {}): T { - return new Proxy(ai, { - get(target, prop, receiver) { - const value = Reflect.get(target, prop, receiver); - if (!VERCEL_METHODS.has(String(prop)) || typeof value !== "function") return value; - return function adrianVercelAI(this: unknown, args: Record = {}, ...rest: unknown[]) { - return captureVercelCall(String(prop), () => value.call(this, args, ...rest), args, options); - }; - }, - }); -} +/** Wrap a Vercel AI SDK module or tools object so Adrian captures LLM and tool events. */ +export function adrian(target: T, options: AdrianOptions = {}): T { + if (!target || typeof target !== "object") return target; -export const withAdrianVercelAI = instrumentVercelAI; + if ("generateText" in target || "streamText" in target) { + return wrapAiModule(target as Record, options) as T; + } -export function instrumentVercelAITools>(tools: T, options: VercelAIToolCallCaptureOptions = {}): T { + const keys = Object.keys(target); + if (keys.length > 0 && keys.every((k) => { + const val = (target as Record)[k]; + return val && typeof val === "object" && ("execute" in val || "description" in val); + })) { + return adrianTools(target as Record, options) as T; + } + + return target; +} + +/** Wrap Vercel AI SDK tool definitions so Adrian captures tool execution events. */ +export function adrianTools>(tools: T, options: ToolCaptureOptions = {}): T { return Object.fromEntries(Object.entries(tools).map(([toolName, toolDef]) => { if (!toolDef || typeof toolDef !== "object") return [toolName, toolDef]; const execute = (toolDef as { execute?: unknown }).execute; @@ -49,7 +54,7 @@ export function instrumentVercelAITools>(tools ...(toolDef as Record), execute(this: unknown, args: unknown, executionOptions?: unknown, ...rest: unknown[]) { const toolCallId = extractToolCallId(executionOptions); - return captureVercelAIToolCall({ + return captureTool({ toolCallId, toolName, args, @@ -59,10 +64,11 @@ export function instrumentVercelAITools>(tools })) as T; } -export async function captureVercelAIToolCall( - toolCall: VercelAIToolCallLike, +/** Wrap manual Vercel AI SDK tool execution so Adrian captures tool events. */ +export async function captureTool( + toolCall: ToolCallLike, execute: () => T | Promise, - options: VercelAIToolCallCaptureOptions = {}, + options: ToolCaptureOptions = {}, ): Promise { const handler = getHandler(); if (!handler) return execute(); @@ -86,9 +92,34 @@ export async function captureVercelAIToolCall( }); } -export const captureAITool = captureVercelAIToolCall; +/** @deprecated Use adrian instead */ +export const instrumentVercelAI = adrian; +/** @deprecated Use adrian instead */ +export const instrument = adrian; +/** @deprecated Use adrian instead */ +export const withAdrianVercelAI = adrian; +/** @deprecated Use adrianTools instead */ +export const instrumentVercelAITools = adrianTools; +/** @deprecated Use captureTool instead */ +export const captureVercelAITool = captureTool; +/** @deprecated Use captureTool instead */ +export const captureVercelAIToolCall = captureTool; +/** @deprecated Use captureTool instead */ +export const captureAITool = captureTool; + +function wrapAiModule>(ai: T, options: AdrianOptions = {}): T { + return new Proxy(ai, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (!VERCEL_METHODS.has(String(prop)) || typeof value !== "function") return value; + return function adrianVercelAI(this: unknown, args: Record = {}, ...rest: unknown[]) { + return captureVercelCall(String(prop), () => value.call(this, args, ...rest), args, options); + }; + }, + }); +} -function captureVercelCall(operation: string, execute: () => T, args: Record, options: VercelAIInstrumentationOptions): unknown { +function captureVercelCall(operation: string, execute: () => T, args: Record, options: AdrianOptions): unknown { const model = extractModelName(args.model); const metadata = integrationMetadata(options.metadata, operation); const result = execute(); @@ -158,3 +189,14 @@ function extractToolCallId(executionOptions: unknown): string { function integrationMetadata(metadata: CallbackMetadata | null | undefined, operation: string): CallbackMetadata { return { ...(metadata ?? {}), adrianIntegration: "vercel-ai", operation }; } + +export { + init, + shutdown, + getHandler, + getWebSocketClient, + version, + __version__, +} from "@secureagentics/adrian"; + +export type { EventData, InitOptions } from "@secureagentics/adrian"; diff --git a/sdk/typescript/tests/vercel.test.ts b/sdk/typescript/packages/vercel/tests/vercel.test.ts similarity index 68% rename from sdk/typescript/tests/vercel.test.ts rename to sdk/typescript/packages/vercel/tests/vercel.test.ts index 00cc887..3054ce6 100644 --- a/sdk/typescript/tests/vercel.test.ts +++ b/sdk/typescript/packages/vercel/tests/vercel.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; -import { captureAITool, captureVercelAIToolCall, init, instrumentVercelAI, instrumentVercelAITools, shutdown } from "../src/index.js"; -import type { EventData } from "../src/types.js"; +import { init, shutdown, type EventData } from "@secureagentics/adrian"; +import { captureTool, adrian, adrianTools } from "../src/index.js"; describe("Vercel AI SDK instrumentation", () => { afterEach(async () => { @@ -9,7 +9,7 @@ describe("Vercel AI SDK instrumentation", () => { it("captures generateText calls as paired LLM events", async () => { const events: EventData[] = []; - const ai = instrumentVercelAI({ + const ai = adrian({ generateText: async (_args: Record) => ({ text: "hello from vercel", toolCalls: [{ toolCallId: "tool-1", toolName: "search", args: { query: "adrian" } }], @@ -17,7 +17,7 @@ describe("Vercel AI SDK instrumentation", () => { }), }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); const result = await ai.generateText({ @@ -39,7 +39,7 @@ describe("Vercel AI SDK instrumentation", () => { it("emits streamText events when the result promises settle", async () => { const events: EventData[] = []; - const ai = instrumentVercelAI({ + const ai = adrian({ streamText: (_args: Record) => ({ text: Promise.resolve("streamed"), toolCalls: Promise.resolve([]), @@ -47,7 +47,7 @@ describe("Vercel AI SDK instrumentation", () => { }), }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { events.push(data); } }); const result = await ai.streamText({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }] }); @@ -64,11 +64,11 @@ describe("Vercel AI SDK instrumentation", () => { it("captures local Vercel AI tool execution as a tool event", async () => { const events: Array<{ type: string; data: EventData }> = []; - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { events.push({ type, data }); } }); - const result = await captureVercelAIToolCall({ + const result = await captureTool({ toolCallId: "tool-weather", toolName: "getWeather", args: { city: "San Francisco" }, @@ -90,11 +90,11 @@ describe("Vercel AI SDK instrumentation", () => { it("captures local Vercel AI tool errors as tool events", async () => { const events: Array<{ type: string; data: EventData }> = []; - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { events.push({ type, data }); } }); - await expect(captureAITool({ + await expect(captureTool({ toolCallId: "tool-weather", toolName: "getWeather", args: { city: "San Francisco" }, @@ -116,14 +116,14 @@ describe("Vercel AI SDK instrumentation", () => { it("wraps Vercel AI SDK tool execute functions", async () => { const events: Array<{ type: string; data: EventData }> = []; - const tools = instrumentVercelAITools({ + const tools = adrianTools({ getWeather: { description: "Get current weather for a city.", execute: async ({ city }: { city: string }, _options?: unknown) => ({ city, temperatureF: 58 }), }, }); - await init({ handlers: [], autoInstrument: false, sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { events.push({ type, data }); } }); @@ -141,4 +141,45 @@ describe("Vercel AI SDK instrumentation", () => { }, }); }); + + it("wraps Vercel AI SDK via adrian()", async () => { + const events: EventData[] = []; + const ai = adrian({ + generateText: async () => ({ + text: "hello from vercel", + }), + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + + await ai.generateText({ + model: "gpt-4o", + prompt: "hello", + }); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ model: "gpt-4o", output: "hello from vercel" }); + }); + + it("wraps Vercel tools via adrian()", async () => { + const events: Array<{ type: string; data: EventData }> = []; + const tools = adrian({ + getWeather: { + description: "Get weather", + execute: async ({ city }: { city: string }) => ({ city, temp: 72 }), + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { + events.push({ type, data }); + } }); + + await tools.getWeather.execute({ city: "SF" }); + + expect(events).toHaveLength(1); + expect(events[0].type).toBe("tool"); + expect(events[0].data).toMatchObject({ toolName: "getWeather", input: '{"city":"SF"}' }); + }); }); diff --git a/sdk/typescript/packages/vercel/tsconfig.build.json b/sdk/typescript/packages/vercel/tsconfig.build.json new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/sdk/typescript/packages/vercel/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["tests/**/*.ts"] +} diff --git a/sdk/typescript/packages/vercel/tsconfig.json b/sdk/typescript/packages/vercel/tsconfig.json new file mode 100644 index 0000000..6140ae3 --- /dev/null +++ b/sdk/typescript/packages/vercel/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/sdk/typescript/tsconfig.base.json b/sdk/typescript/tsconfig.base.json index e1cd4d1..1233a6c 100644 --- a/sdk/typescript/tsconfig.base.json +++ b/sdk/typescript/tsconfig.base.json @@ -1,14 +1,15 @@ { "compilerOptions": { - "target": "es2022", - "module": "nodenext", - "moduleResolution": "nodenext", - "lib": ["es2023"], + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", "strict": true, "declaration": true, + "declarationMap": true, + "sourceMap": true, "esModuleInterop": true, - "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "verbatimModuleSyntax": true + "skipLibCheck": true, + "types": ["node"] } } diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json deleted file mode 100644 index a930282..0000000 --- a/sdk/typescript/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "outDir": "dist", - "rootDir": ".", - "types": ["node"] - }, - "include": ["src/**/*.ts", "tests/**/*.ts"] -} From b2abcb5ee29136bb108f14223a42e65a7cb065e3 Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Thu, 4 Jun 2026 00:01:52 +0530 Subject: [PATCH 04/10] remove deprecated --- sdk/typescript/package-lock.json | 6 +++--- sdk/typescript/packages/langchain/src/index.ts | 7 ------- sdk/typescript/packages/openai/src/index.ts | 11 ----------- sdk/typescript/packages/vercel/src/index.ts | 15 --------------- 4 files changed, 3 insertions(+), 36 deletions(-) diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index 38f334c..16f0605 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -2722,7 +2722,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@secureagentics/adrian": "1.0.0" + "@secureagentics/adrian": "^1.0.0" }, "devDependencies": { "@secureagentics/adrian": "file:../core", @@ -2749,7 +2749,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@secureagentics/adrian": "1.0.0" + "@secureagentics/adrian": "^1.0.0" }, "devDependencies": { "@secureagentics/adrian": "file:../core", @@ -2772,7 +2772,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@secureagentics/adrian": "1.0.0" + "@secureagentics/adrian": "^1.0.0" }, "devDependencies": { "@secureagentics/adrian": "file:../core", diff --git a/sdk/typescript/packages/langchain/src/index.ts b/sdk/typescript/packages/langchain/src/index.ts index 0015615..52a702b 100644 --- a/sdk/typescript/packages/langchain/src/index.ts +++ b/sdk/typescript/packages/langchain/src/index.ts @@ -26,13 +26,6 @@ export async function adrianWith( ]); } -/** @deprecated Use adrian instead */ -export const instrumentLangChain = adrian; -/** @deprecated Use adrianWith instead */ -export const instrumentLangChainWith = adrianWith; -/** @deprecated Use adrian instead */ -export const autoInstrument = adrian; - function injectCallbacks(config: unknown, handler: AdrianCallbackHandler | null): unknown { if (!handler) return config ?? {}; const next = { ...((config && typeof config === "object") ? config as Record : {}) }; diff --git a/sdk/typescript/packages/openai/src/index.ts b/sdk/typescript/packages/openai/src/index.ts index c4a6c88..ec7587d 100644 --- a/sdk/typescript/packages/openai/src/index.ts +++ b/sdk/typescript/packages/openai/src/index.ts @@ -71,17 +71,6 @@ export async function captureTool( }); } -/** @deprecated Use adrian instead */ -export const instrumentOpenAI = adrian; -/** @deprecated Use adrian instead */ -export const instrument = adrian; -/** @deprecated Use adrian instead */ -export const withAdrianOpenAI = adrian; -/** @deprecated Use captureTool instead */ -export const captureOpenAITool = captureTool; -/** @deprecated Use captureTool instead */ -export const captureOpenAIToolCall = captureTool; - function instrumentChat(chat: unknown, options: AdrianOptions): unknown { if (!chat || typeof chat !== "object") return chat; return new Proxy(chat as Record, { diff --git a/sdk/typescript/packages/vercel/src/index.ts b/sdk/typescript/packages/vercel/src/index.ts index fafacec..f8ad1e3 100644 --- a/sdk/typescript/packages/vercel/src/index.ts +++ b/sdk/typescript/packages/vercel/src/index.ts @@ -92,21 +92,6 @@ export async function captureTool( }); } -/** @deprecated Use adrian instead */ -export const instrumentVercelAI = adrian; -/** @deprecated Use adrian instead */ -export const instrument = adrian; -/** @deprecated Use adrian instead */ -export const withAdrianVercelAI = adrian; -/** @deprecated Use adrianTools instead */ -export const instrumentVercelAITools = adrianTools; -/** @deprecated Use captureTool instead */ -export const captureVercelAITool = captureTool; -/** @deprecated Use captureTool instead */ -export const captureVercelAIToolCall = captureTool; -/** @deprecated Use captureTool instead */ -export const captureAITool = captureTool; - function wrapAiModule>(ai: T, options: AdrianOptions = {}): T { return new Proxy(ai, { get(target, prop, receiver) { From 95525a85f89e2e4a16fc98f2e80f629bf11c74f4 Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Thu, 4 Jun 2026 01:06:43 +0530 Subject: [PATCH 05/10] review working --- .../packages/core/src/capture/common.ts | 30 ++++- sdk/typescript/packages/core/src/index.ts | 2 + sdk/typescript/packages/core/src/policy.ts | 56 ++++++++++ .../packages/core/tests/policy.test.ts | 104 ++++++++++++++++++ .../packages/langchain/src/index.ts | 15 +-- .../langchain/tests/langchain.test.ts | 18 +++ sdk/typescript/packages/openai/README.md | 2 + sdk/typescript/packages/openai/src/index.ts | 15 ++- .../packages/openai/tests/openai.test.ts | 37 ++++++- sdk/typescript/packages/vercel/README.md | 2 + sdk/typescript/packages/vercel/src/index.ts | 12 +- .../packages/vercel/tests/vercel.test.ts | 52 ++++++++- 12 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 sdk/typescript/packages/core/src/policy.ts create mode 100644 sdk/typescript/packages/core/tests/policy.test.ts diff --git a/sdk/typescript/packages/core/src/capture/common.ts b/sdk/typescript/packages/core/src/capture/common.ts index 9963e50..fc2dd44 100644 --- a/sdk/typescript/packages/core/src/capture/common.ts +++ b/sdk/typescript/packages/core/src/capture/common.ts @@ -1,8 +1,20 @@ import { randomUUID } from "node:crypto"; +import { currentConfig } from "../config.js"; import type { AdrianCallbackHandler } from "../handler.js"; import { runWithInvocationId } from "../context.js"; +import { assertToolCallsAllowed } from "../policy.js"; +import { getWebSocketClient } from "../registry.js"; import type { CallbackMetadata, ChatMessage, LlmEndData, TokenUsage, ToolArgs, ToolCallRecord } from "../types.js"; +/** Gate tool calls after the paired LLM event has been emitted (maps tool-call ids on the WS client). */ +export async function gateLlmEndData(end: LlmEndData): Promise { + await assertToolCallsAllowed( + end.toolCalls.map((call) => call.id), + getWebSocketClient(), + currentConfig()?.blockTimeout ?? 30, + ); +} + export interface LlmCaptureInput { model: string; messages: ChatMessage[]; @@ -15,6 +27,7 @@ export async function captureLlmCall( input: LlmCaptureInput, execute: () => Promise, extractOutput: (result: T) => LlmEndData | Promise, + afterPairedEmit?: (end: LlmEndData) => void | Promise, ): Promise { const handler = getHandler(); if (!handler) return execute(); @@ -24,7 +37,9 @@ export async function captureLlmCall( await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata ?? null }); try { const result = await execute(); - await handler.handleLLMEnd(await extractOutput(result), runId); + const endData = await extractOutput(result); + await handler.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); return result; } catch (error) { await handler.handleLLMError(error, runId); @@ -38,7 +53,8 @@ export function captureLlmAsyncIterable( input: LlmCaptureInput, iterable: AsyncIterable, aggregate: (chunk: T) => void, - extractOutput: () => LlmEndData, + extractOutput: () => LlmEndData | Promise, + afterPairedEmit?: (end: LlmEndData) => void | Promise, ): AsyncIterable { const handler = getHandler(); if (!handler) return iterable; @@ -57,13 +73,19 @@ export function captureLlmAsyncIterable( yield chunk; } emitted = true; - await handler?.handleLLMEnd(await extractOutput(), runId); + const endData = await extractOutput(); + await handler?.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); } catch (error) { failed = true; await handler?.handleLLMError(error, runId); throw error; } finally { - if (!emitted && !failed) await handler?.handleLLMEnd(await extractOutput(), runId); + if (!emitted && !failed) { + const endData = await extractOutput(); + await handler?.handleLLMEnd(endData, runId); + await afterPairedEmit?.(endData); + } } }); } diff --git a/sdk/typescript/packages/core/src/index.ts b/sdk/typescript/packages/core/src/index.ts index 265aa1d..64e6a47 100644 --- a/sdk/typescript/packages/core/src/index.ts +++ b/sdk/typescript/packages/core/src/index.ts @@ -95,6 +95,8 @@ export { EventPairBuffer } from "./pairing.js"; export { AgentContextTracker, getInvocationId, runWithInvocationId } from "./context.js"; export { deriveAgentId, deriveLangGraphAgentId } from "./identity.js"; export { WebSocketClient, shouldHalt } from "./ws.js"; +export { AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE, assertToolCallsAllowed, gateToolCallIds } from "./policy.js"; +export type { GateToolCallsReason, GateToolCallsResult } from "./policy.js"; export { mcpServers, registerMcpServer, registerMcpConnection } from "./mcp.js"; export { resolveSessionId, envAwareResolveSessionId } from "./sessionPersistence.js"; export { getHandler, getWebSocketClient } from "./registry.js"; diff --git a/sdk/typescript/packages/core/src/policy.ts b/sdk/typescript/packages/core/src/policy.ts new file mode 100644 index 0000000..5f9f1f1 --- /dev/null +++ b/sdk/typescript/packages/core/src/policy.ts @@ -0,0 +1,56 @@ +import { currentConfig } from "./config.js"; +import { shouldHalt, type WebSocketClient } from "./ws.js"; + +/** Tool result content returned when LangGraph ToolNode execution is blocked. */ +export const BLOCKED_TOOL_MESSAGE = "[BLOCKED by security policy]"; + +export type GateToolCallsReason = "missing_tool_call_id" | "policy_halt" | "verdict_timeout"; + +export type GateToolCallsResult = + | { action: "allow" } + | { action: "block"; reason: GateToolCallsReason }; + +export class AdrianPolicyBlockedError extends Error { + readonly reason: GateToolCallsReason; + + constructor(reason: GateToolCallsReason) { + super(`Adrian security policy blocked execution (${reason})`); + this.name = "AdrianPolicyBlockedError"; + this.reason = reason; + } +} + +/** + * Waits for backend verdicts on tool calls proposed by a prior LLM turn. + * No-ops when WebSocket is absent or policy mode is not BLOCK/HITL. + */ +export async function gateToolCallIds( + toolCallIds: string[], + ws: WebSocketClient | null, + blockTimeoutSeconds?: number, +): Promise { + if (toolCallIds.length === 0) return { action: "allow" }; + if (!ws) return { action: "allow" }; + + const timeoutSeconds = blockTimeoutSeconds ?? currentConfig()?.blockTimeout ?? 30; + const policyReady = await ws.waitForPolicyReady(timeoutSeconds); + if (!policyReady || !ws.policyActive()) return { action: "allow" }; + + if (toolCallIds.some((id) => !id)) return { action: "block", reason: "missing_tool_call_id" }; + + const verdictTimeout = ws.blockTimeout(timeoutSeconds); + const verdicts = await Promise.all(toolCallIds.map((id) => ws.waitForToolCallVerdict(id, verdictTimeout))); + if (verdicts.some((verdict) => !verdict)) return { action: "block", reason: "verdict_timeout" }; + if (verdicts.some((verdict) => verdict !== null && shouldHalt(verdict))) return { action: "block", reason: "policy_halt" }; + return { action: "allow" }; +} + +/** Throws {@link AdrianPolicyBlockedError} when {@link gateToolCallIds} would block. */ +export async function assertToolCallsAllowed( + toolCallIds: string[], + ws: WebSocketClient | null, + blockTimeoutSeconds?: number, +): Promise { + const result = await gateToolCallIds(toolCallIds, ws, blockTimeoutSeconds); + if (result.action === "block") throw new AdrianPolicyBlockedError(result.reason); +} diff --git a/sdk/typescript/packages/core/tests/policy.test.ts b/sdk/typescript/packages/core/tests/policy.test.ts new file mode 100644 index 0000000..0895c9b --- /dev/null +++ b/sdk/typescript/packages/core/tests/policy.test.ts @@ -0,0 +1,104 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { setConfig, Mode, type Verdict, type WebSocketClient } from "../src/index.js"; +import { + AdrianPolicyBlockedError, + assertToolCallsAllowed, + gateToolCallIds, +} from "../src/policy.js"; + +function config(): Parameters[0] { + return { + apiKey: null, + logFile: "events.jsonl", + logLevel: null, + sessionId: "sess", + wsUrl: null, + blockTimeout: 5, + onEvent: null, + onVerdict: null, + onBlock: null, + onAudit: null, + onDisconnect: null, + onReconnect: null, + onMcpServer: null, + replayBufferFrames: 1000, + }; +} + +function verdict(eventId: string, halt: boolean): Verdict { + return { + eventId, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, + hitl: null, + }; +} + +describe("gateToolCallIds", () => { + afterEach(() => setConfig(null)); + + it("allows when WebSocket is absent", async () => { + expect(await gateToolCallIds(["call-1"], null)).toEqual({ action: "allow" }); + }); + + it("allows when policy is not active", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => false, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => verdict("evt", false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1"], ws)).toEqual({ action: "allow" }); + }); + + it("blocks on missing tool call id", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => verdict("evt", false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1", ""], ws)).toEqual({ action: "block", reason: "missing_tool_call_id" }); + }); + + it("blocks when a verdict requests halt", async () => { + setConfig(config()); + const waitedFor: string[] = []; + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => { + waitedFor.push(toolCallId); + return verdict(`event-${toolCallId}`, toolCallId === "call-2"); + }, + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1", "call-2"], ws)).toEqual({ action: "block", reason: "policy_halt" }); + expect(waitedFor).toEqual(["call-1", "call-2"]); + }); + + it("allows when all verdicts permit execution", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => verdict(`event-${toolCallId}`, false), + } as unknown as WebSocketClient; + expect(await gateToolCallIds(["call-1", "call-2"], ws)).toEqual({ action: "allow" }); + }); + + it("assertToolCallsAllowed throws AdrianPolicyBlockedError", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async () => null, + } as unknown as WebSocketClient; + await expect(assertToolCallsAllowed(["call-1"], ws)).rejects.toBeInstanceOf(AdrianPolicyBlockedError); + }); +}); diff --git a/sdk/typescript/packages/langchain/src/index.ts b/sdk/typescript/packages/langchain/src/index.ts index 52a702b..ef16aea 100644 --- a/sdk/typescript/packages/langchain/src/index.ts +++ b/sdk/typescript/packages/langchain/src/index.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { currentConfig, getHandler, getWebSocketClient, runWithInvocationId, shouldHalt } from "@secureagentics/adrian"; +import { BLOCKED_TOOL_MESSAGE, currentConfig, gateToolCallIds, getHandler, getWebSocketClient, runWithInvocationId } from "@secureagentics/adrian"; import type { AdrianCallbackHandler, WebSocketClient } from "@secureagentics/adrian"; let patched = false; @@ -105,17 +105,10 @@ async function patchToolNode(getHandlerFn: () => AdrianCallbackHandler | null, g } export async function blockedToolNodeResponse(input: unknown, ws: WebSocketClient | null): Promise<{ messages: Array> } | null> { - if (!ws) return null; - const cfg = currentConfig(); - const policyReady = await ws.waitForPolicyReady(cfg?.blockTimeout ?? 30); - if (!policyReady || !ws.policyActive()) return null; const toolCalls = extractToolCalls(input); if (toolCalls.length === 0) return null; - if (toolCalls.some((call) => !call.id)) return buildBlockedResponse(toolCalls); - - const timeout = ws.blockTimeout(cfg?.blockTimeout ?? 30); - const verdicts = await Promise.all(toolCalls.map((call) => ws.waitForToolCallVerdict(call.id, timeout))); - if (verdicts.some((verdict) => !verdict || shouldHalt(verdict))) return buildBlockedResponse(toolCalls); + const gate = await gateToolCallIds(toolCalls.map((call) => call.id), ws, currentConfig()?.blockTimeout ?? 30); + if (gate.action === "block") return buildBlockedResponse(toolCalls); return null; } @@ -130,7 +123,7 @@ export function extractToolCalls(input: unknown): Array<{ id: string; name: stri } export function buildBlockedResponse(toolCalls: Array<{ id: string; name: string }>): { messages: Array> } { - return { messages: toolCalls.map((call) => ({ content: "[BLOCKED by security policy]", tool_call_id: call.id, name: call.name, type: "tool" })) }; + return { messages: toolCalls.map((call) => ({ content: BLOCKED_TOOL_MESSAGE, tool_call_id: call.id, name: call.name, type: "tool" })) }; } async function importOptional(specifier: string): Promise { diff --git a/sdk/typescript/packages/langchain/tests/langchain.test.ts b/sdk/typescript/packages/langchain/tests/langchain.test.ts index 8ba0764..6572136 100644 --- a/sdk/typescript/packages/langchain/tests/langchain.test.ts +++ b/sdk/typescript/packages/langchain/tests/langchain.test.ts @@ -62,6 +62,24 @@ describe("ToolNode policy gating", () => { expect(response?.messages[0]?.content).toContain("BLOCKED"); }); + it("blocks ToolNode when policy halts a tool call", async () => { + setConfig(config()); + const ws = { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => verdict(`event-${toolCallId}`, true), + } as unknown as WebSocketClient; + + const response = await blockedToolNodeResponse({ + messages: [{ tool_calls: [{ id: "call-weather", name: "get_weather" }] }], + }, ws); + + expect(response?.messages).toHaveLength(1); + expect(response?.messages[0]?.content).toContain("BLOCKED"); + expect(response?.messages[0]?.tool_call_id).toBe("call-weather"); + }); + it("does not block when all correlated verdicts allow execution", async () => { setConfig(config()); const ws = { diff --git a/sdk/typescript/packages/openai/README.md b/sdk/typescript/packages/openai/README.md index 2e5e732..14b491c 100644 --- a/sdk/typescript/packages/openai/README.md +++ b/sdk/typescript/packages/openai/README.md @@ -32,6 +32,8 @@ await shutdown(); `adrian()` wraps `chat.completions.create`, `responses.create`, and their streaming variants. Events are paired, PII-redacted, and streamed to Adrian automatically. +When the dashboard policy is in **BLOCK** or **HITL** mode and `init()` is connected over WebSocket, the SDK waits for verdicts on LLM turns that propose tool calls and throws `AdrianPolicyBlockedError` before those tools can run. `captureTool` applies the same gate before executing your handler. + ## Tool execution capture OpenAI returns tool call requests; your app executes the tools. Wrap that execution with `captureTool`: diff --git a/sdk/typescript/packages/openai/src/index.ts b/sdk/typescript/packages/openai/src/index.ts index ec7587d..c5e9236 100644 --- a/sdk/typescript/packages/openai/src/index.ts +++ b/sdk/typescript/packages/openai/src/index.ts @@ -1,9 +1,10 @@ import { randomUUID } from "node:crypto"; -import { getHandler, runWithInvocationId } from "@secureagentics/adrian"; +import { assertToolCallsAllowed, currentConfig, getHandler, getWebSocketClient, runWithInvocationId } from "@secureagentics/adrian"; import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "@secureagentics/adrian"; import { captureLlmAsyncIterable, captureLlmCall, + gateLlmEndData, emptyLlmEnd, messagesFromPromptLike, normalizeMessages, @@ -58,6 +59,8 @@ export async function captureTool( const input = String(toolCall.function?.arguments ?? toolCall.arguments ?? ""); const metadata = integrationMetadata(options.metadata, "openai.tool_call"); + await assertToolCallsAllowed(toolCallId ? [toolCallId] : [], getWebSocketClient(), currentConfig()?.blockTimeout ?? 30); + return runWithInvocationId(randomUUID(), async () => { await handler.handleToolStart({ name: toolName }, input, runId, options.parentRunId, { metadata, tool_call_id: toolCallId }); try { @@ -96,7 +99,7 @@ function instrumentChatCompletions(completions: unknown, options: AdrianOptions) if (isAsyncIterable(result)) return captureChatCompletionStream(model, messages, metadata, result); return result; } - return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractChatCompletion); + return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractChatCompletion, gateLlmEndData); }; }, }); @@ -117,7 +120,7 @@ function instrumentResponses(responses: unknown, options: AdrianOptions): unknow if (isAsyncIterable(result)) return captureResponseStream(model, messages, metadata, result); return result; } - return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractResponse); + return captureLlmCall(getHandler, { model, messages, metadata }, () => Promise.resolve(value.call(target, body, ...rest)), extractResponse, gateLlmEndData); }; }, }); @@ -147,7 +150,7 @@ function captureChatCompletionStream(model: string, messages: ReturnType emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage)); + }, () => emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage), gateLlmEndData); } function captureResponseStream(model: string, messages: ReturnType, metadata: CallbackMetadata | null, stream: AsyncIterable): AsyncIterable { @@ -160,7 +163,7 @@ function captureResponseStream(model: string, messages: ReturnType emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage)); + }, () => emptyLlmEnd(output, [...toolCallParts.values()].map((call) => ({ id: call.id, name: call.name, args: parseToolArgs(call.args) })), usage), gateLlmEndData); } function extractChatCompletion(result: unknown): LlmEndData { @@ -227,6 +230,8 @@ function isAsyncIterable(value: unknown): value is AsyncIterable { } export { + AdrianPolicyBlockedError, + BLOCKED_TOOL_MESSAGE, init, shutdown, getHandler, diff --git a/sdk/typescript/packages/openai/tests/openai.test.ts b/sdk/typescript/packages/openai/tests/openai.test.ts index 2596e48..3d331e8 100644 --- a/sdk/typescript/packages/openai/tests/openai.test.ts +++ b/sdk/typescript/packages/openai/tests/openai.test.ts @@ -1,9 +1,26 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { init, shutdown, type EventData } from "@secureagentics/adrian"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as adrianCore from "@secureagentics/adrian"; +import { AdrianPolicyBlockedError, init, Mode, shutdown, type EventData, type Verdict, type WebSocketClient } from "@secureagentics/adrian"; import { captureTool, adrian } from "../src/index.js"; +function mockWs(halt: boolean): WebSocketClient { + return { + waitForPolicyReady: async () => true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => ({ + eventId: `event-${toolCallId}`, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, + hitl: null, + } satisfies Verdict), + } as unknown as WebSocketClient; +} + describe("OpenAI instrumentation", () => { afterEach(async () => { + vi.restoreAllMocks(); await shutdown(); }); @@ -165,6 +182,22 @@ describe("OpenAI instrumentation", () => { }); }); + it("blocks captureTool when policy halts", async () => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, blockTimeout: 5 }); + vi.spyOn(adrianCore, "getWebSocketClient").mockReturnValue(mockWs(true)); + + let executed = false; + await expect(captureTool({ + id: "call-weather", + function: { name: "get_weather", arguments: "{}" }, + }, async () => { + executed = true; + return { ok: true }; + })).rejects.toBeInstanceOf(AdrianPolicyBlockedError); + + expect(executed).toBe(false); + }); + it("captures local OpenAI tool execution as a tool event", async () => { const events: Array<{ type: string; data: EventData }> = []; await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { diff --git a/sdk/typescript/packages/vercel/README.md b/sdk/typescript/packages/vercel/README.md index 7f906e3..f3780b3 100644 --- a/sdk/typescript/packages/vercel/README.md +++ b/sdk/typescript/packages/vercel/README.md @@ -32,6 +32,8 @@ await shutdown(); `adrian()` wraps `generateText`, `streamText`, `generateObject`, and `streamObject`. +With **BLOCK** or **HITL** policy and a live WebSocket, `generateText` / streaming calls wait for verdicts when the model returns tool calls, and `captureTool` / `adrianTools` wait before running `execute`. A halt throws `AdrianPolicyBlockedError`. + ## Tools Pass wrapped tool definitions to `generateText`: diff --git a/sdk/typescript/packages/vercel/src/index.ts b/sdk/typescript/packages/vercel/src/index.ts index f8ad1e3..64a6e61 100644 --- a/sdk/typescript/packages/vercel/src/index.ts +++ b/sdk/typescript/packages/vercel/src/index.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; -import { getHandler, runWithInvocationId } from "@secureagentics/adrian"; +import { assertToolCallsAllowed, currentConfig, getHandler, getWebSocketClient, runWithInvocationId } from "@secureagentics/adrian"; import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "@secureagentics/adrian"; -import { captureLlmCall, emptyLlmEnd, messagesFromPromptLike, normalizeUsage, parseToolArgs, stringifyContent } from "@secureagentics/adrian/capture"; +import { captureLlmCall, gateLlmEndData, emptyLlmEnd, messagesFromPromptLike, normalizeUsage, parseToolArgs, stringifyContent } from "@secureagentics/adrian/capture"; export interface AdrianOptions { metadata?: CallbackMetadata | null; @@ -79,6 +79,8 @@ export async function captureTool( const input = stringifyContent(toolCall.args); const metadata = integrationMetadata(options.metadata, "vercel-ai.tool_call"); + await assertToolCallsAllowed(toolCallId ? [toolCallId] : [], getWebSocketClient(), currentConfig()?.blockTimeout ?? 30); + return runWithInvocationId(randomUUID(), async () => { await handler.handleToolStart({ name: toolName }, input, runId, options.parentRunId, { metadata, toolCallId }); try { @@ -114,7 +116,7 @@ function captureVercelCall(operation: string, execute: () => T, args: Record< return result; } - return Promise.resolve(result).then((resolved) => captureLlmCall(getHandler, { model, messages: messagesFromPromptLike(args), metadata }, async () => resolved, extractVercelResult)); + return Promise.resolve(result).then((resolved) => captureLlmCall(getHandler, { model, messages: messagesFromPromptLike(args), metadata }, async () => resolved, extractVercelResult, gateLlmEndData)); } async function emitVercelStreamResult(model: string, args: Record, metadata: CallbackMetadata, result: unknown): Promise { @@ -126,7 +128,7 @@ async function emitVercelStreamResult(model: string, args: Record true, + policyActive: () => true, + blockTimeout: (seconds: number) => seconds, + waitForToolCallVerdict: async (toolCallId: string) => ({ + eventId: `event-${toolCallId}`, + sessionId: "sess", + madCode: "M3_TEST", + policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, + hitl: null, + } satisfies Verdict), + } as unknown as WebSocketClient; +} + describe("Vercel AI SDK instrumentation", () => { afterEach(async () => { + vi.restoreAllMocks(); await shutdown(); }); @@ -62,6 +79,37 @@ describe("Vercel AI SDK instrumentation", () => { }); }); + it("blocks captureTool when policy halts", async () => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, blockTimeout: 5 }); + vi.spyOn(adrianCore, "getWebSocketClient").mockReturnValue(mockWs(true)); + + let executed = false; + await expect(captureTool({ + toolCallId: "tool-weather", + toolName: "getWeather", + args: { city: "SF" }, + }, async () => { + executed = true; + return { ok: true }; + })).rejects.toBeInstanceOf(AdrianPolicyBlockedError); + + expect(executed).toBe(false); + }); + + it("blocks adrianTools execute when policy halts", async () => { + await init({ handlers: [], sessionId: "sess", wsUrl: null, blockTimeout: 5 }); + vi.spyOn(adrianCore, "getWebSocketClient").mockReturnValue(mockWs(true)); + + const tools = adrianTools({ + getWeather: { + description: "Get weather", + execute: async () => ({ temp: 72 }), + }, + }); + + await expect(tools.getWeather.execute({ city: "SF" }, { toolCallId: "tool-weather" })).rejects.toBeInstanceOf(AdrianPolicyBlockedError); + }); + it("captures local Vercel AI tool execution as a tool event", async () => { const events: Array<{ type: string; data: EventData }> = []; await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { From 4800c90b273d4e7ef49f393cca7a16be490ccb2e Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Thu, 4 Jun 2026 01:15:28 +0530 Subject: [PATCH 06/10] removed langchain sdk --- sdk/typescript/README.md | 37 +---- sdk/typescript/package-lock.json | 31 ---- sdk/typescript/packages/core/README.md | 2 +- sdk/typescript/packages/core/src/identity.ts | 19 +-- sdk/typescript/packages/core/src/index.ts | 2 +- sdk/typescript/packages/core/src/mcp.ts | 23 +-- sdk/typescript/packages/core/src/policy.ts | 2 +- sdk/typescript/packages/langchain/README.md | 57 ------- .../packages/langchain/package.json | 57 ------- .../packages/langchain/src/index.ts | 146 ------------------ .../langchain/tests/langchain.test.ts | 96 ------------ .../packages/langchain/tsconfig.build.json | 12 -- .../packages/langchain/tsconfig.json | 8 - 13 files changed, 12 insertions(+), 480 deletions(-) delete mode 100644 sdk/typescript/packages/langchain/README.md delete mode 100644 sdk/typescript/packages/langchain/package.json delete mode 100644 sdk/typescript/packages/langchain/src/index.ts delete mode 100644 sdk/typescript/packages/langchain/tests/langchain.test.ts delete mode 100644 sdk/typescript/packages/langchain/tsconfig.build.json delete mode 100644 sdk/typescript/packages/langchain/tsconfig.json diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 0e24d2c..6aac433 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -8,7 +8,6 @@ Monorepo for the Adrian TypeScript SDK. Pick the package for your framework — |---|---|---|---| | OpenAI | `@secureagentics/adrian-openai` | `npm install @secureagentics/adrian-openai openai` | `import { init, adrian, captureTool } from "@secureagentics/adrian-openai"` | | Vercel AI | `@secureagentics/adrian-vercel` | `npm install @secureagentics/adrian-vercel ai` | `import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"` | -| LangChain | `@secureagentics/adrian-langchain` | `npm install @secureagentics/adrian-langchain @langchain/core @langchain/langgraph` | `import { init, adrian } from "@secureagentics/adrian-langchain"` | | Core only | `@secureagentics/adrian` | `npm install @secureagentics/adrian` | `import { init, shutdown } from "@secureagentics/adrian"` | Provider packages depend on `@secureagentics/adrian` and re-export `init`, `shutdown`, and other core APIs — one install, one import. @@ -27,9 +26,6 @@ import { init, adrian, captureTool } from "@secureagentics/adrian-openai"; // Vercel AI import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; -// LangChain / LangGraph -import { init, adrian } from "@secureagentics/adrian-langchain"; - await init({ apiKey: process.env.ADRIAN_API_KEY }); ``` @@ -37,12 +33,12 @@ await init({ apiKey: process.env.ADRIAN_API_KEY }); All provider packages export the **same function names**: -| Export | OpenAI | Vercel AI | LangChain | -|---|---|---|---| -| `init` / `shutdown` | Re-exported from core | Re-exported from core | Re-exported from core | -| `adrian(...)` | Wrap an OpenAI client | Wrap an AI module or tools object | Enable callbacks (no arguments) | -| `adrianTools(...)` | — | Wrap tool definitions for `generateText` | — | -| `captureTool(...)` | Capture manual tool execution | Capture manual tool execution | — | +| Export | OpenAI | Vercel AI | +|---|---|---| +| `init` / `shutdown` | Re-exported from core | Re-exported from core | +| `adrian(...)` | Wrap an OpenAI client | Wrap an AI module or tools object | +| `adrianTools(...)` | — | Wrap tool definitions for `generateText` | +| `captureTool(...)` | Capture manual tool execution | Capture manual tool execution | Shared option types (same names in every provider package): @@ -107,23 +103,6 @@ for (const toolCall of result.toolCalls ?? []) { await shutdown(); ``` -### LangChain / LangGraph - -```bash -npm install @secureagentics/adrian-langchain @langchain/core @langchain/langgraph -``` - -```ts -import { init, shutdown, adrian } from "@secureagentics/adrian-langchain"; - -await init({ apiKey: process.env.ADRIAN_API_KEY }); -await adrian(); - -// Your normal LangChain / LangGraph code runs here. - -await shutdown(); -``` - ## Using multiple providers Install each provider package you need. Core is deduplicated to a single copy: @@ -135,10 +114,8 @@ npm install @secureagentics/adrian-openai @secureagentics/adrian-vercel openai a ```ts import { init, adrian as adrianOpenAI } from "@secureagentics/adrian-openai"; import { adrian as adrianVercel } from "@secureagentics/adrian-vercel"; -import { adrian as adrianLangChain } from "@secureagentics/adrian-langchain"; await init({ apiKey: process.env.ADRIAN_API_KEY }); -await adrianLangChain(); const openai = adrianOpenAI(new OpenAI()); const monitored = adrianVercel(vercelAi); @@ -170,4 +147,4 @@ npm run build -w @secureagentics/adrian npm test -w @secureagentics/adrian-openai ``` -Per-package docs: [core](./packages/core) · [openai](./packages/openai) · [vercel](./packages/vercel) · [langchain](./packages/langchain) +Per-package docs: [core](./packages/core) · [openai](./packages/openai) · [vercel](./packages/vercel) diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index 16f0605..71b4070 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -1179,10 +1179,6 @@ "resolved": "packages/core", "link": true }, - "node_modules/@secureagentics/adrian-langchain": { - "resolved": "packages/langchain", - "link": true - }, "node_modules/@secureagentics/adrian-openai": { "resolved": "packages/openai", "link": true @@ -2717,33 +2713,6 @@ "vitest": "^4.1.7" } }, - "packages/langchain": { - "name": "@secureagentics/adrian-langchain", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "@secureagentics/adrian": "^1.0.0" - }, - "devDependencies": { - "@secureagentics/adrian": "file:../core", - "@types/node": "^25.9.1", - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "vitest": "^4.1.7" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.0", - "@langchain/langgraph": ">=0.2.0" - }, - "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "@langchain/langgraph": { - "optional": true - } - } - }, "packages/openai": { "name": "@secureagentics/adrian-openai", "version": "1.0.0", diff --git a/sdk/typescript/packages/core/README.md b/sdk/typescript/packages/core/README.md index d971aca..38fe0c9 100644 --- a/sdk/typescript/packages/core/README.md +++ b/sdk/typescript/packages/core/README.md @@ -36,7 +36,7 @@ await shutdown(); | `shutdown()` | Flush handlers and tear down | | `getHandler()` | Access the callback handler for manual wiring | | `getWebSocketClient()` | Access the WebSocket client | -| `AdrianCallbackHandler` | LangChain-compatible callback class | +| `AdrianCallbackHandler` | Event callback handler class | | `JSONLHandler` | Local JSONL event sink | ## Environment diff --git a/sdk/typescript/packages/core/src/identity.ts b/sdk/typescript/packages/core/src/identity.ts index 19001f6..019352b 100644 --- a/sdk/typescript/packages/core/src/identity.ts +++ b/sdk/typescript/packages/core/src/identity.ts @@ -1,22 +1,5 @@ import type { CallbackMetadata, ChatMessage } from "./types.js"; -const CHECKPOINT_NS_KEY = "langgraph_checkpoint_ns"; - -export function isLangGraphMetadata(metadata: CallbackMetadata): boolean { - return CHECKPOINT_NS_KEY in metadata; -} - -export function deriveLangGraphAgentId(metadata: CallbackMetadata): string | null { - const ns = metadata[CHECKPOINT_NS_KEY]; - if (typeof ns !== "string" || ns.length === 0) return null; - const nodeNames = ns.split("|").map((segment) => segment.split(":")[0]).filter(Boolean); - return nodeNames.length > 0 ? nodeNames.join("|") : null; -} - -export function deriveAgentId(metadata: CallbackMetadata | null, _messages?: ChatMessage[] | null): string { - if (metadata) { - const langGraphId = deriveLangGraphAgentId(metadata); - if (langGraphId !== null) return langGraphId; - } +export function deriveAgentId(_metadata: CallbackMetadata | null, _messages?: ChatMessage[] | null): string { return "default"; } diff --git a/sdk/typescript/packages/core/src/index.ts b/sdk/typescript/packages/core/src/index.ts index 64e6a47..a422643 100644 --- a/sdk/typescript/packages/core/src/index.ts +++ b/sdk/typescript/packages/core/src/index.ts @@ -93,7 +93,7 @@ export { JSONLHandler } from "./handlers/jsonl.js"; export { HookRegistry } from "./hooks.js"; export { EventPairBuffer } from "./pairing.js"; export { AgentContextTracker, getInvocationId, runWithInvocationId } from "./context.js"; -export { deriveAgentId, deriveLangGraphAgentId } from "./identity.js"; +export { deriveAgentId } from "./identity.js"; export { WebSocketClient, shouldHalt } from "./ws.js"; export { AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE, assertToolCallsAllowed, gateToolCallIds } from "./policy.js"; export type { GateToolCallsReason, GateToolCallsResult } from "./policy.js"; diff --git a/sdk/typescript/packages/core/src/mcp.ts b/sdk/typescript/packages/core/src/mcp.ts index 55256f4..421519c 100644 --- a/sdk/typescript/packages/core/src/mcp.ts +++ b/sdk/typescript/packages/core/src/mcp.ts @@ -24,7 +24,7 @@ export function registerMcpConnection(name: string, connection: unknown): void { } export async function patchMcpAdapters(): Promise { - await Promise.allSettled([patchLangchainMcpAdapters(), patchMcpTransports()]); + await patchMcpTransports(); } function serverFromConnection(name: string, connection: unknown): McpServer { @@ -44,21 +44,6 @@ function endpointFor(transport: string, conn: Record): string { return ""; } -async function patchLangchainMcpAdapters(): Promise { - const mod = await importOptional("langchain-mcp-adapters"); - const Client = mod?.MultiServerMCPClient ?? mod?.client?.MultiServerMCPClient; - if (!Client || Client._adrianMcpPatched) return; - const original = Client.prototype.constructor; - const originalInit = Client.prototype.__init__ ?? original; - if (typeof originalInit !== "function") return; - Client.prototype.__init__ = function patchedInit(...args: unknown[]) { - const result = originalInit.apply(this, args); - registerAllFromClient(this as Record); - return result; - }; - Client._adrianMcpPatched = true; -} - async function patchMcpTransports(): Promise { const targets: Array<[string, string, string]> = [ ["@modelcontextprotocol/sdk/client/stdio.js", "stdio_client", "stdio"], @@ -77,12 +62,6 @@ async function patchMcpTransports(): Promise { } } -function registerAllFromClient(client: Record): void { - const connections = client.connections; - if (!connections || typeof connections !== "object") return; - for (const [name, connection] of Object.entries(connections)) registerMcpConnection(name, connection); -} - function registerSynthesised(transport: string, endpoint: string): void { if (!endpoint && transport === "unknown") return; if ([...servers.values()].some((server) => server.transport === transport && server.endpoint === endpoint)) return; diff --git a/sdk/typescript/packages/core/src/policy.ts b/sdk/typescript/packages/core/src/policy.ts index 5f9f1f1..837f929 100644 --- a/sdk/typescript/packages/core/src/policy.ts +++ b/sdk/typescript/packages/core/src/policy.ts @@ -1,7 +1,7 @@ import { currentConfig } from "./config.js"; import { shouldHalt, type WebSocketClient } from "./ws.js"; -/** Tool result content returned when LangGraph ToolNode execution is blocked. */ +/** Tool result content returned when tool execution is blocked by policy. */ export const BLOCKED_TOOL_MESSAGE = "[BLOCKED by security policy]"; export type GateToolCallsReason = "missing_tool_call_id" | "policy_halt" | "verdict_timeout"; diff --git a/sdk/typescript/packages/langchain/README.md b/sdk/typescript/packages/langchain/README.md deleted file mode 100644 index 004b1d2..0000000 --- a/sdk/typescript/packages/langchain/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# @secureagentics/adrian-langchain - -LangChain.js and LangGraph.js instrumentation for Adrian security monitoring. Includes `@secureagentics/adrian` as a dependency — no separate core install needed. - -## Install - -```bash -npm install @secureagentics/adrian-langchain @langchain/core @langchain/langgraph -``` - -```ts -import { init, adrian } from "@secureagentics/adrian-langchain"; -``` - -## Usage - -```ts -import { init, shutdown, adrian } from "@secureagentics/adrian-langchain"; - -await init({ apiKey: process.env.ADRIAN_API_KEY }); -await adrian(); - -// Your normal LangChain / LangGraph code runs here. - -await shutdown(); -``` - -After `adrian()`: - -- LangChain runnables and chat models receive the Adrian callback handler automatically. -- LangGraph graphs are wrapped with invocation tracking. -- ToolNode execution waits for BLOCK/HITL verdicts from the Adrian WebSocket. - -Unlike the OpenAI and Vercel packages, LangChain does not wrap a client object. Call `adrian()` once after `init()` to patch the framework globally. - -## Advanced: custom handler wiring - -```ts -import { init, getHandler, getWebSocketClient, adrianWith } from "@secureagentics/adrian-langchain"; - -await init(); -await adrianWith(getHandler, getWebSocketClient); -``` - -## API - -| Export | Description | -|---|---| -| `init(options?)` | Initialise Adrian (re-exported from core) | -| `shutdown()` | Tear down Adrian (re-exported from core) | -| `adrian()` | Enable LangChain / LangGraph instrumentation | -| `adrianWith(getHandler, getWebSocketClient)` | Enable instrumentation with custom handler accessors | -| `blockedToolNodeResponse(input, ws)` | Check ToolNode input against policy (used internally) | -| `extractToolCalls(input)` | Extract tool calls from graph state | -| `buildBlockedResponse(toolCalls)` | Build a blocked tool response message | - -See the [workspace README](../README.md) for environment variables and multi-provider setup. diff --git a/sdk/typescript/packages/langchain/package.json b/sdk/typescript/packages/langchain/package.json deleted file mode 100644 index eb0c153..0000000 --- a/sdk/typescript/packages/langchain/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@secureagentics/adrian-langchain", - "version": "1.0.0", - "description": "LangChain.js and LangGraph.js instrumentation for Adrian security monitoring.", - "license": "Apache-2.0", - "author": "Secure Agentics ", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist", - "README.md" - ], - "scripts": { - "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "test": "vitest run" - }, - "keywords": [ - "langchain", - "langgraph", - "ai", - "agents", - "security", - "monitoring" - ], - "dependencies": { - "@secureagentics/adrian": "^1.0.0" - }, - "peerDependencies": { - "@langchain/core": ">=0.3.0", - "@langchain/langgraph": ">=0.2.0" - }, - "peerDependenciesMeta": { - "@langchain/core": { - "optional": true - }, - "@langchain/langgraph": { - "optional": true - } - }, - "devDependencies": { - "@secureagentics/adrian": "file:../core", - "@types/node": "^25.9.1", - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "vitest": "^4.1.7" - } -} diff --git a/sdk/typescript/packages/langchain/src/index.ts b/sdk/typescript/packages/langchain/src/index.ts deleted file mode 100644 index ef16aea..0000000 --- a/sdk/typescript/packages/langchain/src/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { BLOCKED_TOOL_MESSAGE, currentConfig, gateToolCallIds, getHandler, getWebSocketClient, runWithInvocationId } from "@secureagentics/adrian"; -import type { AdrianCallbackHandler, WebSocketClient } from "@secureagentics/adrian"; - -let patched = false; -const CALLBACK_METHODS = ["invoke", "stream", "batch", "ainvoke", "astream"] as const; -const GRAPH_METHODS = ["invoke", "stream", "ainvoke", "astream"] as const; -const TOOL_NODE_METHODS = ["invoke", "ainvoke"] as const; - -/** Enable Adrian instrumentation for LangChain.js and LangGraph.js. */ -export async function adrian(): Promise { - return adrianWith(getHandler, getWebSocketClient); -} - -export async function adrianWith( - getHandlerFn: () => AdrianCallbackHandler | null, - getWebSocketClientFn: () => WebSocketClient | null, -): Promise { - if (patched) return; - patched = true; - await Promise.allSettled([ - patchRunnable(getHandlerFn), - patchBaseChatModel(getHandlerFn), - patchLangGraph(getHandlerFn), - patchToolNode(getHandlerFn, getWebSocketClientFn), - ]); -} - -function injectCallbacks(config: unknown, handler: AdrianCallbackHandler | null): unknown { - if (!handler) return config ?? {}; - const next = { ...((config && typeof config === "object") ? config as Record : {}) }; - const callbacks = next.callbacks; - if (Array.isArray(callbacks)) { - if (!callbacks.some((cb) => cb?.constructor?.name === "AdrianCallbackHandler")) next.callbacks = [handler, ...callbacks]; - } else if (callbacks) { - next.callbacks = [handler, callbacks]; - } else { - next.callbacks = [handler]; - } - return next; -} - -async function patchRunnable(getHandlerFn: () => AdrianCallbackHandler | null): Promise { - const mod = await importOptional("@langchain/core/runnables"); - const proto = (mod?.Runnable as { prototype?: Record; _adrianPatched?: boolean } | undefined)?.prototype; - if (!proto || (mod.Runnable as { _adrianPatched?: boolean })._adrianPatched) return; - for (const name of CALLBACK_METHODS) { - const original = proto[name]; - if (typeof original !== "function") continue; - proto[name] = function patchedInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - return original.call(this, input, injectCallbacks(config, getHandlerFn()), ...rest); - }; - } - (mod.Runnable as { _adrianPatched?: boolean })._adrianPatched = true; -} - -async function patchBaseChatModel(getHandlerFn: () => AdrianCallbackHandler | null): Promise { - const mod = await importOptional("@langchain/core/language_models/chat_models"); - const proto = (mod?.BaseChatModel as { prototype?: Record; _adrianPatched?: boolean } | undefined)?.prototype; - if (!proto || (mod.BaseChatModel as { _adrianPatched?: boolean })._adrianPatched) return; - for (const name of CALLBACK_METHODS) { - const original = proto[name]; - if (typeof original !== "function") continue; - proto[name] = function patchedChatInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - return original.call(this, input, injectCallbacks(config, getHandlerFn()), ...rest); - }; - } - (mod.BaseChatModel as { _adrianPatched?: boolean })._adrianPatched = true; -} - -async function patchLangGraph(getHandlerFn: () => AdrianCallbackHandler | null): Promise { - const mod = await importOptional("@langchain/langgraph"); - const graphClasses = [mod?.CompiledStateGraph, mod?.StateGraph, mod?.Pregel].filter(Boolean) as Array<{ prototype?: Record; _adrianPatched?: boolean }>; - for (const cls of graphClasses) { - const proto = cls.prototype; - if (!proto || cls._adrianPatched) continue; - for (const name of GRAPH_METHODS) { - const original = proto[name]; - if (typeof original !== "function") continue; - proto[name] = function patchedGraphInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - const run = () => original.call(this, input, injectCallbacks(config, getHandlerFn()), ...rest); - return runWithInvocationId(randomUUID(), run); - }; - } - cls._adrianPatched = true; - } -} - -async function patchToolNode(getHandlerFn: () => AdrianCallbackHandler | null, getWebSocketClientFn: () => WebSocketClient | null): Promise { - const mod = await importOptional("@langchain/langgraph/prebuilt"); - const cls = mod?.ToolNode as { prototype?: Record; _adrianPatched?: boolean } | undefined; - const proto = cls?.prototype; - if (!cls || !proto || cls._adrianPatched) return; - for (const name of TOOL_NODE_METHODS) { - const original = proto[name]; - if (typeof original !== "function") continue; - proto[name] = async function patchedToolNodeInvoke(input: unknown, config?: unknown, ...rest: unknown[]) { - const nextConfig = injectCallbacks(config, getHandlerFn()); - const blockedResponse = await blockedToolNodeResponse(input, getWebSocketClientFn()); - if (blockedResponse) return blockedResponse; - return original.call(this, input, nextConfig, ...rest); - }; - } - cls._adrianPatched = true; -} - -export async function blockedToolNodeResponse(input: unknown, ws: WebSocketClient | null): Promise<{ messages: Array> } | null> { - const toolCalls = extractToolCalls(input); - if (toolCalls.length === 0) return null; - const gate = await gateToolCallIds(toolCalls.map((call) => call.id), ws, currentConfig()?.blockTimeout ?? 30); - if (gate.action === "block") return buildBlockedResponse(toolCalls); - return null; -} - -export function extractToolCalls(input: unknown): Array<{ id: string; name: string }> { - const messages = Array.isArray(input) ? input : (input && typeof input === "object" ? (input as Record).messages : []); - if (!Array.isArray(messages)) return []; - for (const message of [...messages].reverse()) { - const calls = message && typeof message === "object" ? (message as Record).tool_calls ?? (message as Record).toolCalls : null; - if (Array.isArray(calls)) return calls.map((call) => ({ id: String((call as Record).id ?? ""), name: String((call as Record).name ?? "") })); - } - return []; -} - -export function buildBlockedResponse(toolCalls: Array<{ id: string; name: string }>): { messages: Array> } { - return { messages: toolCalls.map((call) => ({ content: BLOCKED_TOOL_MESSAGE, tool_call_id: call.id, name: call.name, type: "tool" })) }; -} - -async function importOptional(specifier: string): Promise { - try { - return await import(specifier); - } catch { - return null; - } -} - -export { - init, - shutdown, - getHandler, - getWebSocketClient, - version, - __version__, -} from "@secureagentics/adrian"; - -export type { EventData, InitOptions } from "@secureagentics/adrian"; diff --git a/sdk/typescript/packages/langchain/tests/langchain.test.ts b/sdk/typescript/packages/langchain/tests/langchain.test.ts deleted file mode 100644 index 6572136..0000000 --- a/sdk/typescript/packages/langchain/tests/langchain.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { init, shutdown, type EventData } from "@secureagentics/adrian"; -import { setConfig, Mode, type Verdict, type WebSocketClient } from "@secureagentics/adrian"; -import { blockedToolNodeResponse } from "../src/index.js"; - -function config(): Parameters[0] { - return { - apiKey: null, - logFile: "events.jsonl", - logLevel: null, - sessionId: "sess", - wsUrl: null, - blockTimeout: 5, - onEvent: null, - onVerdict: null, - onBlock: null, - onAudit: null, - onDisconnect: null, - onReconnect: null, - onMcpServer: null, - replayBufferFrames: 1000, - }; -} - -function verdict(eventId: string, halt: boolean): Verdict { - return { - eventId, - sessionId: "sess", - madCode: "M3_TEST", - policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, - hitl: null, - }; -} - -describe("ToolNode policy gating", () => { - afterEach(() => setConfig(null)); - - it("waits for policy readiness and evaluates every tool call verdict", async () => { - setConfig(config()); - const waitedFor: string[] = []; - let waitedForPolicy = false; - const ws = { - waitForPolicyReady: async () => { - waitedForPolicy = true; - return true; - }, - policyActive: () => true, - blockTimeout: (seconds: number) => seconds, - waitForToolCallVerdict: async (toolCallId: string) => { - waitedFor.push(toolCallId); - return verdict(`event-${toolCallId}`, toolCallId === "call-2"); - }, - } as unknown as WebSocketClient; - - const response = await blockedToolNodeResponse({ - messages: [{ tool_calls: [{ id: "call-1", name: "search" }, { id: "call-2", name: "write" }] }], - }, ws); - - expect(waitedForPolicy).toBe(true); - expect(waitedFor).toEqual(["call-1", "call-2"]); - expect(response?.messages).toHaveLength(2); - expect(response?.messages[0]?.content).toContain("BLOCKED"); - }); - - it("blocks ToolNode when policy halts a tool call", async () => { - setConfig(config()); - const ws = { - waitForPolicyReady: async () => true, - policyActive: () => true, - blockTimeout: (seconds: number) => seconds, - waitForToolCallVerdict: async (toolCallId: string) => verdict(`event-${toolCallId}`, true), - } as unknown as WebSocketClient; - - const response = await blockedToolNodeResponse({ - messages: [{ tool_calls: [{ id: "call-weather", name: "get_weather" }] }], - }, ws); - - expect(response?.messages).toHaveLength(1); - expect(response?.messages[0]?.content).toContain("BLOCKED"); - expect(response?.messages[0]?.tool_call_id).toBe("call-weather"); - }); - - it("does not block when all correlated verdicts allow execution", async () => { - setConfig(config()); - const ws = { - waitForPolicyReady: async () => true, - policyActive: () => true, - blockTimeout: (seconds: number) => seconds, - waitForToolCallVerdict: async (toolCallId: string) => verdict(`event-${toolCallId}`, false), - } as unknown as WebSocketClient; - - await expect(blockedToolNodeResponse({ - messages: [{ toolCalls: [{ id: "call-1", name: "search" }, { id: "call-2", name: "write" }] }], - }, ws)).resolves.toBeNull(); - }); -}); diff --git a/sdk/typescript/packages/langchain/tsconfig.build.json b/sdk/typescript/packages/langchain/tsconfig.build.json deleted file mode 100644 index d7b2c67..0000000 --- a/sdk/typescript/packages/langchain/tsconfig.build.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true, - "outDir": "dist" - }, - "include": ["src/**/*.ts"], - "exclude": ["tests/**/*.ts"] -} diff --git a/sdk/typescript/packages/langchain/tsconfig.json b/sdk/typescript/packages/langchain/tsconfig.json deleted file mode 100644 index 6140ae3..0000000 --- a/sdk/typescript/packages/langchain/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "." - }, - "include": ["src/**/*.ts", "tests/**/*.ts"] -} From f765f033c8a17c079ed59a755e94d79fc68a83bb Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Thu, 4 Jun 2026 01:15:58 +0530 Subject: [PATCH 07/10] updating contributing --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1c977a..c902abe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ source .venv/bin/activate pre-commit install # wires the git hook ``` -The TypeScript SDK lives at `sdk/typescript/` as an npm workspace with four packages (`core`, `openai`, `vercel`, `langchain`). From that directory: +The TypeScript SDK lives at `sdk/typescript/` as an npm workspace with three packages (`core`, `openai`, `vercel`). From that directory: ```sh cd sdk/typescript From a4f4c5f699cdb8986959de3a4b5cc35283d54fc7 Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Mon, 8 Jun 2026 21:05:12 +0530 Subject: [PATCH 08/10] openai-ts-sdk --- CONTRIBUTING.md | 2 +- sdk/typescript/README.md | 67 +---- sdk/typescript/package-lock.json | 27 -- sdk/typescript/packages/core/README.md | 2 +- sdk/typescript/packages/vercel/README.md | 94 ------- sdk/typescript/packages/vercel/package.json | 52 ---- sdk/typescript/packages/vercel/src/index.ts | 191 -------------- .../packages/vercel/tests/vercel.test.ts | 233 ------------------ .../packages/vercel/tsconfig.build.json | 12 - sdk/typescript/packages/vercel/tsconfig.json | 8 - 10 files changed, 10 insertions(+), 678 deletions(-) delete mode 100644 sdk/typescript/packages/vercel/README.md delete mode 100644 sdk/typescript/packages/vercel/package.json delete mode 100644 sdk/typescript/packages/vercel/src/index.ts delete mode 100644 sdk/typescript/packages/vercel/tests/vercel.test.ts delete mode 100644 sdk/typescript/packages/vercel/tsconfig.build.json delete mode 100644 sdk/typescript/packages/vercel/tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c902abe..a85c488 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ source .venv/bin/activate pre-commit install # wires the git hook ``` -The TypeScript SDK lives at `sdk/typescript/` as an npm workspace with three packages (`core`, `openai`, `vercel`). From that directory: +The TypeScript SDK lives at `sdk/typescript/` as an npm workspace with two packages (`core`, `openai`). From that directory: ```sh cd sdk/typescript diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 6aac433..5c1c92c 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -1,13 +1,12 @@ # Adrian TypeScript SDK -Monorepo for the Adrian TypeScript SDK. Pick the package for your framework — the core SDK is installed automatically. +Monorepo for the Adrian TypeScript SDK. Pick the package for your framework - the core SDK is installed automatically. ## Packages | Package | npm name | Install | Import | |---|---|---|---| | OpenAI | `@secureagentics/adrian-openai` | `npm install @secureagentics/adrian-openai openai` | `import { init, adrian, captureTool } from "@secureagentics/adrian-openai"` | -| Vercel AI | `@secureagentics/adrian-vercel` | `npm install @secureagentics/adrian-vercel ai` | `import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"` | | Core only | `@secureagentics/adrian` | `npm install @secureagentics/adrian` | `import { init, shutdown } from "@secureagentics/adrian"` | Provider packages depend on `@secureagentics/adrian` and re-export `init`, `shutdown`, and other core APIs — one install, one import. @@ -20,25 +19,20 @@ Provider packages depend on `@secureagentics/adrian` and re-export `init`, `shut Both come from the same provider package: ```ts -// OpenAI import { init, adrian, captureTool } from "@secureagentics/adrian-openai"; -// Vercel AI -import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; - await init({ apiKey: process.env.ADRIAN_API_KEY }); ``` ## Unified API -All provider packages export the **same function names**: +The OpenAI provider package exports the familiar Adrian entrypoints: -| Export | OpenAI | Vercel AI | -|---|---|---| -| `init` / `shutdown` | Re-exported from core | Re-exported from core | -| `adrian(...)` | Wrap an OpenAI client | Wrap an AI module or tools object | -| `adrianTools(...)` | — | Wrap tool definitions for `generateText` | -| `captureTool(...)` | Capture manual tool execution | Capture manual tool execution | +| Export | Purpose | +|---|---| +| `init` / `shutdown` | Re-exported from core | +| `adrian(...)` | Wrap an OpenAI client | +| `captureTool(...)` | Capture manual tool execution | Shared option types (same names in every provider package): @@ -76,51 +70,6 @@ for (const toolCall of response.choices[0].message.tool_calls ?? []) { await shutdown(); ``` -### Vercel AI SDK - -```bash -npm install @secureagentics/adrian-vercel ai -``` - -```ts -import * as ai from "ai"; -import { init, shutdown, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; - -await init({ apiKey: process.env.ADRIAN_API_KEY }); - -const monitored = adrian(ai); - -const result = await monitored.generateText({ - model, - prompt: "What's the weather in London?", - tools: adrianTools(tools), -}); - -for (const toolCall of result.toolCalls ?? []) { - await captureTool(toolCall, () => runTool(toolCall.toolName, toolCall.args)); -} - -await shutdown(); -``` - -## Using multiple providers - -Install each provider package you need. Core is deduplicated to a single copy: - -```bash -npm install @secureagentics/adrian-openai @secureagentics/adrian-vercel openai ai -``` - -```ts -import { init, adrian as adrianOpenAI } from "@secureagentics/adrian-openai"; -import { adrian as adrianVercel } from "@secureagentics/adrian-vercel"; - -await init({ apiKey: process.env.ADRIAN_API_KEY }); - -const openai = adrianOpenAI(new OpenAI()); -const monitored = adrianVercel(vercelAi); -``` - ## Environment variables | Variable | Description | @@ -147,4 +96,4 @@ npm run build -w @secureagentics/adrian npm test -w @secureagentics/adrian-openai ``` -Per-package docs: [core](./packages/core) · [openai](./packages/openai) · [vercel](./packages/vercel) +Per-package docs: [core](./packages/core) · [openai](./packages/openai) diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index 71b4070..d39fb7f 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -1183,10 +1183,6 @@ "resolved": "packages/openai", "link": true }, - "node_modules/@secureagentics/adrian-vercel": { - "resolved": "packages/vercel", - "link": true - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2735,29 +2731,6 @@ "optional": true } } - }, - "packages/vercel": { - "name": "@secureagentics/adrian-vercel", - "version": "1.0.0", - "license": "Apache-2.0", - "dependencies": { - "@secureagentics/adrian": "^1.0.0" - }, - "devDependencies": { - "@secureagentics/adrian": "file:../core", - "@types/node": "^25.9.1", - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "vitest": "^4.1.7" - }, - "peerDependencies": { - "ai": ">=4.0.0" - }, - "peerDependenciesMeta": { - "ai": { - "optional": true - } - } } } } diff --git a/sdk/typescript/packages/core/README.md b/sdk/typescript/packages/core/README.md index 38fe0c9..2e686e7 100644 --- a/sdk/typescript/packages/core/README.md +++ b/sdk/typescript/packages/core/README.md @@ -60,4 +60,4 @@ const handler = getHandler(); ## Subpath export -`@secureagentics/adrian/capture` exposes shared LLM capture helpers used internally by the OpenAI and Vercel provider packages. Most apps should use `adrian()` from a provider package instead. +`@secureagentics/adrian/capture` exposes shared LLM capture helpers used internally by provider packages. Most apps should use `adrian()` from a provider package instead. diff --git a/sdk/typescript/packages/vercel/README.md b/sdk/typescript/packages/vercel/README.md deleted file mode 100644 index f3780b3..0000000 --- a/sdk/typescript/packages/vercel/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# @secureagentics/adrian-vercel - -Vercel AI SDK instrumentation for Adrian security monitoring. Includes `@secureagentics/adrian` as a dependency — no separate core install needed. - -## Install - -```bash -npm install @secureagentics/adrian-vercel ai -``` - -```ts -import { init, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; -``` - -## Usage - -```ts -import * as ai from "ai"; -import { init, shutdown, adrian, adrianTools, captureTool } from "@secureagentics/adrian-vercel"; - -await init({ apiKey: process.env.ADRIAN_API_KEY }); - -const ai = adrian(ai); - -await ai.generateText({ - model, - prompt: "Hello", -}); - -await shutdown(); -``` - -`adrian()` wraps `generateText`, `streamText`, `generateObject`, and `streamObject`. - -With **BLOCK** or **HITL** policy and a live WebSocket, `generateText` / streaming calls wait for verdicts when the model returns tool calls, and `captureTool` / `adrianTools` wait before running `execute`. A halt throws `AdrianPolicyBlockedError`. - -## Tools - -Pass wrapped tool definitions to `generateText`: - -```ts -const result = await ai.generateText({ - model, - prompt: "Use the weather tool.", - tools: adrianTools(tools), -}); -``` - -`adrian()` also accepts a plain tools object directly (auto-detected when every key has `execute` or `description`): - -```ts -const tools = adrian({ - getWeather: { - description: "Get weather for a city", - execute: async ({ city }) => ({ city, temp: 72 }), - }, -}); - -await tools.getWeather.execute({ city: "London" }); -``` - -## Manual tool capture - -When you execute Vercel AI tool calls yourself: - -```ts -for (const toolCall of result.toolCalls ?? []) { - await captureTool(toolCall, () => runTool(toolCall.toolName, toolCall.args)); -} -``` - -## API - - -| Export | Description | -| ------------------------------------------ | ---------------------------------------------- | -| `init(options?)` | Initialise Adrian (re-exported from core) | -| `shutdown()` | Tear down Adrian (re-exported from core) | -| `adrian(target, options?)` | Wrap a Vercel AI module or tools object | -| `adrianTools(tools, options?)` | Wrap tool definitions passed to `generateText` | -| `captureTool(toolCall, execute, options?)` | Capture manual tool execution | - - -### Types - - -| Type | Fields | -| -------------------- | ----------------------------------- | -| `AdrianOptions` | `metadata?` | -| `ToolCallLike` | `toolCallId`, `toolName`, `args`, … | -| `ToolCaptureOptions` | `metadata?`, `parentRunId?` | - - -See the [workspace README](../README.md) for environment variables and multi-provider setup. diff --git a/sdk/typescript/packages/vercel/package.json b/sdk/typescript/packages/vercel/package.json deleted file mode 100644 index 3f3a2a8..0000000 --- a/sdk/typescript/packages/vercel/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@secureagentics/adrian-vercel", - "version": "1.0.0", - "description": "Vercel AI SDK instrumentation for Adrian security monitoring.", - "license": "Apache-2.0", - "author": "Secure Agentics ", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist", - "README.md" - ], - "scripts": { - "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "test": "vitest run" - }, - "keywords": [ - "vercel-ai", - "ai", - "agents", - "security", - "monitoring" - ], - "dependencies": { - "@secureagentics/adrian": "^1.0.0" - }, - "peerDependencies": { - "ai": ">=4.0.0" - }, - "peerDependenciesMeta": { - "ai": { - "optional": true - } - }, - "devDependencies": { - "@secureagentics/adrian": "file:../core", - "@types/node": "^25.9.1", - "tsup": "^8.5.1", - "typescript": "^6.0.3", - "vitest": "^4.1.7" - } -} diff --git a/sdk/typescript/packages/vercel/src/index.ts b/sdk/typescript/packages/vercel/src/index.ts deleted file mode 100644 index 64a6e61..0000000 --- a/sdk/typescript/packages/vercel/src/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { assertToolCallsAllowed, currentConfig, getHandler, getWebSocketClient, runWithInvocationId } from "@secureagentics/adrian"; -import type { CallbackMetadata, LlmEndData, ToolCallRecord } from "@secureagentics/adrian"; -import { captureLlmCall, gateLlmEndData, emptyLlmEnd, messagesFromPromptLike, normalizeUsage, parseToolArgs, stringifyContent } from "@secureagentics/adrian/capture"; - -export interface AdrianOptions { - metadata?: CallbackMetadata | null; -} - -export interface ToolCallLike { - toolCallId?: string; - id?: string; - toolName?: string; - name?: string; - args?: unknown; -} - -export interface ToolCaptureOptions { - metadata?: CallbackMetadata | null; - parentRunId?: string; -} - -type VercelToolExecute = (args: unknown, options?: unknown, ...rest: unknown[]) => unknown; - -const VERCEL_METHODS = new Set(["generateText", "streamText", "generateObject", "streamObject"]); - -/** Wrap a Vercel AI SDK module or tools object so Adrian captures LLM and tool events. */ -export function adrian(target: T, options: AdrianOptions = {}): T { - if (!target || typeof target !== "object") return target; - - if ("generateText" in target || "streamText" in target) { - return wrapAiModule(target as Record, options) as T; - } - - const keys = Object.keys(target); - if (keys.length > 0 && keys.every((k) => { - const val = (target as Record)[k]; - return val && typeof val === "object" && ("execute" in val || "description" in val); - })) { - return adrianTools(target as Record, options) as T; - } - - return target; -} - -/** Wrap Vercel AI SDK tool definitions so Adrian captures tool execution events. */ -export function adrianTools>(tools: T, options: ToolCaptureOptions = {}): T { - return Object.fromEntries(Object.entries(tools).map(([toolName, toolDef]) => { - if (!toolDef || typeof toolDef !== "object") return [toolName, toolDef]; - const execute = (toolDef as { execute?: unknown }).execute; - if (typeof execute !== "function") return [toolName, toolDef]; - - return [toolName, { - ...(toolDef as Record), - execute(this: unknown, args: unknown, executionOptions?: unknown, ...rest: unknown[]) { - const toolCallId = extractToolCallId(executionOptions); - return captureTool({ - toolCallId, - toolName, - args, - }, () => (execute as VercelToolExecute).call(this, args, executionOptions, ...rest), options); - }, - }]; - })) as T; -} - -/** Wrap manual Vercel AI SDK tool execution so Adrian captures tool events. */ -export async function captureTool( - toolCall: ToolCallLike, - execute: () => T | Promise, - options: ToolCaptureOptions = {}, -): Promise { - const handler = getHandler(); - if (!handler) return execute(); - - const runId = randomUUID(); - const toolName = String(toolCall.toolName ?? toolCall.name ?? "unknown"); - const toolCallId = String(toolCall.toolCallId ?? toolCall.id ?? ""); - const input = stringifyContent(toolCall.args); - const metadata = integrationMetadata(options.metadata, "vercel-ai.tool_call"); - - await assertToolCallsAllowed(toolCallId ? [toolCallId] : [], getWebSocketClient(), currentConfig()?.blockTimeout ?? 30); - - return runWithInvocationId(randomUUID(), async () => { - await handler.handleToolStart({ name: toolName }, input, runId, options.parentRunId, { metadata, toolCallId }); - try { - const result = await execute(); - await handler.handleToolEnd(result, runId); - return result; - } catch (error) { - await handler.handleToolError(error, runId); - throw error; - } - }); -} - -function wrapAiModule>(ai: T, options: AdrianOptions = {}): T { - return new Proxy(ai, { - get(target, prop, receiver) { - const value = Reflect.get(target, prop, receiver); - if (!VERCEL_METHODS.has(String(prop)) || typeof value !== "function") return value; - return function adrianVercelAI(this: unknown, args: Record = {}, ...rest: unknown[]) { - return captureVercelCall(String(prop), () => value.call(this, args, ...rest), args, options); - }; - }, - }); -} - -function captureVercelCall(operation: string, execute: () => T, args: Record, options: AdrianOptions): unknown { - const model = extractModelName(args.model); - const metadata = integrationMetadata(options.metadata, operation); - const result = execute(); - - if (operation.startsWith("stream")) { - void Promise.resolve(result).then((resolved) => emitVercelStreamResult(model, args, metadata, resolved)).catch(() => undefined); - return result; - } - - return Promise.resolve(result).then((resolved) => captureLlmCall(getHandler, { model, messages: messagesFromPromptLike(args), metadata }, async () => resolved, extractVercelResult, gateLlmEndData)); -} - -async function emitVercelStreamResult(model: string, args: Record, metadata: CallbackMetadata, result: unknown): Promise { - await captureLlmCall(getHandler, { model, messages: messagesFromPromptLike(args), metadata }, async () => result, async (streamResult) => { - const obj = streamResult && typeof streamResult === "object" ? streamResult as Record : {}; - const [text, toolCalls, usage] = await Promise.all([ - resolveMaybe(obj.text, ""), - resolveMaybe(obj.toolCalls, []), - resolveMaybe(obj.usage, null), - ]); - return emptyLlmEnd(typeof text === "string" ? text : stringifyContent(text), normalizeVercelToolCalls(toolCalls), normalizeVercelUsage(usage)); - }, gateLlmEndData); -} - -function extractVercelResult(result: unknown): LlmEndData { - const obj = result && typeof result === "object" ? result as Record : {}; - const output = typeof obj.text === "string" ? obj.text : typeof obj.object !== "undefined" ? stringifyContent(obj.object) : ""; - return emptyLlmEnd(output, normalizeVercelToolCalls(obj.toolCalls), normalizeVercelUsage(obj.usage)); -} - -function normalizeVercelToolCalls(raw: unknown): ToolCallRecord[] { - if (!Array.isArray(raw)) return []; - return raw.map((call) => { - const obj = call && typeof call === "object" ? call as Record : {}; - return { - id: String(obj.toolCallId ?? obj.id ?? ""), - name: String(obj.toolName ?? obj.name ?? ""), - args: parseToolArgs(obj.args), - }; - }); -} - -function normalizeVercelUsage(usage: unknown): LlmEndData["usage"] { - return normalizeUsage(usage, ["promptTokens", "inputTokens"], ["completionTokens", "outputTokens"]); -} - -async function resolveMaybe(value: unknown, fallback: unknown): Promise { - if (value === undefined || value === null) return fallback; - return Promise.resolve(value); -} - -function extractModelName(model: unknown): string { - if (typeof model === "string") return model; - if (model && typeof model === "object") { - const obj = model as Record; - return String(obj.modelId ?? obj.model ?? obj.id ?? obj.name ?? "vercel-ai"); - } - return "vercel-ai"; -} - -function extractToolCallId(executionOptions: unknown): string { - if (!executionOptions || typeof executionOptions !== "object") return ""; - const obj = executionOptions as Record; - return String(obj.toolCallId ?? obj.id ?? ""); -} - -function integrationMetadata(metadata: CallbackMetadata | null | undefined, operation: string): CallbackMetadata { - return { ...(metadata ?? {}), adrianIntegration: "vercel-ai", operation }; -} - -export { - AdrianPolicyBlockedError, - BLOCKED_TOOL_MESSAGE, - init, - shutdown, - getHandler, - getWebSocketClient, - version, - __version__, -} from "@secureagentics/adrian"; - -export type { EventData, InitOptions } from "@secureagentics/adrian"; diff --git a/sdk/typescript/packages/vercel/tests/vercel.test.ts b/sdk/typescript/packages/vercel/tests/vercel.test.ts deleted file mode 100644 index e23cea9..0000000 --- a/sdk/typescript/packages/vercel/tests/vercel.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import * as adrianCore from "@secureagentics/adrian"; -import { AdrianPolicyBlockedError, init, Mode, shutdown, type EventData, type Verdict, type WebSocketClient } from "@secureagentics/adrian"; -import { captureTool, adrian, adrianTools } from "../src/index.js"; - -function mockWs(halt: boolean): WebSocketClient { - return { - waitForPolicyReady: async () => true, - policyActive: () => true, - blockTimeout: (seconds: number) => seconds, - waitForToolCallVerdict: async (toolCallId: string) => ({ - eventId: `event-${toolCallId}`, - sessionId: "sess", - madCode: "M3_TEST", - policy: { mode: Mode.MODE_BLOCK, policyM0: false, policyM2: false, policyM3: halt, policyM4: false }, - hitl: null, - } satisfies Verdict), - } as unknown as WebSocketClient; -} - -describe("Vercel AI SDK instrumentation", () => { - afterEach(async () => { - vi.restoreAllMocks(); - await shutdown(); - }); - - it("captures generateText calls as paired LLM events", async () => { - const events: EventData[] = []; - const ai = adrian({ - generateText: async (_args: Record) => ({ - text: "hello from vercel", - toolCalls: [{ toolCallId: "tool-1", toolName: "search", args: { query: "adrian" } }], - usage: { promptTokens: 8, completionTokens: 9, totalTokens: 17 }, - }), - }); - - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { - events.push(data); - } }); - const result = await ai.generateText({ - model: { modelId: "openai/gpt-4o-mini" }, - system: "be brief", - prompt: "hello", - }); - - expect(result.text).toBe("hello from vercel"); - expect(events).toHaveLength(1); - expect(events[0]).toMatchObject({ - kind: "llm", - model: "openai/gpt-4o-mini", - output: "hello from vercel", - toolCalls: [{ id: "tool-1", name: "search", args: { query: "adrian" } }], - usage: { promptTokens: 8, completionTokens: 9, totalTokens: 17 }, - }); - }); - - it("emits streamText events when the result promises settle", async () => { - const events: EventData[] = []; - const ai = adrian({ - streamText: (_args: Record) => ({ - text: Promise.resolve("streamed"), - toolCalls: Promise.resolve([]), - usage: Promise.resolve({ inputTokens: 2, outputTokens: 3, totalTokens: 5 }), - }), - }); - - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { - events.push(data); - } }); - const result = await ai.streamText({ model: "gpt-4o", messages: [{ role: "user", content: "hi" }] }); - await result.text; - - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(events[0]).toMatchObject({ - kind: "llm", - model: "gpt-4o", - output: "streamed", - usage: { promptTokens: 2, completionTokens: 3, totalTokens: 5 }, - }); - }); - - it("blocks captureTool when policy halts", async () => { - await init({ handlers: [], sessionId: "sess", wsUrl: null, blockTimeout: 5 }); - vi.spyOn(adrianCore, "getWebSocketClient").mockReturnValue(mockWs(true)); - - let executed = false; - await expect(captureTool({ - toolCallId: "tool-weather", - toolName: "getWeather", - args: { city: "SF" }, - }, async () => { - executed = true; - return { ok: true }; - })).rejects.toBeInstanceOf(AdrianPolicyBlockedError); - - expect(executed).toBe(false); - }); - - it("blocks adrianTools execute when policy halts", async () => { - await init({ handlers: [], sessionId: "sess", wsUrl: null, blockTimeout: 5 }); - vi.spyOn(adrianCore, "getWebSocketClient").mockReturnValue(mockWs(true)); - - const tools = adrianTools({ - getWeather: { - description: "Get weather", - execute: async () => ({ temp: 72 }), - }, - }); - - await expect(tools.getWeather.execute({ city: "SF" }, { toolCallId: "tool-weather" })).rejects.toBeInstanceOf(AdrianPolicyBlockedError); - }); - - it("captures local Vercel AI tool execution as a tool event", async () => { - const events: Array<{ type: string; data: EventData }> = []; - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { - events.push({ type, data }); - } }); - - const result = await captureTool({ - toolCallId: "tool-weather", - toolName: "getWeather", - args: { city: "San Francisco" }, - }, async () => ({ temperatureF: 58, condition: "cloudy" })); - - expect(result).toEqual({ temperatureF: 58, condition: "cloudy" }); - expect(events).toHaveLength(1); - expect(events[0]).toMatchObject({ - type: "tool", - data: { - kind: "tool", - toolName: "getWeather", - toolCallId: "tool-weather", - input: "{\"city\":\"San Francisco\"}", - output: "{\"temperatureF\":58,\"condition\":\"cloudy\"}", - }, - }); - }); - - it("captures local Vercel AI tool errors as tool events", async () => { - const events: Array<{ type: string; data: EventData }> = []; - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { - events.push({ type, data }); - } }); - - await expect(captureTool({ - toolCallId: "tool-weather", - toolName: "getWeather", - args: { city: "San Francisco" }, - }, async () => { - throw new Error("weather API unavailable"); - })).rejects.toThrow("weather API unavailable"); - - expect(events[0]).toMatchObject({ - type: "tool", - data: { - kind: "tool", - toolName: "getWeather", - toolCallId: "tool-weather", - output: "[ERROR] Error: weather API unavailable", - error: { name: "Error", message: "weather API unavailable" }, - }, - }); - }); - - it("wraps Vercel AI SDK tool execute functions", async () => { - const events: Array<{ type: string; data: EventData }> = []; - const tools = adrianTools({ - getWeather: { - description: "Get current weather for a city.", - execute: async ({ city }: { city: string }, _options?: unknown) => ({ city, temperatureF: 58 }), - }, - }); - - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { - events.push({ type, data }); - } }); - - const result = await tools.getWeather.execute({ city: "San Francisco" }, { toolCallId: "tool-weather" }); - - expect(result).toEqual({ city: "San Francisco", temperatureF: 58 }); - expect(events[0]).toMatchObject({ - type: "tool", - data: { - kind: "tool", - toolName: "getWeather", - toolCallId: "tool-weather", - input: "{\"city\":\"San Francisco\"}", - output: "{\"city\":\"San Francisco\",\"temperatureF\":58}", - }, - }); - }); - - it("wraps Vercel AI SDK via adrian()", async () => { - const events: EventData[] = []; - const ai = adrian({ - generateText: async () => ({ - text: "hello from vercel", - }), - }); - - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { - events.push(data); - } }); - - await ai.generateText({ - model: "gpt-4o", - prompt: "hello", - }); - - expect(events).toHaveLength(1); - expect(events[0]).toMatchObject({ model: "gpt-4o", output: "hello from vercel" }); - }); - - it("wraps Vercel tools via adrian()", async () => { - const events: Array<{ type: string; data: EventData }> = []; - const tools = adrian({ - getWeather: { - description: "Get weather", - execute: async ({ city }: { city: string }) => ({ city, temp: 72 }), - }, - }); - - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (type, data) => { - events.push({ type, data }); - } }); - - await tools.getWeather.execute({ city: "SF" }); - - expect(events).toHaveLength(1); - expect(events[0].type).toBe("tool"); - expect(events[0].data).toMatchObject({ toolName: "getWeather", input: '{"city":"SF"}' }); - }); -}); diff --git a/sdk/typescript/packages/vercel/tsconfig.build.json b/sdk/typescript/packages/vercel/tsconfig.build.json deleted file mode 100644 index d7b2c67..0000000 --- a/sdk/typescript/packages/vercel/tsconfig.build.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true, - "outDir": "dist" - }, - "include": ["src/**/*.ts"], - "exclude": ["tests/**/*.ts"] -} diff --git a/sdk/typescript/packages/vercel/tsconfig.json b/sdk/typescript/packages/vercel/tsconfig.json deleted file mode 100644 index 6140ae3..0000000 --- a/sdk/typescript/packages/vercel/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "." - }, - "include": ["src/**/*.ts", "tests/**/*.ts"] -} From 9e83a97d9678f3f355d2efd117257328ba32e73c Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Mon, 8 Jun 2026 23:01:31 +0530 Subject: [PATCH 09/10] updated docs added --- sdk/typescript/README.md | 111 ++++++++++++++++++++++- sdk/typescript/packages/core/README.md | 97 ++++++++++++++++++-- sdk/typescript/packages/openai/README.md | 76 +++++++++++++++- 3 files changed, 270 insertions(+), 14 deletions(-) diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md index 5c1c92c..4c02612 100644 --- a/sdk/typescript/README.md +++ b/sdk/typescript/README.md @@ -2,6 +2,8 @@ Monorepo for the Adrian TypeScript SDK. Pick the package for your framework - the core SDK is installed automatically. +The core package owns the event pipeline: event pairing, PII redaction, JSONL logging, WebSocket streaming, policy verdicts, and shared capture helpers. Provider packages, such as OpenAI, adapt framework-specific request and response shapes into that core pipeline. + ## Packages | Package | npm name | Install | Import | @@ -52,7 +54,7 @@ npm install @secureagentics/adrian-openai openai ```ts import OpenAI from "openai"; -import { init, shutdown, adrian, captureTool } from "@secureagentics/adrian-openai"; +import { init, shutdown, adrian } from "@secureagentics/adrian-openai"; await init({ apiKey: process.env.ADRIAN_API_KEY }); @@ -63,13 +65,116 @@ const response = await openai.chat.completions.create({ messages: [{ role: "user", content: "Hello" }], }); -for (const toolCall of response.choices[0].message.tool_calls ?? []) { - await captureTool(toolCall, () => runTool(toolCall.function.name, toolCall.function.arguments)); +await shutdown(); +``` + +### OpenAI tool execution + +OpenAI returns tool call requests; your app still executes the tools. Wrap that execution with `captureTool` so Adrian can apply BLOCK/HITL policy and capture the tool result: + +```ts +import OpenAI from "openai"; +import { init, shutdown, adrian, captureTool, AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE } from "@secureagentics/adrian-openai"; + +await init({ apiKey: process.env.ADRIAN_API_KEY }); + +const openai = adrian(new OpenAI()); +const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "user", content: "What is the weather in Paris?" }, +]; + +async function getWeather(city: string) { + return { city, forecast: "sunny" }; +} + +const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages, + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather for a city", + parameters: { + type: "object", + properties: { city: { type: "string" } }, + required: ["city"], + }, + }, + }, + ], +}); + +const assistantMessage = response.choices[0]?.message; +if (!assistantMessage) throw new Error("OpenAI response did not include an assistant message"); + +messages.push(assistantMessage); + +for (const toolCall of assistantMessage.tool_calls ?? []) { + let toolResult: unknown; + + try { + toolResult = await captureTool(toolCall, async () => { + const args = JSON.parse(toolCall.function.arguments || "{}") as { city?: string }; + return getWeather(args.city ?? ""); + }); + } catch (error) { + if (!(error instanceof AdrianPolicyBlockedError)) throw error; + toolResult = BLOCKED_TOOL_MESSAGE; + } + + messages.push({ + role: "tool", + tool_call_id: toolCall.id, + content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult), + }); } await shutdown(); ``` +### Responses API + +```ts +const response = await openai.responses.create({ + model: "gpt-4o-mini", + input: "Summarize the security considerations for this workflow.", +}); + +console.log(response.output_text); +``` + +### Streaming + +Streaming calls are passed through unchanged. Adrian emits one paired event when the stream finishes or the consumer exits early: + +```ts +const stream = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Write a short haiku." }], + stream: true, +}); + +for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); +} +``` + +### Local logging only + +Use `wsUrl: null` when you want JSONL logging without connecting to the Adrian backend: + +```ts +await init({ + wsUrl: null, + logFile: "events.jsonl", + onEvent: (eventType, data, runId, parentRunId, eventId) => { + console.log({ eventType, runId, parentRunId, eventId, data }); + }, +}); +``` + ## Environment variables | Variable | Description | diff --git a/sdk/typescript/packages/core/README.md b/sdk/typescript/packages/core/README.md index 2e686e7..eb9f1b1 100644 --- a/sdk/typescript/packages/core/README.md +++ b/sdk/typescript/packages/core/README.md @@ -21,10 +21,15 @@ npm install @secureagentics/adrian ## Quick start (via a provider package) ```ts +import OpenAI from "openai"; import { init, shutdown, adrian } from "@secureagentics/adrian-openai"; await init({ apiKey: process.env.ADRIAN_API_KEY }); const openai = adrian(new OpenAI()); +await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Hello" }], +}); await shutdown(); ``` @@ -41,12 +46,28 @@ await shutdown(); ## Environment -- `ADRIAN_API_KEY` -- `ADRIAN_LOG_FILE` -- `ADRIAN_WS_URL` -- `ADRIAN_SESSION_ID` -- `ADRIAN_BLOCK_TIMEOUT` -- `ADRIAN_REPLAY_BUFFER_FRAMES` +- `ADRIAN_API_KEY` — API key used for WebSocket authentication. +- `ADRIAN_LOG_FILE` — local JSONL log path. Defaults to `events.jsonl`. +- `ADRIAN_WS_URL` — Adrian WebSocket endpoint. Defaults to `ws://localhost:8080/ws`. +- `ADRIAN_SESSION_ID` — session identifier for grouping events. +- `ADRIAN_BLOCK_TIMEOUT` — seconds to wait for BLOCK/HITL verdicts. +- `ADRIAN_REPLAY_BUFFER_FRAMES` — number of WebSocket frames buffered while disconnected. + +Set `wsUrl: null` in `init()` for local JSONL logging without a WebSocket connection: + +```ts +import { init, shutdown } from "@secureagentics/adrian"; + +await init({ + wsUrl: null, + logFile: "events.jsonl", + onEvent: (eventType, data, runId, parentRunId, eventId) => { + console.log({ eventType, runId, parentRunId, eventId, data }); + }, +}); + +await shutdown(); +``` ## Manual callback wiring @@ -58,6 +79,70 @@ const handler = getHandler(); // Pass handler into your framework's callback system. ``` +For custom integrations, pair an LLM start and end with the same `runId`: + +```ts +import { randomUUID } from "node:crypto"; +import { init, shutdown, getHandler } from "@secureagentics/adrian"; + +await init({ wsUrl: null }); + +const handler = getHandler(); +const runId = randomUUID(); + +await handler?.handleChatModelStart( + { name: "custom-model" }, + [[{ role: "user", content: "Hello" }]], + runId, +); + +await handler?.handleLLMEnd( + { + output: "Hi there", + toolCalls: [], + usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 }, + }, + runId, +); + +await shutdown(); +``` + +Manual tool events work the same way: + +```ts +const toolRunId = randomUUID(); + +await handler?.handleToolStart( + { name: "lookup_user" }, + JSON.stringify({ userId: "user_123" }), + toolRunId, + undefined, + { tool_call_id: "call_123", metadata: { source: "custom-integration" } }, +); + +await handler?.handleToolEnd(JSON.stringify({ ok: true }), toolRunId); +``` + +## Custom event handlers + +Provide `handlers` when you want to replace the default JSONL/WebSocket sinks: + +```ts +import { init, type EventHandler, type PairedEvent } from "@secureagentics/adrian"; + +const handler: EventHandler = { + onPairedEvent(event: PairedEvent) { + console.log(event.pairType, event.eventId); + }, + close() { + // Flush resources if needed. + }, +}; + +await init({ handlers: [handler] }); +``` + ## Subpath export `@secureagentics/adrian/capture` exposes shared LLM capture helpers used internally by provider packages. Most apps should use `adrian()` from a provider package instead. diff --git a/sdk/typescript/packages/openai/README.md b/sdk/typescript/packages/openai/README.md index 14b491c..bb7d1fb 100644 --- a/sdk/typescript/packages/openai/README.md +++ b/sdk/typescript/packages/openai/README.md @@ -34,20 +34,84 @@ await shutdown(); When the dashboard policy is in **BLOCK** or **HITL** mode and `init()` is connected over WebSocket, the SDK waits for verdicts on LLM turns that propose tool calls and throws `AdrianPolicyBlockedError` before those tools can run. `captureTool` applies the same gate before executing your handler. +Pass metadata when wrapping the client to attach app-specific context to each captured event: + +```ts +const openai = adrian(new OpenAI(), { + metadata: { environment: "production", service: "checkout-agent" }, +}); +``` + +## Responses API + +```ts +const response = await openai.responses.create({ + model: "gpt-4o-mini", + input: "Summarize the security considerations for this workflow.", +}); + +console.log(response.output_text); +``` + +## Streaming + +Streaming chunks are passed through unchanged. Adrian emits one paired LLM event after the stream completes, or when the consumer exits early: + +```ts +const stream = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "Write a short haiku." }], + stream: true, +}); + +for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); +} +``` + +The Responses API streaming path is captured the same way: + +```ts +const stream = await openai.responses.create({ + model: "gpt-4o-mini", + input: "Write a one-sentence project update.", + stream: true, +}); + +for await (const event of stream) { + if (event.type === "response.output_text.delta") process.stdout.write(event.delta); +} +``` + ## Tool execution capture OpenAI returns tool call requests; your app executes the tools. Wrap that execution with `captureTool`: ```ts +import { AdrianPolicyBlockedError, BLOCKED_TOOL_MESSAGE } from "@secureagentics/adrian-openai"; + +async function runTool(name: string, argsJson: string) { + const args = JSON.parse(argsJson || "{}") as Record; + if (name === "get_weather") return { city: args.city, forecast: "sunny" }; + throw new Error(`Unknown tool: ${name}`); +} + for (const toolCall of assistantMessage.tool_calls ?? []) { - const toolResult = await captureTool(toolCall, () => - runTool(toolCall.function.name, toolCall.function.arguments), - ); + let toolResult: unknown; + + try { + toolResult = await captureTool(toolCall, () => + runTool(toolCall.function.name, toolCall.function.arguments), + ); + } catch (error) { + if (!(error instanceof AdrianPolicyBlockedError)) throw error; + toolResult = BLOCKED_TOOL_MESSAGE; + } messages.push({ role: "tool", tool_call_id: toolCall.id, - content: toolResult, + content: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult), }); } ``` @@ -60,6 +124,8 @@ for (const toolCall of assistantMessage.tool_calls ?? []) { | `shutdown()` | Tear down Adrian (re-exported from core) | | `adrian(client, options?)` | Wrap an OpenAI client | | `captureTool(toolCall, execute, options?)` | Capture manual tool execution | +| `AdrianPolicyBlockedError` | Error thrown when policy blocks a tool call | +| `BLOCKED_TOOL_MESSAGE` | Standard blocked tool result string | ### Types @@ -69,4 +135,4 @@ for (const toolCall of assistantMessage.tool_calls ?? []) { | `ToolCallLike` | `id`, `function.name`, `function.arguments`, … | | `ToolCaptureOptions` | `metadata?`, `parentRunId?` | -See the [workspace README](../README.md) for environment variables and multi-provider setup. +See the [workspace README](../../README.md) for environment variables and multi-provider setup. From abbc5eb65b3b408f4642ac715bfd425c5f58dbc3 Mon Sep 17 00:00:00 2001 From: Adarsh Tiwari Date: Tue, 9 Jun 2026 21:45:42 +0530 Subject: [PATCH 10/10] streams working fine --- .../packages/core/src/capture/common.ts | 48 ++++++++-- sdk/typescript/packages/core/src/handler.ts | 2 +- .../packages/core/tests/capture.test.ts | 48 ++++++++++ sdk/typescript/packages/openai/src/index.ts | 9 +- .../packages/openai/tests/openai.test.ts | 87 ++++++++++++------- 5 files changed, 149 insertions(+), 45 deletions(-) create mode 100644 sdk/typescript/packages/core/tests/capture.test.ts diff --git a/sdk/typescript/packages/core/src/capture/common.ts b/sdk/typescript/packages/core/src/capture/common.ts index fc2dd44..ebd18a2 100644 --- a/sdk/typescript/packages/core/src/capture/common.ts +++ b/sdk/typescript/packages/core/src/capture/common.ts @@ -34,7 +34,7 @@ export async function captureLlmCall( const runId = randomUUID(); return runWithInvocationId(randomUUID(), async () => { - await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata ?? null }); + await handler.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata }); try { const result = await execute(); const endData = await extractOutput(result); @@ -63,7 +63,7 @@ export function captureLlmAsyncIterable( const invocationId = randomUUID(); async function* wrapped(): AsyncGenerator { - await handler?.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata ?? null }); + await handler?.handleChatModelStart({ name: input.model }, [input.messages], runId, input.parentRunId, { metadata: input.metadata }); yield* runWithInvocationId(invocationId, async function* () { let emitted = false; let failed = false; @@ -99,18 +99,50 @@ export function normalizeMessages(input: unknown): ChatMessage[] { return input.map((message) => { const obj = message && typeof message === "object" ? message as Record : {}; return { - role: String(obj.role ?? "unknown"), - content: stringifyContent(obj.content ?? obj.text ?? ""), + role: String(obj.role), + content: stringifyContent(obj.content ?? obj.text), }; }); } export function messagesFromPromptLike(args: Record): ChatMessage[] { + const system = args.instructions; const messages = normalizeMessages(args.messages); - if (messages.length > 0) return prependSystem(args.system, messages); - if (typeof args.prompt === "string") return prependSystem(args.system, [{ role: "user", content: args.prompt }]); - if (typeof args.input === "string") return prependSystem(args.system, [{ role: "user", content: args.input }]); - return prependSystem(args.system, []); + if (messages.length > 0) return prependSystem(system, messages); + if (typeof args.input === "string") return prependSystem(system, [{ role: "user", content: args.input }]); + const inputMessages = normalizeResponseInput(args.input); + if (inputMessages.length > 0) return prependSystem(system, inputMessages); + return prependSystem(system, []); +} + +/** Normalise OpenAI Responses API `input` arrays (roles, tool calls, tool outputs). */ +export function normalizeResponseInput(input: unknown): ChatMessage[] { + if (!Array.isArray(input)) return []; + const messages: ChatMessage[] = []; + for (const item of input) { + if (!item || typeof item !== "object") continue; + const obj = item as Record; + const type = String(obj.type); + + if (type === "function_call" || type === "tool_call") { + const name = String(obj.name); + const args = typeof obj.arguments === "string" ? obj.arguments : stringifyJson(obj.arguments); + messages.push({ role: "assistant", content: `[tool_call:${name}] ${args}` }); + continue; + } + if (type === "function_call_output") { + messages.push({ role: "tool", content: String(obj.output) }); + continue; + } + + const role = String(obj.role); + if (!role) continue; + messages.push({ + role: role === "developer" ? "system" : role, + content: stringifyContent(obj.content ?? obj.text), + }); + } + return messages; } export function stringifyContent(value: unknown): string { diff --git a/sdk/typescript/packages/core/src/handler.ts b/sdk/typescript/packages/core/src/handler.ts index c31fb4a..1c5310a 100644 --- a/sdk/typescript/packages/core/src/handler.ts +++ b/sdk/typescript/packages/core/src/handler.ts @@ -213,7 +213,7 @@ function extractLlmEndData(output: unknown): LlmEndData { const first = generations[0]?.[0] as Record | undefined; const text = typeof first?.text === "string" ? first.text : ""; const message = first?.message as Record | undefined; - const rawCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : Array.isArray(message?.toolCalls) ? message.toolCalls : []; + const rawCalls = Array.isArray(message?.tool_calls) ? message.tool_calls : []; const toolCalls: ToolCallRecord[] = rawCalls.map((call) => { const obj = call && typeof call === "object" ? call as Record : {}; return { id: String(obj.id ?? ""), name: String(obj.name ?? ""), args: (obj.args && typeof obj.args === "object" ? obj.args : {}) as ToolCallRecord["args"] }; diff --git a/sdk/typescript/packages/core/tests/capture.test.ts b/sdk/typescript/packages/core/tests/capture.test.ts new file mode 100644 index 0000000..f6c0c04 --- /dev/null +++ b/sdk/typescript/packages/core/tests/capture.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { messagesFromPromptLike, normalizeResponseInput } from "../src/capture/common.js"; + +describe("normalizeResponseInput", () => { + it("maps role-based input items", () => { + expect(normalizeResponseInput([ + { role: "user", content: "hello" }, + ])).toEqual([{ role: "user", content: "hello" }]); + }); + + it("maps function calls and outputs", () => { + expect(normalizeResponseInput([ + { type: "function_call", name: "get_weather", arguments: '{"city":"SF"}' }, + { type: "function_call_output", output: '{"temp":58}' }, + ])).toEqual([ + { role: "assistant", content: '[tool_call:get_weather] {"city":"SF"}' }, + { role: "tool", content: '{"temp":58}' }, + ]); + }); +}); + +describe("messagesFromPromptLike", () => { + it("prepends instructions as system for string input", () => { + expect(messagesFromPromptLike({ + instructions: "You are helpful.", + input: "Run the task.", + })).toEqual([ + { role: "system", content: "You are helpful." }, + { role: "user", content: "Run the task." }, + ]); + }); + + it("prepends instructions for Responses API input arrays", () => { + expect(messagesFromPromptLike({ + instructions: "You are an autonomous assistant.", + input: [ + { role: "user", content: "Do the work." }, + { type: "function_call", name: "add_numbers", arguments: '{"a":1,"b":2}' }, + { type: "function_call_output", output: '{"result":3}' }, + ], + })).toEqual([ + { role: "system", content: "You are an autonomous assistant." }, + { role: "user", content: "Do the work." }, + { role: "assistant", content: '[tool_call:add_numbers] {"a":1,"b":2}' }, + { role: "tool", content: '{"result":3}' }, + ]); + }); +}); diff --git a/sdk/typescript/packages/openai/src/index.ts b/sdk/typescript/packages/openai/src/index.ts index c5e9236..68d3672 100644 --- a/sdk/typescript/packages/openai/src/index.ts +++ b/sdk/typescript/packages/openai/src/index.ts @@ -114,7 +114,10 @@ function instrumentResponses(responses: unknown, options: AdrianOptions): unknow return async function adrianOpenAIResponsesCreate(this: unknown, body: Record = {}, ...rest: unknown[]) { const model = String(body.model ?? "openai"); const metadata = integrationMetadata(options.metadata, "openai.responses"); - const messages = messagesFromPromptLike({ input: body.input }); + const messages = messagesFromPromptLike({ + input: body.input, + instructions: body.instructions, + }); if (body.stream === true) { const result = await Promise.resolve(value.call(target, body, ...rest)); if (isAsyncIterable(result)) return captureResponseStream(model, messages, metadata, result); @@ -137,7 +140,7 @@ function captureChatCompletionStream(model: string, messages: ReturnType).delta as Record | undefined; output += stringifyContent(delta?.content); - const calls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : Array.isArray(delta?.toolCalls) ? delta.toolCalls : []; + const calls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : []; for (const rawCall of calls) { const call = rawCall as Record; const index = typeof call.index === "number" ? call.index : toolCallParts.size; @@ -172,7 +175,7 @@ function extractChatCompletion(result: unknown): LlmEndData { const choices = Array.isArray(obj.choices) ? obj.choices : []; const first = choices[0] && typeof choices[0] === "object" ? choices[0] as Record : {}; const message = first.message && typeof first.message === "object" ? first.message as Record : {}; - const toolCalls = normalizeOpenAIToolCalls(message.tool_calls ?? message.toolCalls); + const toolCalls = normalizeOpenAIToolCalls(message.tool_calls); return emptyLlmEnd(stringifyContent(message.content), toolCalls, normalizeUsage(obj.usage)); } diff --git a/sdk/typescript/packages/openai/tests/openai.test.ts b/sdk/typescript/packages/openai/tests/openai.test.ts index 3d331e8..810893d 100644 --- a/sdk/typescript/packages/openai/tests/openai.test.ts +++ b/sdk/typescript/packages/openai/tests/openai.test.ts @@ -89,39 +89,6 @@ describe("OpenAI instrumentation", () => { }); }); - it("captures camelCase chat tool calls", async () => { - const events: EventData[] = []; - const client = adrian({ - chat: { - completions: { - create: async (_body: Record) => ({ - choices: [{ - message: { - content: null, - toolCalls: [{ - id: "call-3", - name: "search", - args: { query: "camel" }, - }], - }, - }], - }), - }, - }, - }); - - await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { - events.push(data); - } }); - await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "use search" }] }); - - expect(events[0]).toMatchObject({ - kind: "llm", - model: "gpt-4o-mini", - toolCalls: [{ id: "call-3", name: "search", args: { query: "camel" } }], - }); - }); - it("captures responses API streaming tool calls and text", async () => { const events: EventData[] = []; async function* stream() { @@ -153,6 +120,60 @@ describe("OpenAI instrumentation", () => { }); }); + it("captures Responses API instructions and array input for stream and non-stream", async () => { + const responseBody = { + instructions: "You are an autonomous assistant.", + input: [ + { role: "user", content: "Do the work." }, + { type: "function_call", call_id: "call-1", name: "get_weather", arguments: '{"city":"SF"}' }, + { type: "function_call_output", call_id: "call-1", output: '{"temp":58}' }, + ], + }; + + for (const stream of [false, true]) { + const events: EventData[] = []; + async function* responseStream() { + yield { type: "response.output_text.delta", delta: "Done." }; + } + const client = adrian({ + responses: { + create: async (_body: Record) => ( + stream ? responseStream() : { output_text: "Done.", usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 } } + ), + }, + }); + + await init({ handlers: [], sessionId: "sess", wsUrl: null, onEvent: (_type, data) => { + events.push(data); + } }); + + const result = await client.responses.create({ + model: "gpt-4o-mini", + ...responseBody, + stream, + }); + + if (stream) { + for await (const _chunk of result as AsyncIterable) { + // consume stream + } + } + + expect(events[0]).toMatchObject({ + kind: "llm", + model: "gpt-4o-mini", + messages: [ + { role: "system", content: "You are an autonomous assistant." }, + { role: "user", content: "Do the work." }, + { role: "assistant", content: '[tool_call:get_weather] {"city":"SF"}' }, + { role: "tool", content: '{"temp":58}' }, + ], + }); + + await shutdown(); + } + }); + it("emits partial stream data when the consumer stops early", async () => { const events: EventData[] = []; async function* stream() {