diff --git a/.changeset/add-valkey-store-providers.md b/.changeset/add-valkey-store-providers.md new file mode 100644 index 000000000..0947d5885 --- /dev/null +++ b/.changeset/add-valkey-store-providers.md @@ -0,0 +1,18 @@ +--- +"@voltagent/a2a-server": major +"@voltagent/resumable-streams": minor +--- + +feat: add Valkey-backed TaskStore and ResumableStreamStore providers + +Adds `ValkeyTaskStore` to `@voltagent/a2a-server` and `createResumableStreamValkeyStore` to +`@voltagent/resumable-streams`, enabling distributed persistence via the `@valkey/valkey-glide` +client library. Both stores support configurable key prefixes, optional TTL-based expiration, and +standalone or cluster Valkey deployments. The `@valkey/valkey-glide` peer dependency is optional so +consumers who don't use Valkey are unaffected. + +**Breaking change in `@voltagent/a2a-server`:** `A2AServerConfig` now accepts an optional +`taskStore` property. When provided, it takes precedence over the `deps.taskStore` argument passed +to `A2AServer.initialize()`. The full precedence chain is: +`config.taskStore` > `deps.taskStore` > `InMemoryTaskStore`. A debug-level log is emitted when +`config.taskStore` overrides a non-null `deps.taskStore`. diff --git a/.gitignore b/.gitignore index 60ad947c8..e2366b8b9 100644 --- a/.gitignore +++ b/.gitignore @@ -169,6 +169,9 @@ skills !packages/core/src/workspace/skills !packages/core/src/workspace/skills/** +# kiro +.kiro + dev-debug.log node_modules/ diff --git a/examples/with-valkey-store/.env.example b/examples/with-valkey-store/.env.example new file mode 100644 index 000000000..6bd9978be --- /dev/null +++ b/examples/with-valkey-store/.env.example @@ -0,0 +1,5 @@ +OPENAI_API_KEY= + +# Valkey connection (defaults shown) +VALKEY_HOST=localhost +VALKEY_PORT=6379 diff --git a/examples/with-valkey-store/.gitignore b/examples/with-valkey-store/.gitignore new file mode 100644 index 000000000..9c97bbd46 --- /dev/null +++ b/examples/with-valkey-store/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/examples/with-valkey-store/README.md b/examples/with-valkey-store/README.md new file mode 100644 index 000000000..71b8999d5 --- /dev/null +++ b/examples/with-valkey-store/README.md @@ -0,0 +1,170 @@ +
+ +VoltAgent banner + + +
+
+ +
+ Home Page | + Documentation | + Examples | + Discord | + Blog +
+
+ +
+ +
+ VoltAgent is an open source TypeScript framework for building and orchestrating AI agents.
+ Escape the limitations of no-code builders and the complexity of starting from scratch. +
+
+
+ +
+ +[![npm version](https://img.shields.io/npm/v/@voltagent/core.svg)](https://www.npmjs.com/package/@voltagent/core) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](../../CODE_OF_CONDUCT.md) +[![Discord](https://img.shields.io/discord/1361559153780195478.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://s.voltagent.dev/discord) +[![Twitter Follow](https://img.shields.io/twitter/follow/voltagent_dev?style=social)](https://twitter.com/voltagent_dev) + +
+ +
+ +# VoltAgent with Valkey Store Example + +This example demonstrates how to use **Valkey** as a distributed backing store for both A2A task persistence and resumable streaming in VoltAgent. It uses the `@valkey/valkey-glide` client library for high-performance access to Valkey (standalone or cluster). + +## What you get + +- **ValkeyTaskStore** — Persists A2A task records to Valkey with configurable key prefixes and TTL-based expiration. +- **ValkeyResumableStreamStore** — Manages resumable streaming sessions via Valkey pub/sub and key-value operations. +- A minimal VoltAgent project with a `SupportAgent` exposed over the A2A protocol, backed entirely by Valkey. + +## Structure + +```text +examples/with-valkey-store +├── src/ +│ ├── agents/assistant.ts # Example agent definition +│ └── index.ts # VoltAgent bootstrap with Valkey stores +├── .env.example # Environment variable template +├── package.json +├── tsconfig.json +└── README.md +``` + +## Prerequisites + +- Node.js 20+ +- `pnpm` +- A running Valkey instance (or Redis-compatible server) +- `OPENAI_API_KEY` in your environment + +### Start Valkey locally with Docker + +```bash +docker run -d --name valkey -p 6379:6379 valkey/valkey:8 +``` + +## Run locally + +1. Copy the environment template and fill in your keys: + +```bash +cp .env.example .env +``` + +2. Install dependencies and start the dev server: + +```bash +pnpm install +pnpm --filter voltagent-example-with-valkey-store dev +``` + +The server listens on `http://localhost:3141`. + +## Configuration + +Environment variables: + +| Variable | Default | Description | +| ---------------- | ----------- | ------------------------------------ | +| `OPENAI_API_KEY` | — | OpenAI API key for the example agent | +| `VALKEY_HOST` | `localhost` | Valkey server hostname | +| `VALKEY_PORT` | `6379` | Valkey server port | + +### Key prefixes and TTL + +Both stores accept `keyPrefix` and `ttlSeconds` options: + +```typescript +// Task store — keys like "my-tasks:agentId::taskId" +const taskStore = await createValkeyTaskStore({ + client: valkeyClient, + keyPrefix: "my-tasks", + ttlSeconds: 3600, +}); + +// Stream store — keys like "my-streams:active:userId-conversationId" +const streamStore = await createResumableStreamValkeyStore({ + client: valkeyClient, + clientConfig: { addresses: [{ host: "localhost", port: 6379 }] }, + keyPrefix: "my-streams", + ttlSeconds: 600, +}); +``` + +### Cluster mode + +Both stores accept `GlideClient` or `GlideClusterClient`: + +```typescript +import { GlideClusterClient } from "@valkey/valkey-glide"; + +const clusterClient = await GlideClusterClient.createClient({ + addresses: [ + { host: "node1.example.com", port: 6379 }, + { host: "node2.example.com", port: 6379 }, + ], + useTLS: true, +}); + +const taskStore = new ValkeyTaskStore({ client: clusterClient }); +``` + +## Try it + +```bash +# Fetch the agent card +curl http://localhost:3141/.well-known/supportagent/agent-card.json | jq + +# Send a message +curl -X POST http://localhost:3141/a2a/supportagent \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": "1", + "method": "message/send", + "params": { + "message": { + "kind": "message", + "role": "user", + "messageId": "msg-1", + "parts": [{ "kind": "text", "text": "What time is it?" }] + } + } + }' +``` + +## Next steps + +- Adjust `ttlSeconds` to match your retention requirements (or omit it for no expiration). +- Use `GlideClusterClient` for production Valkey cluster deployments (e.g., AWS ElastiCache Valkey). +- Add TLS by setting `useTLS: true` in the client configuration. + +Happy hacking! 🚀 diff --git a/examples/with-valkey-store/package.json b/examples/with-valkey-store/package.json new file mode 100644 index 000000000..0b8cd1ecd --- /dev/null +++ b/examples/with-valkey-store/package.json @@ -0,0 +1,32 @@ +{ + "name": "voltagent-example-with-valkey-store", + "version": "0.0.0", + "dependencies": { + "@valkey/valkey-glide": "^2.3.1", + "@voltagent/a2a-server": "^2.0.3", + "@voltagent/core": "^2.7.2", + "@voltagent/internal": "^1.0.3", + "@voltagent/logger": "^2.0.2", + "@voltagent/resumable-streams": "^2.0.2", + "@voltagent/server-hono": "^2.0.12", + "ai": "^6.0.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.2.1", + "tsx": "^4.21.0", + "typescript": "^5.8.2" + }, + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/VoltAgent/voltagent.git", + "directory": "examples/with-valkey-store" + }, + "scripts": { + "build": "tsc", + "dev": "tsx watch --env-file=.env ./src", + "start": "node dist/index.js" + }, + "type": "module" +} diff --git a/examples/with-valkey-store/src/agents/assistant.ts b/examples/with-valkey-store/src/agents/assistant.ts new file mode 100644 index 000000000..a5e74823b --- /dev/null +++ b/examples/with-valkey-store/src/agents/assistant.ts @@ -0,0 +1,22 @@ +import { Agent, createTool } from "@voltagent/core"; +import { z } from "zod"; + +const statusTool = createTool({ + name: "status", + description: "Return the current time in ISO format", + parameters: z.object({}), + async execute() { + return { + timestamp: new Date().toISOString(), + }; + }, +}); + +/** Pre-configured support agent with a `status` tool that returns the current time. */ +export const assistant = new Agent({ + id: "supportagent", + name: "SupportAgent", + instructions: "Reply with helpful answers and include the current time when relevant.", + model: "openai/gpt-4o-mini", + tools: [statusTool], +}); diff --git a/examples/with-valkey-store/src/index.ts b/examples/with-valkey-store/src/index.ts new file mode 100644 index 000000000..30a7081a4 --- /dev/null +++ b/examples/with-valkey-store/src/index.ts @@ -0,0 +1,77 @@ +import { GlideClient } from "@valkey/valkey-glide"; +import { A2AServer } from "@voltagent/a2a-server"; +import { createValkeyTaskStore } from "@voltagent/a2a-server/valkey-store"; +import { VoltAgent } from "@voltagent/core"; +import { createPinoLogger } from "@voltagent/logger"; +import { createResumableStreamAdapter } from "@voltagent/resumable-streams"; +import { createResumableStreamValkeyStore } from "@voltagent/resumable-streams/valkey-store"; +import { honoServer } from "@voltagent/server-hono"; +import { assistant } from "./agents/assistant.js"; + +const logger = createPinoLogger({ + name: "with-valkey-store", + level: "debug", +}); + +const host = process.env.VALKEY_HOST ?? "localhost"; +const rawPort = process.env.VALKEY_PORT; +const port = rawPort !== undefined ? Number(rawPort) : 6379; +if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new Error(`Invalid VALKEY_PORT "${rawPort}": must be an integer between 1 and 65535`); +} + +/** + * Bootstraps a VoltAgent instance backed by Valkey for both A2A task + * persistence and resumable streaming. Connects to the Valkey server + * specified by `VALKEY_HOST` / `VALKEY_PORT` environment variables + * (defaulting to `localhost:6379`), then starts an HTTP server on port 3141. + */ +async function main() { + const valkeyClient = await GlideClient.createClient({ + addresses: [{ host, port }], + clientName: "voltagent_a2a_store_client", + }); + logger.info(`Connected to Valkey at ${host}:${port}`); + + const taskStore = await createValkeyTaskStore({ + client: valkeyClient, + keyPrefix: "example-tasks", + ttlSeconds: 3600, + }); + + const streamStore = await createResumableStreamValkeyStore({ + client: valkeyClient, + clientConfig: { addresses: [{ host, port }], clientName: "voltagent_a2a_stream_client" }, + keyPrefix: "example-streams", + ttlSeconds: 600, + }); + + const streamAdapter = await createResumableStreamAdapter({ + streamStore, + }); + + const a2aServerFactory = () => + new A2AServer({ + name: "SupportAgent", + version: "0.1.0", + description: "A2A server with Valkey-backed task and stream persistence", + taskStore, + }); + + new VoltAgent({ + agents: { assistant }, + a2aServers: { supportAgent: a2aServerFactory }, + server: honoServer({ + port: 3141, + resumableStream: { adapter: streamAdapter }, + }), + logger, + }); + + logger.info("VoltAgent with Valkey stores running on http://localhost:3141"); +} + +main().catch((err) => { + logger.error("Failed to start", { error: err }); + process.exit(1); +}); diff --git a/examples/with-valkey-store/tsconfig.json b/examples/with-valkey-store/tsconfig.json new file mode 100644 index 000000000..811b07146 --- /dev/null +++ b/examples/with-valkey-store/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "types": ["node"], + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["dist"] +} diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index b3d536dc9..34f6df3fc 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -20,6 +20,16 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" } + }, + "./valkey-store": { + "import": { + "types": "./dist/valkey-store.d.mts", + "default": "./dist/valkey-store.mjs" + }, + "require": { + "types": "./dist/valkey-store.d.ts", + "default": "./dist/valkey-store.js" + } } }, "files": [ @@ -29,8 +39,14 @@ "main": "dist/index.js", "module": "dist/index.mjs", "peerDependencies": { + "@valkey/valkey-glide": ">=2.3.1", "@voltagent/core": "^2.0.0" }, + "peerDependenciesMeta": { + "@valkey/valkey-glide": { + "optional": true + } + }, "repository": { "type": "git", "url": "https://github.com/VoltAgent/voltagent.git", diff --git a/packages/a2a-server/src/schemas.ts b/packages/a2a-server/src/schemas.ts new file mode 100644 index 000000000..cf2cb86ad --- /dev/null +++ b/packages/a2a-server/src/schemas.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; + +/** + * Zod schemas for A2A task-related types. + * + * These are the **single source of truth** — the corresponding TypeScript + * types in `./types.ts` are derived via `z.infer` so the runtime validation + * and static types can never drift apart. + */ + +/** Zod schema for the set of valid task lifecycle states. */ +export const TaskStateSchema = z.enum([ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", +]); + +/** Zod schema for a text-only message part. */ +export const A2AMessagePartTextSchema = z.object({ + kind: z.literal("text"), + text: z.string(), +}); + +/** Zod schema for a message part. Currently only `text` parts exist; extend with `z.discriminatedUnion` when new kinds are added. */ +export const A2AMessagePartSchema = A2AMessagePartTextSchema; + +/** Zod schema for a single A2A message (user or agent). */ +export const A2AMessageSchema = z.object({ + kind: z.literal("message"), + role: z.enum(["user", "agent"]), + messageId: z.string(), + parts: z.array(A2AMessagePartSchema), + contextId: z.string().optional(), + taskId: z.string().optional(), + referenceTaskIds: z.array(z.string()).optional(), + extensions: z.array(z.string()).optional(), + metadata: z.record(z.unknown()).optional(), +}); + +/** Zod schema for a task's current status including state and timestamp. */ +export const TaskStatusSchema = z.object({ + state: TaskStateSchema, + message: A2AMessageSchema.optional(), + timestamp: z.string(), +}); + +/** Zod schema for a text-only artifact part. */ +export const TaskArtifactPartSchema = z.object({ + kind: z.literal("text"), + text: z.string(), +}); + +/** Zod schema for an artifact produced by an agent during task execution. */ +export const TaskArtifactSchema = z.object({ + name: z.string(), + parts: z.array(TaskArtifactPartSchema), + description: z.string().optional(), + metadata: z.record(z.unknown()).optional(), +}); + +/** Zod schema for a complete task record including status, history, and optional artifacts. */ +export const TaskRecordSchema = z.object({ + id: z.string(), + contextId: z.string(), + status: TaskStatusSchema, + history: z.array(A2AMessageSchema), + artifacts: z.array(TaskArtifactSchema).optional(), + metadata: z.record(z.unknown()).optional(), +}); diff --git a/packages/a2a-server/src/server.ts b/packages/a2a-server/src/server.ts index eb801f063..2172ae8b5 100644 --- a/packages/a2a-server/src/server.ts +++ b/packages/a2a-server/src/server.ts @@ -1,8 +1,9 @@ import { randomUUID } from "node:crypto"; -import { type Agent, convertUsage } from "@voltagent/core"; +import { type Agent, convertUsage, getGlobalLogger } from "@voltagent/core"; import { buildAgentCard } from "./adapters/agent"; import { fromVoltAgentMessage, toVoltAgentMessage } from "./adapters/message"; import { createSuccessResponse, normalizeError } from "./protocol"; +import { A2AMessageSchema } from "./schemas"; import { InMemoryTaskStore } from "./store"; import { appendMessage, @@ -55,6 +56,13 @@ function resolveAgentCardUrl(serverId: string, requestUrl?: string): string { } } +/** + * A2A (Agent-to-Agent) protocol server. + * + * Manages agent registration, JSON-RPC request routing, task lifecycle, and + * streaming responses. Call {@link initialize} with runtime dependencies + * before handling any requests. + */ export class A2AServer { private deps?: Required; private readonly config: A2AServerConfig; @@ -62,6 +70,7 @@ export class A2AServer { private readonly configuredAgents = new Map(); private readonly agentFilter: A2AFilterFunction | undefined; + /** Creates a new A2AServer from the given configuration, registering any pre-configured agents. */ constructor(config: A2AServerConfig) { this.config = config; this.agentFilter = config.filterAgents; @@ -76,13 +85,30 @@ export class A2AServer { } } + /** + * Initializes the server with runtime dependencies. + * + * Task store precedence: `config.taskStore` (constructor) > `deps.taskStore` > `InMemoryTaskStore`. + * If a `taskStore` was provided in the `A2AServerConfig` constructor, it takes priority over + * the one supplied here in `deps`. + */ initialize(deps: A2AServerDeps): void { + if (this.config.taskStore && deps.taskStore) { + getGlobalLogger() + .child({ component: "a2a-server" }) + .debug( + "config.taskStore is overriding deps.taskStore. " + + "The task store provided in A2AServerConfig takes precedence.", + ); + } + this.deps = { ...deps, - taskStore: deps.taskStore ?? new InMemoryTaskStore(), + taskStore: this.config.taskStore ?? deps.taskStore ?? new InMemoryTaskStore(), } as Required; } + /** Returns the server's public metadata (id, name, version, description, provider). */ getMetadata() { return { id: this.config.id, @@ -93,6 +119,7 @@ export class A2AServer { }; } + /** Builds and returns the {@link AgentCard} for the specified agent, including its endpoint URL. */ getAgentCard(agentId: string, context: A2ARequestContext = {}): AgentCard { const agent = this.resolveAgent(agentId, context); const url = resolveAgentCardUrl(agentId, context.requestUrl); @@ -108,6 +135,11 @@ export class A2AServer { }); } + /** + * Routes an incoming JSON-RPC request to the appropriate handler. + * + * Supported methods: `message/send`, `message/stream`, `tasks/get`, `tasks/cancel`. + */ async handleRequest( agentId: string, request: JsonRpcRequest, @@ -304,6 +336,33 @@ export class A2AServer { if (abortController.signal.aborted) { return await this.ensureCanceledRecord(agentId, record); } + + const failureText = + error instanceof Error ? error.message : "Task failed with an unknown error"; + const failureMessage: A2AMessage = { + kind: "message", + role: "agent", + messageId: randomUUID(), + taskId: record.id, + contextId: record.contextId, + parts: [{ kind: "text", text: failureText }], + }; + + record = appendMessage(record, failureMessage); + record = transitionStatus(record, { state: "failed", message: failureMessage }); + + try { + await taskStore.save({ agentId, data: record }); + } catch (saveErr) { + getGlobalLogger() + .child({ component: "a2a-server" }) + .warn("Failed to persist task failure status", { + agentId, + taskId: record.id, + saveError: saveErr, + }); + } + throw error; } finally { this.clearActiveOperation(agentId, record.id); @@ -664,24 +723,16 @@ export class A2AServer { if (!payload || typeof payload !== "object") { throw VoltA2AError.invalidParams("Params must be an object"); } - const candidate = payload as Partial; + const candidate = payload as Record; - if (!candidate.message || typeof candidate.message !== "object") { - throw VoltA2AError.invalidParams("'message' must be provided"); - } - - if (!Array.isArray(candidate.message.parts) || candidate.message.parts.length === 0) { - throw VoltA2AError.invalidParams("Message must include at least one part"); - } - - const hasInvalidPart = candidate.message.parts.some( - (part) => part.kind !== "text" || typeof part.text !== "string", - ); - if (hasInvalidPart) { - throw VoltA2AError.invalidParams("Only plain text message parts are supported"); + try { + A2AMessageSchema.parse(candidate.message); + } catch (error) { + const message = error instanceof Error ? error.message : "Invalid message payload"; + throw VoltA2AError.invalidParams(message); } - return candidate as MessageSendParams; + return candidate as unknown as MessageSendParams; } private validateTaskQueryParams(payload: unknown): TaskQueryParams { diff --git a/packages/a2a-server/src/types.ts b/packages/a2a-server/src/types.ts index 352db4eeb..1bb9ed6c4 100644 --- a/packages/a2a-server/src/types.ts +++ b/packages/a2a-server/src/types.ts @@ -4,15 +4,29 @@ import type { A2AServerLike as BaseA2AServerLike, A2AServerMetadata as BaseA2AServerMetadata, } from "@voltagent/internal/a2a"; - +import type { z } from "zod"; +import type { + A2AMessagePartSchema, + A2AMessagePartTextSchema, + A2AMessageSchema, + TaskArtifactPartSchema, + TaskArtifactSchema, + TaskRecordSchema, + TaskStateSchema, + TaskStatusSchema, +} from "./schemas"; + +/** Identifier for a JSON-RPC request — a string, number, or `null` for notifications. */ export type A2AJsonRpcId = string | number | null; +/** Standard JSON-RPC 2.0 error object returned inside a {@link JsonRpcResponse}. */ export interface JsonRpcError { code: number; message: string; data?: Data; } +/** Standard JSON-RPC 2.0 response envelope. */ export interface JsonRpcResponse { jsonrpc: "2.0"; id: A2AJsonRpcId; @@ -20,16 +34,19 @@ export interface JsonRpcResponse { error?: JsonRpcError | null; } +/** Wrapper around an async generator that yields {@link JsonRpcResponse} objects for streaming calls. */ export interface JsonRpcStream { kind: "stream"; id: A2AJsonRpcId; stream: AsyncGenerator>; } +/** Discriminated union of a single {@link JsonRpcResponse} or a {@link JsonRpcStream}. */ export type JsonRpcHandlerResult = | JsonRpcResponse | JsonRpcStream; +/** Incoming JSON-RPC 2.0 request envelope. */ export interface JsonRpcRequest { jsonrpc: "2.0"; id: A2AJsonRpcId; @@ -37,65 +54,37 @@ export interface JsonRpcRequest { params?: Params; } -export type TaskState = - | "submitted" - | "working" - | "input-required" - | "completed" - | "failed" - | "canceled"; - -export interface A2AMessagePartText { - kind: "text"; - text: string; -} +/** The set of valid task lifecycle states. Derived from {@link TaskStateSchema}. */ +export type TaskState = z.infer; -export type A2AMessagePart = A2AMessagePartText; - -export interface A2AMessage { - kind: "message"; - role: "user" | "agent"; - messageId: string; - parts: A2AMessagePart[]; - contextId?: string; - taskId?: string; - referenceTaskIds?: string[]; - extensions?: string[]; - metadata?: Record; -} +/** A text-only message part. Derived from {@link A2AMessagePartTextSchema}. */ +export type A2AMessagePartText = z.infer; -export interface TaskStatus { - state: TaskState; - message?: A2AMessage; - timestamp: string; -} +/** Union of all message part kinds. Derived from {@link A2AMessagePartSchema}. */ +export type A2AMessagePart = z.infer; -export interface TaskArtifactPart { - kind: "text"; - text: string; -} +/** A single message exchanged between user and agent. Derived from {@link A2AMessageSchema}. */ +export type A2AMessage = z.infer; -export interface TaskArtifact { - name: string; - parts: TaskArtifactPart[]; - description?: string; - metadata?: Record; -} +/** Current status of a task including lifecycle state and timestamp. Derived from {@link TaskStatusSchema}. */ +export type TaskStatus = z.infer; -export interface TaskRecord { - id: string; - contextId: string; - status: TaskStatus; - history: A2AMessage[]; - artifacts?: TaskArtifact[]; - metadata?: Record; -} +/** A text-only artifact part. Derived from {@link TaskArtifactPartSchema}. */ +export type TaskArtifactPart = z.infer; + +/** An artifact produced by an agent during task execution. Derived from {@link TaskArtifactSchema}. */ +export type TaskArtifact = z.infer; +/** A complete task record including status, history, and optional artifacts. Derived from {@link TaskRecordSchema}. */ +export type TaskRecord = z.infer; + +/** Persistence layer for loading and saving {@link TaskRecord} instances keyed by agent and task ID. */ export interface TaskStore { load(params: { agentId: string; taskId: string }): Promise; save(params: { agentId: string; data: TaskRecord }): Promise; } +/** Per-request context forwarded to agent invocations and filter functions. */ export interface A2ARequestContext { userId?: string; sessionId?: string; @@ -103,13 +92,16 @@ export interface A2ARequestContext { requestUrl?: string; } +/** Parameters passed to an {@link A2AFilterFunction} for filtering a list of items. */ export interface A2AFilterParams { items: T[]; context?: A2ARequestContext; } +/** Callback that filters a list of items (e.g. agents) based on the current request context. */ export type A2AFilterFunction = (params: A2AFilterParams) => T[]; +/** Parameters for the `message/send` and `message/stream` JSON-RPC methods. */ export interface MessageSendParams { id?: string; sessionId?: string; @@ -118,21 +110,27 @@ export interface MessageSendParams { message: A2AMessage; } +/** Parameters for the `tasks/get` JSON-RPC method. */ export interface TaskQueryParams { id: string; historyLength?: number; metadata?: Record; } +/** Parameters for the `tasks/cancel` JSON-RPC method. */ export interface TaskIdParams { id: string; metadata?: Record; } +/** Result of a `message/send` or `message/stream` call — the updated {@link TaskRecord}. */ export type MessageSendResult = TaskRecord; +/** Result of a `tasks/get` call — the requested {@link TaskRecord}. */ export type TaskGetResult = TaskRecord; +/** Result of a `tasks/cancel` call — the canceled {@link TaskRecord}. */ export type TaskCancelResult = TaskRecord; +/** Configuration supplied to the {@link A2AServer} constructor. */ export interface A2AServerConfig { id?: string; name: string; @@ -144,14 +142,22 @@ export interface A2AServerConfig { }; agents?: Record; filterAgents?: A2AFilterFunction; + /** + * Optional task store. When provided, takes precedence over `deps.taskStore` passed to + * `A2AServer.initialize()`. Falls back to an in-memory store if neither is set. + */ + taskStore?: TaskStore; } +/** Metadata describing an {@link A2AServer} instance (name, version, etc.). */ export interface A2AServerMetadata extends BaseA2AServerMetadata {} +/** Runtime dependencies injected into {@link A2AServer} via `initialize()`. */ export interface A2AServerDeps extends BaseA2AServerDeps { taskStore?: TaskStore; } +/** Minimal public surface of an A2A server, used by server-provider adapters. */ export interface A2AServerLike extends BaseA2AServerLike { getAgentCard?(agentId: string, context?: A2ARequestContext): AgentCard; handleRequest?( @@ -161,8 +167,10 @@ export interface A2AServerLike extends BaseA2AServerLike { ): Promise; } +/** Factory function that creates a new {@link A2AServerLike} instance on demand. */ export type A2AServerFactory = () => T; +/** A single skill advertised by an agent in its {@link AgentCard}. */ export interface AgentCardSkill { id: string; name: string; @@ -170,6 +178,7 @@ export interface AgentCardSkill { tags?: string[]; } +/** Public metadata card describing an agent's capabilities, skills, and endpoint URL. */ export interface AgentCard { name: string; description?: string; @@ -189,6 +198,7 @@ export interface AgentCard { skills: AgentCardSkill[]; } +/** Standard A2A JSON-RPC error codes, extending the base JSON-RPC 2.0 error range with task-specific codes. */ export const A2AErrorCode = { PARSE_ERROR: -32700, INVALID_REQUEST: -32600, @@ -203,7 +213,14 @@ export const A2AErrorCode = { export type A2AErrorCode = (typeof A2AErrorCode)[keyof typeof A2AErrorCode]; +/** + * Typed error class for A2A JSON-RPC failures. + * + * Provides static factory methods for every standard error code so callers + * never need to remember numeric codes. + */ export class VoltA2AError extends Error { + /** Creates a new {@link VoltA2AError} with the given code, message, and optional details. */ constructor( public code: A2AErrorCode, message: string, @@ -214,33 +231,39 @@ export class VoltA2AError extends Error { this.name = "VoltA2AError"; } + /** Converts this error into a plain {@link JsonRpcError} object suitable for serialisation. */ toJsonRpcError(): JsonRpcError { return { code: this.code, message: this.message, data: { taskId: this.taskId, - ...(this.data ? { details: this.data } : {}), + ...(this.data !== undefined ? { details: this.data } : {}), }, }; } + /** Creates a parse-error (-32700) for malformed JSON payloads. */ static parseError(details?: unknown) { return new VoltA2AError(A2AErrorCode.PARSE_ERROR, "Invalid JSON payload", details); } + /** Creates an invalid-request (-32600) error. */ static invalidRequest(message = "Invalid request", details?: unknown) { return new VoltA2AError(A2AErrorCode.INVALID_REQUEST, message, details); } + /** Creates a method-not-found (-32601) error for an unknown JSON-RPC method. */ static methodNotFound(method: string) { return new VoltA2AError(A2AErrorCode.METHOD_NOT_FOUND, `Unknown method '${method}'`); } + /** Creates an invalid-params (-32602) error. */ static invalidParams(message = "Invalid parameters", details?: unknown) { return new VoltA2AError(A2AErrorCode.INVALID_PARAMS, message, details); } + /** Creates a task-not-found (-32001) error for the given task ID. */ static taskNotFound(taskId: string) { return new VoltA2AError( A2AErrorCode.TASK_NOT_FOUND, @@ -250,6 +273,7 @@ export class VoltA2AError extends Error { ); } + /** Creates a task-not-cancelable (-32002) error for a task that can no longer be canceled. */ static taskNotCancelable(taskId: string) { return new VoltA2AError( A2AErrorCode.TASK_NOT_CANCELABLE, @@ -259,10 +283,12 @@ export class VoltA2AError extends Error { ); } + /** Creates an unsupported-operation (-32004) error. */ static unsupportedOperation(message = "Unsupported operation") { return new VoltA2AError(A2AErrorCode.UNSUPPORTED_OPERATION, message); } + /** Creates an internal-error (-32603) error. */ static internal(message = "Internal error", details?: unknown) { return new VoltA2AError(A2AErrorCode.INTERNAL_ERROR, message, details); } diff --git a/packages/a2a-server/src/valkey-store.spec.ts b/packages/a2a-server/src/valkey-store.spec.ts new file mode 100644 index 000000000..bb09fedcc --- /dev/null +++ b/packages/a2a-server/src/valkey-store.spec.ts @@ -0,0 +1,190 @@ +import { safeStringify } from "@voltagent/internal"; +import { TaskRecordSchema } from "./schemas"; +import type { TaskRecord } from "./types"; +import { ValkeyTaskStore, createValkeyTaskStore } from "./valkey-store"; + +// Mock @valkey/valkey-glide so tests don't require the actual package +vi.mock("@valkey/valkey-glide", () => ({ + TimeUnit: { Seconds: "EX" }, +})); + +function makeClient() { + return { + get: vi.fn(), + set: vi.fn(), + }; +} + +function makeTaskRecord(overrides: Partial = {}): TaskRecord { + return { + id: "task-1", + contextId: "ctx-1", + status: { state: "submitted", timestamp: new Date().toISOString() }, + history: [], + ...overrides, + }; +} + +describe("ValkeyTaskStore", () => { + it("load() returns deserialized TaskRecord when client.get returns a JSON string", async () => { + const client = makeClient(); + const record = makeTaskRecord(); + client.get.mockResolvedValue(safeStringify(record)); + + const store = new ValkeyTaskStore({ client } as any); + const result = await store.load({ agentId: "agent-1", taskId: "task-1" }); + + expect(result).toEqual(record); + }); + + it("load() returns null when client.get returns null", async () => { + const client = makeClient(); + client.get.mockResolvedValue(null); + + const store = new ValkeyTaskStore({ client } as any); + const result = await store.load({ agentId: "agent-1", taskId: "task-1" }); + + expect(result).toBeNull(); + }); + + it("load() handles GlideString Buffer values by converting to string", async () => { + const client = makeClient(); + const record = makeTaskRecord(); + client.get.mockResolvedValue(Buffer.from(safeStringify(record))); + + const store = new ValkeyTaskStore({ client } as any); + const result = await store.load({ agentId: "agent-1", taskId: "task-1" }); + + expect(result).toEqual(record); + }); + + it("save() calls client.set with the correct composite key {keyPrefix}:{agentId}::{taskId}", async () => { + const client = makeClient(); + client.set.mockResolvedValue("OK"); + const record = makeTaskRecord({ id: "task-42" }); + + const store = new ValkeyTaskStore({ client, keyPrefix: "my-prefix" } as any); + await store.save({ agentId: "agent-x", data: record }); + + expect(client.set.mock.calls[0][0]).toBe("my-prefix:agent-x::task-42"); + }); + + it("save() uses default keyPrefix 'a2a-tasks' when none is provided", async () => { + const client = makeClient(); + client.set.mockResolvedValue("OK"); + const record = makeTaskRecord({ id: "task-42" }); + + const store = new ValkeyTaskStore({ client } as any); + await store.save({ agentId: "agent-x", data: record }); + + expect(client.set.mock.calls[0][0]).toBe("a2a-tasks:agent-x::task-42"); + }); + + it("save() serializes the TaskRecord as a JSON string containing the task data", async () => { + const client = makeClient(); + client.set.mockResolvedValue("OK"); + const record = makeTaskRecord({ id: "task-99" }); + + const store = new ValkeyTaskStore({ client } as any); + await store.save({ agentId: "agent-1", data: record }); + + const storedValue = client.set.mock.calls[0][1]; + expect(typeof storedValue).toBe("string"); + const parsed = JSON.parse(storedValue); + expect(parsed.id).toBe("task-99"); + expect(parsed.contextId).toBe(record.contextId); + }); + + it("save() calls client.set with expiry options when ttlSeconds is configured", async () => { + const client = makeClient(); + client.set.mockResolvedValue("OK"); + const record = makeTaskRecord({ id: "task-ttl" }); + + const store = new ValkeyTaskStore({ client, ttlSeconds: 300 } as any); + await store.save({ agentId: "agent-1", data: record }); + + const options = client.set.mock.calls[0][2]; + expect(options).toEqual({ expiry: { type: "EX", count: 300 } }); + }); + + it("save() calls client.set without expiry options when ttlSeconds is not configured", async () => { + const client = makeClient(); + client.set.mockResolvedValue("OK"); + const record = makeTaskRecord({ id: "task-no-ttl" }); + + const store = new ValkeyTaskStore({ client } as any); + await store.save({ agentId: "agent-1", data: record }); + + expect(client.set.mock.calls[0]).toHaveLength(2); + }); + + it("load() propagates errors thrown by client.get", async () => { + const client = makeClient(); + client.get.mockRejectedValue(new Error("connection refused")); + + const store = new ValkeyTaskStore({ client } as any); + await expect(store.load({ agentId: "agent-1", taskId: "task-1" })).rejects.toThrow( + "connection refused", + ); + }); + + it("load() throws a descriptive error when stored data is corrupted JSON", async () => { + const client = makeClient(); + client.get.mockResolvedValue("not-valid-json{{{"); + + const store = new ValkeyTaskStore({ client, keyPrefix: "pfx" } as any); + await expect(store.load({ agentId: "agent-1", taskId: "task-1" })).rejects.toThrow( + /Failed to parse stored TaskRecord for key "pfx:agent-1::task-1"/, + ); + }); + + it("load() throws when stored data is valid JSON but fails schema validation", async () => { + const client = makeClient(); + // Valid JSON but missing required TaskRecord fields (id, contextId, status, history) + client.get.mockResolvedValue(safeStringify({ bogus: true })); + + const store = new ValkeyTaskStore({ client, keyPrefix: "pfx" } as any); + await expect(store.load({ agentId: "agent-1", taskId: "task-1" })).rejects.toThrow( + /Invalid TaskRecord for key "pfx:agent-1::task-1"/, + ); + }); + + it("save() propagates errors thrown by client.set", async () => { + const client = makeClient(); + client.set.mockRejectedValue(new Error("write timeout")); + const record = makeTaskRecord({ id: "task-err" }); + + const store = new ValkeyTaskStore({ client } as any); + await expect(store.save({ agentId: "agent-1", data: record })).rejects.toThrow("write timeout"); + }); + + it("exposes activeCancellations Set for task cancellation signaling", () => { + const client = makeClient(); + const store = new ValkeyTaskStore({ client } as any); + + expect(store.activeCancellations).toBeInstanceOf(Set); + }); +}); + +describe("createValkeyTaskStore", () => { + it("returns a ValkeyTaskStore instance with eagerly-resolved TimeUnit", async () => { + const client = makeClient(); + client.set.mockResolvedValue("OK"); + + const store = await createValkeyTaskStore({ client, ttlSeconds: 60 } as any); + + expect(store).toBeInstanceOf(ValkeyTaskStore); + + const record = makeTaskRecord({ id: "task-factory" }); + await store.save({ agentId: "agent-1", data: record }); + + const options = client.set.mock.calls[0][2]; + expect(options).toEqual({ expiry: { type: "EX", count: 60 } }); + }); + + it("returns a ValkeyTaskStore without resolving TimeUnit when no ttlSeconds", async () => { + const client = makeClient(); + const store = await createValkeyTaskStore({ client } as any); + expect(store).toBeInstanceOf(ValkeyTaskStore); + }); +}); diff --git a/packages/a2a-server/src/valkey-store.ts b/packages/a2a-server/src/valkey-store.ts new file mode 100644 index 000000000..0ec5dd13c --- /dev/null +++ b/packages/a2a-server/src/valkey-store.ts @@ -0,0 +1,192 @@ +import { randomUUID } from "node:crypto"; +import type { GlideClient, GlideClusterClient, TimeUnit } from "@valkey/valkey-glide"; +import { safeStringify } from "@voltagent/internal"; +import { TaskRecordSchema } from "./schemas"; +import type { TaskRecord, TaskStore } from "./types"; + +/** + * Configuration options for {@link ValkeyTaskStore}. + */ +export interface ValkeyTaskStoreOptions { + /** Valkey client instance (standalone {@link GlideClient} or {@link GlideClusterClient}). */ + client: GlideClient | GlideClusterClient; + /** Key prefix for all task records stored in Valkey. Defaults to `"a2a-tasks"`. */ + keyPrefix?: string; + /** Optional TTL in seconds applied to every persisted task record. Must be a positive finite number. */ + ttlSeconds?: number; +} + +const VALKEY_GLIDE_REQUIRED = + "@valkey/valkey-glide is required for ValkeyTaskStore. Install it with: pnpm add @valkey/valkey-glide"; + +/** + * Creates a {@link ValkeyTaskStore} with eagerly-resolved Valkey dependencies. + * + * Validates `ttlSeconds` and pre-resolves the `TimeUnit.Seconds` enum from + * `@valkey/valkey-glide` so that subsequent `save()` calls do not need to + * perform a dynamic import. + * + * @param options - Store configuration including the Valkey client, optional key prefix, and TTL. + * @returns A fully initialised {@link ValkeyTaskStore} instance. + * @throws If `ttlSeconds` is provided but is not a positive finite number. + * @throws If `@valkey/valkey-glide` is not installed when `ttlSeconds` is set. + */ +export async function createValkeyTaskStore( + options: ValkeyTaskStoreOptions, +): Promise { + if ( + options.ttlSeconds !== undefined && + (!Number.isFinite(options.ttlSeconds) || options.ttlSeconds <= 0) + ) { + throw new Error("ttlSeconds must be a positive finite number"); + } + + let timeUnitSeconds: TimeUnit | undefined; + if (options.ttlSeconds !== undefined) { + try { + const mod = await import("@valkey/valkey-glide"); + timeUnitSeconds = mod.TimeUnit.Seconds; + } catch { + throw new Error(VALKEY_GLIDE_REQUIRED); + } + } + return new ValkeyTaskStore(options, timeUnitSeconds); +} + +/** + * Valkey-backed implementation of {@link TaskStore}. + * + * **Important:** `activeCancellations` is an in-process `Set` and is **not** propagated across + * server instances sharing the same Valkey backend. For cross-instance cancellation, `A2AServer` + * would need to subscribe to a Valkey pub/sub channel for cancellation events instead of relying + * on process-local `AbortController` signaling. + */ +export class ValkeyTaskStore implements TaskStore { + private readonly client: GlideClient | GlideClusterClient; + private readonly keyPrefix: string; + private readonly ttlSeconds?: number; + /** @internal */ + private timeUnitSeconds?: TimeUnit; + + // In-process only, not propagated across instances via Valkey. + readonly activeCancellations = new Set(); + + /** + * Creates a new ValkeyTaskStore. + * + * Prefer {@link createValkeyTaskStore} which eagerly resolves the Valkey + * `TimeUnit` dependency. Direct construction is supported but the caller + * must supply the resolved `timeUnitSeconds` when `ttlSeconds` is used. + * + * @param options - Store configuration. + * @param timeUnitSeconds - Pre-resolved `TimeUnit.Seconds` value from `@valkey/valkey-glide`. + * @throws If `ttlSeconds` is provided but is not a positive finite number. + */ + constructor(options: ValkeyTaskStoreOptions, /** @internal */ timeUnitSeconds?: TimeUnit) { + if ( + options.ttlSeconds !== undefined && + (!Number.isFinite(options.ttlSeconds) || options.ttlSeconds <= 0) + ) { + throw new Error("ttlSeconds must be a positive finite number"); + } + this.client = options.client; + this.keyPrefix = options.keyPrefix ?? "a2a-tasks"; + this.ttlSeconds = options.ttlSeconds; + this.timeUnitSeconds = timeUnitSeconds; + } + + /** + * Lazily resolves and caches the `TimeUnit.Seconds` enum value from + * `@valkey/valkey-glide`. Called internally by {@link save} when TTL is configured. + */ + private async getTimeUnitSeconds(): Promise { + if (this.timeUnitSeconds !== undefined) return this.timeUnitSeconds; + try { + const mod = await import("@valkey/valkey-glide"); + this.timeUnitSeconds = mod.TimeUnit.Seconds; + return this.timeUnitSeconds; + } catch { + throw new Error(VALKEY_GLIDE_REQUIRED); + } + } + + /** + * Loads a task record from Valkey by agent and task ID. + * + * @param params - The agent ID and task ID identifying the record. + * @returns The deserialised {@link TaskRecord}, or `null` if not found. + * @throws If the stored value cannot be parsed as valid JSON. + */ + async load(params: { agentId: string; taskId: string }): Promise { + const key = this.makeKey(params.agentId, params.taskId); + const result = await this.client.get(key); + if (result === null) return null; + + let parsed: unknown; + try { + parsed = JSON.parse(String(result)); + } catch (error) { + const detail = error instanceof Error ? error.message : "Unknown error"; + throw new Error(`Failed to parse stored TaskRecord for key "${key}": ${detail}`); + } + + const validation = TaskRecordSchema.safeParse(parsed); + if (!validation.success) { + throw new Error( + `Invalid TaskRecord for key "${key}": ${safeStringify(validation.error.issues)}`, + ); + } + + return validation.data; + } + + /** + * Persists a task record to Valkey. + * + * The record is serialised with {@link safeStringify} and stored under a + * composite key derived from the agent ID and the record's task ID. When + * `ttlSeconds` is configured the key is set with an expiry. + * + * Note: unlike {@link load}, `save` does **not** run Zod validation. The + * caller is trusted to supply a well-typed `TaskRecord`, and skipping + * validation on the write path avoids the per-call overhead. Any schema + * drift will surface on the next `load()`. + * + * @param params - The agent ID and the {@link TaskRecord} to persist. + */ + async save(params: { agentId: string; data: TaskRecord }): Promise { + const taskId = params.data.id ?? randomUUID(); + const normalized: TaskRecord = + taskId === params.data.id ? params.data : { ...params.data, id: taskId }; + const key = this.makeKey(params.agentId, taskId); + const json = safeStringify(normalized); + + if (json.startsWith("SAFE_STRINGIFY_ERROR:")) { + throw new Error( + `Failed to serialize TaskRecord for agent "${params.agentId}", task "${taskId}": ${json}`, + ); + } + + if (this.ttlSeconds !== undefined) { + const seconds = await this.getTimeUnitSeconds(); + await this.client.set(key, json, { + expiry: { type: seconds, count: this.ttlSeconds }, + }); + } else { + await this.client.set(key, json); + } + } + + /** + * Builds the Valkey key for a given agent/task pair. + * + * Colons inside `agentId` and `taskId` are escaped to prevent collisions + * with the `keyPrefix:agentId::taskId` delimiter scheme. + */ + private makeKey(agentId: string, taskId: string): string { + // Escape colons in user-provided IDs to prevent key collisions with the delimiter. + const safeAgentId = agentId.replace(/:/g, "\\:"); + const safeTaskId = taskId.replace(/:/g, "\\:"); + return `${this.keyPrefix}:${safeAgentId}::${safeTaskId}`; + } +} diff --git a/packages/a2a-server/tsup.config.ts b/packages/a2a-server/tsup.config.ts index 0819104fd..ff573024e 100644 --- a/packages/a2a-server/tsup.config.ts +++ b/packages/a2a-server/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"; import { markAsExternalPlugin } from "../shared/tsup-plugins/mark-as-external"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/valkey-store.ts"], format: ["cjs", "esm"], splitting: false, sourcemap: true, diff --git a/packages/resumable-streams/package.json b/packages/resumable-streams/package.json index 8c2c03a27..369d69bc8 100644 --- a/packages/resumable-streams/package.json +++ b/packages/resumable-streams/package.json @@ -20,6 +20,16 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" } + }, + "./valkey-store": { + "import": { + "types": "./dist/valkey-store.d.mts", + "default": "./dist/valkey-store.mjs" + }, + "require": { + "types": "./dist/valkey-store.d.ts", + "default": "./dist/valkey-store.js" + } } }, "files": [ @@ -35,9 +45,15 @@ "main": "dist/index.js", "module": "dist/index.mjs", "peerDependencies": { + "@valkey/valkey-glide": ">=2.3.1", "@voltagent/core": "^2.0.0", "ai": "^6.0.0" }, + "peerDependenciesMeta": { + "@valkey/valkey-glide": { + "optional": true + } + }, "publishConfig": { "access": "public" }, diff --git a/packages/resumable-streams/src/resumable-streams.ts b/packages/resumable-streams/src/resumable-streams.ts index cad154466..80c282164 100644 --- a/packages/resumable-streams/src/resumable-streams.ts +++ b/packages/resumable-streams/src/resumable-streams.ts @@ -84,7 +84,8 @@ const getResumableStreamDisabledInfo = (value: unknown) => { return { reason, docsUrl }; }; -const markResumableStreamStoreType = ( +/** @internal */ +export const markResumableStreamStoreType = ( value: T, type: string, displayName?: string, @@ -201,7 +202,8 @@ const buildStreamKey = ({ conversationId, userId }: ResumableStreamContext) => { return `${userId}-${conversationId}`; }; -const buildActiveStreamKey = (keyPrefix: string, context: ResumableStreamContext) => +/** @internal */ +export const buildActiveStreamKey = (keyPrefix: string, context: ResumableStreamContext) => `${keyPrefix}:active:${buildStreamKey(context)}`; const buildActiveStreamQuery = (context: ResumableStreamContext, streamId?: string): string => { @@ -216,7 +218,8 @@ const buildActiveStreamQuery = (context: ResumableStreamContext, streamId?: stri return params.toString(); }; -const createActiveStreamStoreFromPublisher = ( +/** @internal */ +export const createActiveStreamStoreFromPublisher = ( publisher: ResumableStreamPublisher, keyPrefix: string, ): ResumableStreamActiveStore => ({ @@ -250,7 +253,8 @@ const createActiveStreamStoreFromPublisher = ( }, }); -const mergeStreamAndActiveStore = ( +/** @internal */ +export const mergeStreamAndActiveStore = ( streamStore: T, activeStreamStore: ResumableStreamActiveStore, ): T & ResumableStreamActiveStore => ({ @@ -260,6 +264,12 @@ const mergeStreamAndActiveStore = ( clearActiveStream: activeStreamStore.clearActiveStream, }); +/** + * Creates an in-memory active-stream store that tracks which stream ID is + * currently active for each conversation/user pair. + * + * @returns A {@link ResumableStreamActiveStore} backed by a `Map`. + */ export function createMemoryResumableStreamActiveStore(): ResumableStreamActiveStore { const activeStreams = new Map(); @@ -383,6 +393,14 @@ const createInMemoryPubSub = () => { return { publisher, subscriber }; }; +/** + * Creates an in-memory resumable stream store backed by the `resumable-stream` library. + * + * Useful for development and testing. Data does not survive process restarts. + * + * @param options - Optional key prefix and `waitUntil` callback. + * @returns A {@link ResumableStreamStore} with active-stream tracking. + */ export async function createResumableStreamMemoryStore( options: ResumableStreamStoreOptions = {}, ): Promise { @@ -402,6 +420,15 @@ export async function createResumableStreamMemoryStore( return markResumableStreamStoreType(mergedStore, "memory", "Memory"); } +/** + * Creates a Redis-backed resumable stream store. + * + * If `publisher` / `subscriber` clients are not provided, they are created + * automatically from the `REDIS_URL` or `KV_URL` environment variable. + * + * @param options - Redis connection and key prefix options. + * @returns A {@link ResumableStreamStore} with active-stream tracking. + */ export async function createResumableStreamRedisStore( options: ResumableStreamRedisStoreOptions = {}, ): Promise { @@ -446,6 +473,13 @@ export async function createResumableStreamRedisStore( return markResumableStreamStoreType(mergedStore, "redis", "Redis"); } +/** + * Creates a resumable stream store from user-supplied publisher and subscriber instances. + * + * @param options - Must include both `publisher` and `subscriber`. + * @returns A {@link ResumableStreamStore} with active-stream tracking. + * @throws If `publisher` or `subscriber` is missing. + */ export async function createResumableStreamGenericStore( options: ResumableStreamGenericStoreOptions, ): Promise { @@ -468,6 +502,14 @@ export async function createResumableStreamGenericStore( return markResumableStreamStoreType(mergedStore, "custom", "Custom"); } +/** + * Creates a resumable stream store backed by the VoltOps managed service. + * + * Returns a disabled store when the required API keys are not configured. + * + * @param options - VoltOps client or API key configuration. + * @returns A {@link ResumableStreamStore} with active-stream tracking, or a disabled stub. + */ export async function createResumableStreamVoltOpsStore( options: ResumableStreamVoltOpsStoreOptions = {}, ): Promise { @@ -571,6 +613,16 @@ export async function createResumableStreamVoltOpsStore( return markResumableStreamStoreType(mergedStore, "voltops", "VoltOps"); } +/** + * Builds a {@link ResumableStreamAdapter} from a stream store and active-stream store. + * + * If `activeStreamStore` is not provided explicitly, it is inferred from `streamStore` + * when the store implements the {@link ResumableStreamActiveStore} interface. + * + * @param config - Must include `streamStore`; `activeStreamStore` is inferred when possible. + * @returns A fully wired {@link ResumableStreamAdapter}. + * @throws If `streamStore` is missing or `activeStreamStore` cannot be resolved. + */ export async function createResumableStreamAdapter( config: ResumableStreamAdapterConfig, ): Promise { @@ -624,6 +676,17 @@ export async function createResumableStreamAdapter( return adapter; } +/** + * Merges an existing resumable stream adapter into server-provider dependencies. + * + * If `deps` already contains a `resumableStream`, the provided adapter is ignored + * (with a warning). Disabled adapters are also filtered out. + * + * @param deps - Current server-provider dependencies. + * @param adapter - Optional adapter to inject. + * @param logger - Optional logger for warnings. + * @returns Updated dependencies with the resolved adapter, if any. + */ export async function resolveResumableStreamDeps( deps: ServerProviderDeps, adapter: ResumableStreamAdapter | undefined, @@ -647,6 +710,14 @@ export async function resolveResumableStreamDeps( }; } +/** + * Validates a {@link ResumableStreamAdapter}, returning `undefined` + * when the adapter is disabled or invalid. + * + * @param adapter - The adapter to validate. + * @param logger - Optional logger for warnings. + * @returns The validated adapter, or `undefined`. + */ export function resolveResumableStreamAdapter( adapter: ResumableStreamAdapter | undefined, logger?: Logger, diff --git a/packages/resumable-streams/src/valkey-store.spec.ts b/packages/resumable-streams/src/valkey-store.spec.ts new file mode 100644 index 000000000..9c4a8b164 --- /dev/null +++ b/packages/resumable-streams/src/valkey-store.spec.ts @@ -0,0 +1,360 @@ +import { createResumableStreamValkeyStore } from "./valkey-store"; + +vi.mock("@valkey/valkey-glide", () => ({ + GlideClient: { createClient: vi.fn() }, + GlideClientConfiguration: { PubSubChannelModes: { Exact: 0 } }, + TimeUnit: { Seconds: "EX" }, +})); + +vi.mock("resumable-stream/generic", () => ({ + createResumableStreamContext: vi.fn().mockReturnValue({ + createNewResumableStream: vi.fn(), + resumeExistingStream: vi.fn(), + }), +})); + +function makeGlideClient() { + return { + get: vi.fn(), + set: vi.fn(), + incr: vi.fn(), + publish: vi.fn(), + del: vi.fn(), + }; +} + +function makeOptions(clientOverrides = {}) { + return { + client: { ...makeGlideClient(), ...clientOverrides }, + clientConfig: { addresses: [{ host: "localhost", port: 6379 }] }, + }; +} + +async function getPublisher(clientOverrides = {}) { + const { createResumableStreamContext } = await import("resumable-stream/generic"); + const mockCtx = vi.mocked(createResumableStreamContext); + mockCtx.mockClear(); + + const opts = makeOptions(clientOverrides); + await createResumableStreamValkeyStore(opts as any); + + const callArgs = mockCtx.mock.calls[0][0] as any; + return { publisher: callArgs.publisher, subscriber: callArgs.subscriber, client: opts.client }; +} + +describe("ValkeyResumableStreamStore — Publisher adapter", () => { + it("set with { EX: 60 } calls client.set with expiry", async () => { + const { publisher, client } = await getPublisher(); + (client as any).set.mockResolvedValue("OK"); + + await publisher.set("my-key", "my-value", { EX: 60 }); + + expect((client as any).set).toHaveBeenCalledWith("my-key", "my-value", { + expiry: { type: "EX", count: 60 }, + }); + }); + + it("set without EX calls client.set with just key and value", async () => { + const { publisher, client } = await getPublisher(); + (client as any).set.mockResolvedValue("OK"); + + await publisher.set("my-key", "my-value"); + + expect((client as any).set).toHaveBeenCalledWith("my-key", "my-value"); + expect((client as any).set).toHaveBeenCalledTimes(1); + expect((client as any).set.mock.calls[0]).toHaveLength(2); + }); + + it("get converts GlideString (Buffer) to string", async () => { + const { publisher, client } = await getPublisher(); + (client as any).get.mockResolvedValue(Buffer.from("hello")); + + const result = await publisher.get("some-key"); + + expect(result).toBe("hello"); + }); + + it("get returns null when client returns null", async () => { + const { publisher, client } = await getPublisher(); + (client as any).get.mockResolvedValue(null); + + const result = await publisher.get("missing-key"); + + expect(result).toBeNull(); + }); + + it("incr delegates to client.incr", async () => { + const { publisher, client } = await getPublisher(); + (client as any).incr.mockResolvedValue(5); + + const result = await publisher.incr("counter-key"); + + expect((client as any).incr).toHaveBeenCalledWith("counter-key"); + expect(result).toBe(5); + }); + + it("publish delegates to client.publish", async () => { + const { publisher, client } = await getPublisher(); + (client as any).publish.mockResolvedValue(1); + + const result = await publisher.publish("my-channel", "my-message"); + + expect((client as any).publish).toHaveBeenCalledWith("my-message", "my-channel"); + expect(result).toBe(1); + }); + + it("del calls client.del([key])", async () => { + const { publisher, client } = await getPublisher(); + (client as any).del.mockResolvedValue(1); + + await publisher.del("some-key"); + + expect((client as any).del).toHaveBeenCalledWith(["some-key"]); + }); +}); + +describe("ValkeyResumableStreamStore — Subscriber adapter", () => { + it("subscribe calls GlideClient.createClient with correct pubsubSubscriptions config", async () => { + const { GlideClient } = await import("@valkey/valkey-glide"); + const mockCreateClient = vi.mocked(GlideClient.createClient); + const mockSubClient = { close: vi.fn() }; + mockCreateClient.mockResolvedValue(mockSubClient as any); + + const { subscriber } = await getPublisher(); + const callback = vi.fn(); + + await subscriber.subscribe("test-channel", callback); + + expect(mockCreateClient).toHaveBeenCalledWith( + expect.objectContaining({ + addresses: [{ host: "localhost", port: 6379 }], + pubsubSubscriptions: expect.objectContaining({ + channelsAndPatterns: expect.objectContaining({ + 0: expect.any(Set), + }), + callback: expect.any(Function), + }), + }), + ); + + const callArg = mockCreateClient.mock.calls[0][0] as any; + expect(callArg.pubsubSubscriptions.channelsAndPatterns[0].has("test-channel")).toBe(true); + }); + + it("subscribe callback invokes the provided callback with msg.message", async () => { + const { GlideClient } = await import("@valkey/valkey-glide"); + const mockCreateClient = vi.mocked(GlideClient.createClient); + const mockSubClient = { close: vi.fn() }; + mockCreateClient.mockClear(); + mockCreateClient.mockResolvedValue(mockSubClient as any); + + const { subscriber } = await getPublisher(); + const callback = vi.fn(); + + mockCreateClient.mockClear(); + await subscriber.subscribe("test-channel", callback); + + const callArg = mockCreateClient.mock.calls[0][0] as any; + callArg.pubsubSubscriptions.callback({ message: "hello-world" }, null); + + expect(callback).toHaveBeenCalledWith("hello-world"); + }); + + it("unsubscribe calls close() on the subscription client and removes it", async () => { + const { GlideClient } = await import("@valkey/valkey-glide"); + const mockCreateClient = vi.mocked(GlideClient.createClient); + const mockSubClient = { close: vi.fn() }; + mockCreateClient.mockResolvedValue(mockSubClient as any); + + const { subscriber } = await getPublisher(); + await subscriber.subscribe("test-channel", vi.fn()); + await subscriber.unsubscribe("test-channel"); + + expect(mockSubClient.close).toHaveBeenCalledTimes(1); + + // Unsubscribing again should resolve safely (client already removed) + await subscriber.unsubscribe("test-channel"); + expect(mockSubClient.close).toHaveBeenCalledTimes(1); + }); +}); + +describe("ValkeyResumableStreamStore — factory", () => { + it("returns object with all required store methods", async () => { + const opts = makeOptions(); + const store = await createResumableStreamValkeyStore(opts as any); + + expect(typeof store.createNewResumableStream).toBe("function"); + expect(typeof store.resumeExistingStream).toBe("function"); + expect(typeof store.getActiveStreamId).toBe("function"); + expect(typeof store.setActiveStreamId).toBe("function"); + expect(typeof store.clearActiveStream).toBe("function"); + expect(typeof store.close).toBe("function"); + }); + + it("setActiveStreamId applies ttlSeconds as EX when configured", async () => { + const client = makeGlideClient(); + client.set.mockResolvedValue("OK"); + client.get.mockResolvedValue(null); + + const opts = { + ...makeOptions(), + client, + ttlSeconds: 600, + }; + const store = await createResumableStreamValkeyStore(opts as any); + + await store.setActiveStreamId( + { conversationId: "conv-1", userId: "user-1" } as any, + "stream-42", + ); + + expect(client.set).toHaveBeenCalledWith("resumable-stream:active:user-1-conv-1", "stream-42", { + expiry: { type: "EX", count: 600 }, + }); + }); + + it("setActiveStreamId does NOT apply EX when ttlSeconds is not configured", async () => { + const client = makeGlideClient(); + client.set.mockResolvedValue("OK"); + client.get.mockResolvedValue(null); + + const opts = { + ...makeOptions(), + client, + }; + const store = await createResumableStreamValkeyStore(opts as any); + + await store.setActiveStreamId( + { conversationId: "conv-1", userId: "user-1" } as any, + "stream-99", + ); + + const setCalls = client.set.mock.calls; + const activeSetCall = setCalls.find( + (c: any[]) => typeof c[0] === "string" && c[0].includes("active:"), + ); + expect(activeSetCall).toBeDefined(); + expect(activeSetCall).toHaveLength(2); + }); + + it("getActiveStreamId returns stored stream ID", async () => { + const client = makeGlideClient(); + client.get.mockResolvedValue("stream-123"); + client.set.mockResolvedValue("OK"); + + const opts = { ...makeOptions(), client }; + const store = await createResumableStreamValkeyStore(opts as any); + + const result = await store.getActiveStreamId({ + conversationId: "conv-1", + userId: "user-1", + } as any); + + expect(result).toBe("stream-123"); + expect(client.get).toHaveBeenCalledWith("resumable-stream:active:user-1-conv-1"); + }); + + it("getActiveStreamId returns null when no active stream exists", async () => { + const client = makeGlideClient(); + client.get.mockResolvedValue(null); + client.set.mockResolvedValue("OK"); + + const opts = { ...makeOptions(), client }; + const store = await createResumableStreamValkeyStore(opts as any); + + const result = await store.getActiveStreamId({ + conversationId: "conv-1", + userId: "user-1", + } as any); + + expect(result).toBeNull(); + }); + + it("getActiveStreamId returns null when stored value is empty string", async () => { + const client = makeGlideClient(); + client.get.mockResolvedValue(""); + client.set.mockResolvedValue("OK"); + + const opts = { ...makeOptions(), client }; + const store = await createResumableStreamValkeyStore(opts as any); + + const result = await store.getActiveStreamId({ + conversationId: "conv-1", + userId: "user-1", + } as any); + + expect(result).toBeNull(); + }); + + it("clearActiveStream deletes key when streamId matches current value", async () => { + const client = makeGlideClient(); + client.get.mockResolvedValue("stream-42"); + client.del.mockResolvedValue(1); + client.set.mockResolvedValue("OK"); + + const opts = { ...makeOptions(), client }; + const store = await createResumableStreamValkeyStore(opts as any); + + await store.clearActiveStream({ + conversationId: "conv-1", + userId: "user-1", + streamId: "stream-42", + } as any); + + expect(client.del).toHaveBeenCalledWith(["resumable-stream:active:user-1-conv-1"]); + }); + + it("clearActiveStream does NOT delete key when streamId does not match", async () => { + const client = makeGlideClient(); + client.get.mockResolvedValue("stream-other"); + client.del.mockResolvedValue(1); + client.set.mockResolvedValue("OK"); + + const opts = { ...makeOptions(), client }; + const store = await createResumableStreamValkeyStore(opts as any); + + await store.clearActiveStream({ + conversationId: "conv-1", + userId: "user-1", + streamId: "stream-42", + } as any); + + expect(client.del).not.toHaveBeenCalled(); + }); + + it("store is tagged with valkey type marker", async () => { + const opts = makeOptions(); + const store = (await createResumableStreamValkeyStore(opts as any)) as any; + + expect(store.__voltagentResumableStoreType).toBe("valkey"); + }); + + it("close() closes all subscription clients and clears the map", async () => { + const { GlideClient } = await import("@valkey/valkey-glide"); + const mockCreateClient = vi.mocked(GlideClient.createClient); + const mockSubClient1 = { close: vi.fn() }; + const mockSubClient2 = { close: vi.fn() }; + mockCreateClient + .mockResolvedValueOnce(mockSubClient1 as any) + .mockResolvedValueOnce(mockSubClient2 as any); + + const opts = makeOptions(); + const store = await createResumableStreamValkeyStore(opts as any); + + const { createResumableStreamContext } = await import("resumable-stream/generic"); + const mockCtx = vi.mocked(createResumableStreamContext); + const callArgs = mockCtx.mock.calls[mockCtx.mock.calls.length - 1][0] as any; + await callArgs.subscriber.subscribe("channel-1", vi.fn()); + await callArgs.subscriber.subscribe("channel-2", vi.fn()); + + await store.close(); + + expect(mockSubClient1.close).toHaveBeenCalledTimes(1); + expect(mockSubClient2.close).toHaveBeenCalledTimes(1); + + // Calling close() again should be safe (map is cleared) + await store.close(); + expect(mockSubClient1.close).toHaveBeenCalledTimes(1); + expect(mockSubClient2.close).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/resumable-streams/src/valkey-store.ts b/packages/resumable-streams/src/valkey-store.ts new file mode 100644 index 000000000..fb444ffe0 --- /dev/null +++ b/packages/resumable-streams/src/valkey-store.ts @@ -0,0 +1,251 @@ +import type { GlideClient, GlideClusterClient, TimeUnit } from "@valkey/valkey-glide"; +import { + buildActiveStreamKey, + createActiveStreamStoreFromPublisher, + markResumableStreamStoreType, + mergeStreamAndActiveStore, +} from "./resumable-streams"; +import type { ResumableStreamActiveStore, ResumableStreamStore } from "./types"; + +const DEFAULT_KEY_PREFIX = "resumable-stream"; + +/** + * Connection configuration passed to the Valkey GLIDE client. + * + * At minimum, `addresses` must contain one `{ host, port }` entry. Additional + * properties (TLS, timeouts, etc.) are forwarded to the underlying GLIDE + * client constructor. + */ +export interface ValkeyConnectionConfig { + addresses: Array<{ host: string; port: number }>; + useTLS?: boolean; + requestTimeout?: number; + clientName?: string; + [key: string]: unknown; +} + +/** + * Options for creating a Valkey-backed resumable stream store via + * {@link createResumableStreamValkeyStore}. + */ +export interface ResumableStreamValkeyStoreOptions { + /** Valkey client instance (standalone {@link GlideClient} or {@link GlideClusterClient}). */ + client: GlideClient | GlideClusterClient; + /** Connection config reused when creating per-channel subscription clients. */ + clientConfig: ValkeyConnectionConfig; + /** Key prefix for all Valkey keys managed by this store. Defaults to `"resumable-stream"`. */ + keyPrefix?: string; + /** Optional TTL in seconds applied to active-stream keys. Must be a positive finite number. */ + // Applied to active stream keys only; stream data keys are managed by resumable-stream/generic + ttlSeconds?: number; + /** + * Maximum number of concurrent subscription channels. Each subscription creates a + * dedicated GlideClient TCP connection (required by the Glide pub/sub model), so this + * also caps the number of open connections. Defaults to 1000. + */ + maxSubscriptions?: number; + /** Optional callback (e.g. from a serverless runtime) to keep the process alive while background work completes. */ + waitUntil?: ((promise: Promise) => void) | null; +} + +/** + * A resumable stream store backed by Valkey, combining stream creation/resumption + * with active-stream tracking and a {@link close} method for cleanup. + */ +export type ValkeyResumableStreamStore = ResumableStreamStore & + ResumableStreamActiveStore & { + close(): Promise; + }; + +/** + * Creates a Valkey-backed resumable stream store. + * + * The returned store uses the provided {@link GlideClient} (or + * {@link GlideClusterClient}) for key-value and pub/sub operations required by + * the `resumable-stream/generic` library. Each pub/sub subscription creates a + * dedicated GLIDE client connection (required by the GLIDE pub/sub model). + * + * @param options - Store configuration including the Valkey client, connection + * config, optional key prefix, TTL, and subscription limits. + * @returns A {@link ValkeyResumableStreamStore} ready for use. + * @throws If `@valkey/valkey-glide` is not installed or is an incompatible version. + * @throws If `ttlSeconds` is provided but is not a positive finite number. + */ +export async function createResumableStreamValkeyStore( + options: ResumableStreamValkeyStoreOptions, +): Promise { + let GlideClientClass: typeof GlideClient; + let GlideClientConfigurationClass: { PubSubChannelModes: { Exact: number } }; + let timeUnit: typeof TimeUnit; + + try { + const mod = await import("@valkey/valkey-glide"); + GlideClientClass = mod.GlideClient; + // PubSubChannelModes isn't exported as a value type; use a single guarded access. + const PubSubExact = (mod as any).GlideClientConfiguration?.PubSubChannelModes?.Exact; + if (PubSubExact === undefined) { + throw new Error( + "GlideClientConfiguration.PubSubChannelModes.Exact is not available. " + + "The installed version of @valkey/valkey-glide may be incompatible.", + ); + } + GlideClientConfigurationClass = { PubSubChannelModes: { Exact: PubSubExact } }; + timeUnit = mod.TimeUnit; + } catch (err) { + if (err instanceof Error && err.message.includes("PubSubChannelModes")) { + throw err; + } + throw new Error( + "@valkey/valkey-glide is required for createResumableStreamValkeyStore. " + + "Install it with: pnpm add @valkey/valkey-glide", + ); + } + + const keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX; + const { client, clientConfig } = options; + + if ( + options.ttlSeconds !== undefined && + (!Number.isFinite(options.ttlSeconds) || options.ttlSeconds <= 0) + ) { + throw new Error("ttlSeconds must be a positive finite number"); + } + + // Publisher adapter + const publisher = { + async connect() {}, + async publish(channel: string, message: string) { + return client.publish(message, channel); + }, + async set(key: string, value: string, setOptions?: { EX?: number }) { + if (setOptions?.EX !== undefined) { + return client.set(key, value, { + expiry: { type: timeUnit.Seconds, count: setOptions.EX }, + }); + } + return client.set(key, value); + }, + async get(key: string): Promise { + const result = await client.get(key); + return result !== null ? String(result) : null; + }, + async incr(key: string) { + return client.incr(key); + }, + async del(key: string) { + return client.del([key]); + }, + }; + + // Subscriber adapter — one dedicated client per channel (Glide pub/sub requirement). + // Detect whether the caller provided a cluster client so subscription clients match. + let GlideClusterClientClass: typeof GlideClusterClient | undefined; + try { + const mod = await import("@valkey/valkey-glide"); + GlideClusterClientClass = mod.GlideClusterClient; + } catch { + // Already handled above; GlideClusterClient is only needed for cluster mode. + } + const isClusterMode = + GlideClusterClientClass !== undefined && client instanceof GlideClusterClientClass; + + const maxSubscriptions = options.maxSubscriptions ?? 1000; + const subscriptionClients = new Map(); + // Guard against concurrent subscribe calls interleaving across awaits. + const pendingSubscriptions = new Set(); + + const subscriber = { + async connect() {}, + async subscribe(channel: string, callback: (message: string) => void) { + // Close any existing client for this channel to avoid resource leaks on duplicate calls + const existing = subscriptionClients.get(channel); + if (existing) { + existing.close(); + subscriptionClients.delete(channel); + } + + if (pendingSubscriptions.has(channel)) { + throw new Error(`A subscription for channel "${channel}" is already being established.`); + } + + if (subscriptionClients.size + pendingSubscriptions.size >= maxSubscriptions) { + throw new Error( + `Maximum subscription limit (${maxSubscriptions}) reached. Unsubscribe from existing channels before subscribing to new ones.`, + ); + } + + pendingSubscriptions.add(channel); + try { + const pubsubConfig = { + ...clientConfig, + pubsubSubscriptions: { + channelsAndPatterns: { + [GlideClientConfigurationClass.PubSubChannelModes.Exact]: new Set([channel]), + }, + callback: (msg: { message: unknown }, _ctx: unknown) => callback(String(msg.message)), + }, + }; + + const subClient = + isClusterMode && GlideClusterClientClass + ? await GlideClusterClientClass.createClient(pubsubConfig) + : await GlideClientClass.createClient(pubsubConfig); + + subscriptionClients.set(channel, subClient); + } finally { + pendingSubscriptions.delete(channel); + } + }, + async unsubscribe(channel: string) { + const subClient = subscriptionClients.get(channel); + if (subClient) { + subClient.close(); + subscriptionClients.delete(channel); + } + }, + }; + + const { createResumableStreamContext } = await import("resumable-stream/generic"); + + const streamStore = createResumableStreamContext({ + keyPrefix, + waitUntil: options.waitUntil ?? null, + publisher, + subscriber, + }) as ResumableStreamStore; + + const activeStreamStore = createActiveStreamStoreFromPublisher(publisher, keyPrefix); + + // Wire ttlSeconds into setActiveStreamId so active stream keys expire + const ttlSeconds = options.ttlSeconds; + const ttlActiveStreamStore = + ttlSeconds !== undefined + ? { + ...activeStreamStore, + async setActiveStreamId( + context: Parameters[0], + streamId: string, + ) { + const key = buildActiveStreamKey(keyPrefix, context); + await publisher.set(key, streamId, { EX: ttlSeconds }); + }, + } + : activeStreamStore; + + const mergedStore = mergeStreamAndActiveStore(streamStore, ttlActiveStreamStore); + const taggedStore = markResumableStreamStoreType(mergedStore, "valkey", "Valkey"); + + return { + ...taggedStore, + /** + * Closes all internally-created subscription clients. The main `client` passed in + * `options` is **not** closed — the caller retains ownership of its lifecycle. + */ + async close() { + for (const subClient of subscriptionClients.values()) { + subClient.close(); + } + subscriptionClients.clear(); + }, + }; +} diff --git a/packages/resumable-streams/tsconfig.json b/packages/resumable-streams/tsconfig.json index d4612442c..a8814988a 100644 --- a/packages/resumable-streams/tsconfig.json +++ b/packages/resumable-streams/tsconfig.json @@ -23,8 +23,8 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "types": ["node"] + "types": ["vitest/globals", "node"] }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "include": ["src/**/*.ts", "src/**/*.spec-d.ts"], + "exclude": ["node_modules", "dist", "src/**/*.spec.ts"] } diff --git a/packages/resumable-streams/tsup.config.ts b/packages/resumable-streams/tsup.config.ts index e019584d6..c2af14e7f 100644 --- a/packages/resumable-streams/tsup.config.ts +++ b/packages/resumable-streams/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "tsup"; import { markAsExternalPlugin } from "../shared/tsup-plugins/mark-as-external"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/valkey-store.ts"], format: ["cjs", "esm"], splitting: false, sourcemap: true, diff --git a/packages/resumable-streams/vitest.config.ts b/packages/resumable-streams/vitest.config.ts new file mode 100644 index 000000000..236b2f5eb --- /dev/null +++ b/packages/resumable-streams/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.spec.ts"], + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.d.ts", "src/**/index.ts"], + }, + globals: true, + testTimeout: 10000, + hookTimeout: 10000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1369a0c8..ff08bd649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2104,7 +2104,7 @@ importers: version: 6.0.3(zod@3.25.76) nuxt: specifier: ^4.1.2 - version: 4.1.3(@biomejs/biome@1.9.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.2.1)(@vue/compiler-sfc@3.5.22)(eslint@9.33.0)(typescript@5.9.3)(vite@7.2.7) + version: 4.1.3(@biomejs/biome@1.9.4)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.2.1)(@vue/compiler-sfc@3.5.22)(eslint@9.33.0)(typescript@5.9.3)(vite@7.2.7) vue: specifier: ^3.5.22 version: 3.5.22(typescript@5.9.3) @@ -2980,6 +2980,46 @@ importers: specifier: ^5.8.2 version: 5.9.2 + examples/with-valkey-store: + dependencies: + '@valkey/valkey-glide': + specifier: ^2.3.1 + version: 2.3.1 + '@voltagent/a2a-server': + specifier: ^2.0.3 + version: link:../../packages/a2a-server + '@voltagent/core': + specifier: ^2.7.2 + version: link:../../packages/core + '@voltagent/internal': + specifier: ^1.0.3 + version: link:../../packages/internal + '@voltagent/logger': + specifier: ^2.0.2 + version: link:../../packages/logger + '@voltagent/resumable-streams': + specifier: ^2.0.2 + version: link:../../packages/resumable-streams + '@voltagent/server-hono': + specifier: ^2.0.12 + version: link:../../packages/server-hono + ai: + specifier: ^6.0.0 + version: 6.0.3(zod@3.25.76) + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^24.2.1 + version: 24.6.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.8.2 + version: 5.9.3 + examples/with-vector-search: dependencies: '@ai-sdk/openai': @@ -3667,6 +3707,9 @@ importers: '@a2a-js/sdk': specifier: ^0.2.5 version: 0.2.5 + '@valkey/valkey-glide': + specifier: '>=2.3.1' + version: 2.3.1 '@voltagent/internal': specifier: ^1.0.2 version: link:../internal @@ -4254,6 +4297,9 @@ importers: packages/resumable-streams: dependencies: + '@valkey/valkey-glide': + specifier: '>=2.3.1' + version: 2.3.1 '@voltagent/core': specifier: ^2.6.6 version: link:../core @@ -4274,10 +4320,10 @@ importers: dependencies: '@blaxel/core': specifier: ^0.2.0 - version: 0.2.81(@hey-api/openapi-ts@0.97.1) + version: 0.2.95(@hey-api/openapi-ts@0.99.0) es-toolkit: specifier: ^1.46.1 - version: 1.46.1 + version: 1.51.0 devDependencies: '@types/node': specifier: ^24.2.1 @@ -6428,7 +6474,7 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -6446,10 +6492,10 @@ packages: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.0) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 @@ -6647,7 +6693,7 @@ packages: dependencies: '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -6661,7 +6707,7 @@ packages: dependencies: '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -6730,10 +6776,6 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.27.1: - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier@7.28.5: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} @@ -6760,14 +6802,6 @@ packages: '@babel/template': 7.27.2 '@babel/types': 7.28.5 - /@babel/parser@7.28.0: - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.28.5 - dev: true - /@babel/parser@7.28.4: resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} engines: {node: '>=6.0.0'} @@ -7528,7 +7562,7 @@ packages: '@babel/core': 7.28.0 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.0) '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.4 transitivePeerDependencies: - supports-color @@ -8065,7 +8099,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 dev: true /@babel/types@7.28.5: @@ -8167,11 +8201,11 @@ packages: requiresBuild: true optional: true - /@blaxel/core@0.2.81(@hey-api/openapi-ts@0.97.1): - resolution: {integrity: sha512-uMi58QUOXm+I/aeoUW7mNHP5SW+O6PiPqHGmKnXLKjxHe9I2Zgvw/tea87oPshAq4q6mWLhjg5QfnXghB7Op0w==} + /@blaxel/core@0.2.95(@hey-api/openapi-ts@0.99.0): + resolution: {integrity: sha512-wbwIhqXlgPbyCDY4vZReqk60YLE8C2HKILgag4rG2y5Ag2mEdnM5YS5MZi3Zj9f1z9SuTyrME8DJYCiKRmlqAw==} engines: {node: '>=18'} dependencies: - '@hey-api/client-fetch': 0.10.2(@hey-api/openapi-ts@0.97.1) + '@hey-api/client-fetch': 0.10.2(@hey-api/openapi-ts@0.99.0) '@modelcontextprotocol/sdk': 1.30.0(zod@3.25.76) archiver: 7.0.1 axios: 1.13.5 @@ -9204,7 +9238,7 @@ packages: /@daytonaio/api-client@0.139.0: resolution: {integrity: sha512-Xmjrkq7MW/DaZLQEf0HfT7Y38N8SesXvWqAKSEjdf3ifGVQVx37aOB8El1jOvfrndqzTcSbumSxro2nhKK5K5A==} dependencies: - axios: 1.13.2 + axios: 1.13.5 transitivePeerDependencies: - debug dev: false @@ -9236,7 +9270,7 @@ packages: /@daytonaio/toolbox-api-client@0.139.0: resolution: {integrity: sha512-zLonkWHsdmrwT2qCZ/zBt4dpWNJ7N08eGYjCk30Bihzk4JY0afNBPDqd+pdGdXKtuJ44yCxR4iJyjfWR11J9PA==} dependencies: - axios: 1.13.2 + axios: 1.13.5 transitivePeerDependencies: - debug dev: false @@ -9269,11 +9303,11 @@ packages: pathe: 1.1.2 dev: false - /@emnapi/core@1.11.1: - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + /@emnapi/core@1.10.0: + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} requiresBuild: true dependencies: - '@emnapi/wasi-threads': 1.2.2 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 dev: false optional: true @@ -9285,8 +9319,8 @@ packages: tslib: 2.8.1 dev: true - /@emnapi/runtime@1.11.1: - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + /@emnapi/runtime@1.10.0: + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} requiresBuild: true dependencies: tslib: 2.8.1 @@ -9304,8 +9338,8 @@ packages: tslib: 2.8.1 dev: true - /@emnapi/wasi-threads@1.2.2: - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + /@emnapi/wasi-threads@1.2.1: + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} requiresBuild: true dependencies: tslib: 2.8.1 @@ -10059,7 +10093,7 @@ packages: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -10403,18 +10437,18 @@ packages: axios: 1.11.0 dev: false - /@hey-api/client-fetch@0.10.2(@hey-api/openapi-ts@0.97.1): + /@hey-api/client-fetch@0.10.2(@hey-api/openapi-ts@0.99.0): resolution: {integrity: sha512-AGiFYDx+y8VT1wlQ3EbzzZtfU8EfV+hLLRTtr8Y/tjYZaxIECwJagVZf24YzNbtEBXONFV50bwcU1wLVGXe1ow==} deprecated: Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts. peerDependencies: '@hey-api/openapi-ts': < 2 dependencies: - '@hey-api/openapi-ts': 0.97.1(typescript@5.9.3) + '@hey-api/openapi-ts': 0.99.0(typescript@5.9.3) dev: false - /@hey-api/codegen-core@0.8.1: - resolution: {integrity: sha512-Iciv2vUCJTW9lWM/ROvyZLblmcbYJHPuXfzb1SzeDVVn4xEXu2ilLU1pq3fn+09FZ/Y0P7VyvRE47UDU6om8xA==} - engines: {node: '>=22.13.0'} + /@hey-api/codegen-core@0.9.1: + resolution: {integrity: sha512-s97jL1dgTMuiMHv2BZ1X4Tgd99Mf9GOvGdNqNcGwIMmnR+PgYNoraj4Zvp134MKsNCap/m7k0r0vKKnl56pj4w==} + engines: {node: '>=22.18.0'} dependencies: '@hey-api/types': 0.1.4 ansi-colors: 4.1.3 @@ -10424,49 +10458,49 @@ packages: - magicast dev: false - /@hey-api/json-schema-ref-parser@1.4.2: - resolution: {integrity: sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q==} - engines: {node: '>=22.13.0'} + /@hey-api/json-schema-ref-parser@1.4.4: + resolution: {integrity: sha512-otmd+zCxbYVBIp/mlMTnGkvlNYLkVKgs3VOIq0kSnenhB1+fRwLPQIeSwyWM6E51oXhUedkYjVsVpkVexeuJOA==} + engines: {node: '>=22.18.0'} dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 - js-yaml: 4.1.1 + js-yaml: 4.2.0 dev: false - /@hey-api/openapi-ts@0.97.1(typescript@5.9.3): - resolution: {integrity: sha512-LksUJeXAqwf6OhcCCr3/B4YjnBs5rqSqjDUKMBvkgp4OhaCQiJrOvntctFxdnugy8jUojP4yi/eJf5xYzcYzCQ==} - engines: {node: '>=22.13.0'} + /@hey-api/openapi-ts@0.99.0(typescript@5.9.3): + resolution: {integrity: sha512-SePU/5oEWWkvUBYmvzdYRctseoLuskyhs4ET0RvLIcmzc8yLQoA2R+KtBIQ8bPsoSUB0m4E5SmBnl6aGSA0szQ==} + engines: {node: '>=22.18.0'} hasBin: true peerDependencies: typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' dependencies: - '@hey-api/codegen-core': 0.8.1 - '@hey-api/json-schema-ref-parser': 1.4.2 - '@hey-api/shared': 0.4.3 + '@hey-api/codegen-core': 0.9.1 + '@hey-api/json-schema-ref-parser': 1.4.4 + '@hey-api/shared': 0.5.0 '@hey-api/spec-types': 0.2.0 '@hey-api/types': 0.1.4 '@lukeed/ms': 2.0.2 ansi-colors: 4.1.3 color-support: 1.1.3 - commander: 14.0.3 + commander: 15.0.0 get-tsconfig: 4.14.0 typescript: 5.9.3 transitivePeerDependencies: - magicast dev: false - /@hey-api/shared@0.4.3: - resolution: {integrity: sha512-3tHfZNXgGOt+3P3Kq9cvqmZ9i7e3jtrkip1uDpZTX1+hTNboHhYdjxnT8AbrDuvslTaQHoAOlP4/iCDdzd9Jag==} - engines: {node: '>=22.13.0'} + /@hey-api/shared@0.5.0: + resolution: {integrity: sha512-JN/j4Ebh4cJGYIQ5cwWuqe7GeSUyQoz7oC51WqyhKOcrejK6DKZMDkshc5d1eKTRuRL+rjozuRcoUaZZn2DGPw==} + engines: {node: '>=22.18.0'} dependencies: - '@hey-api/codegen-core': 0.8.1 - '@hey-api/json-schema-ref-parser': 1.4.2 + '@hey-api/codegen-core': 0.9.1 + '@hey-api/json-schema-ref-parser': 1.4.4 '@hey-api/spec-types': 0.2.0 '@hey-api/types': 0.1.4 ansi-colors: 4.1.3 cross-spawn: 7.0.6 open: 11.0.0 - semver: 7.7.4 + semver: 7.8.4 transitivePeerDependencies: - magicast dev: false @@ -10498,13 +10532,13 @@ packages: hono: 4.9.1 dev: false - /@hono/node-server@1.19.17(hono@4.12.34): + /@hono/node-server@1.19.17(hono@4.13.2): resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 dependencies: - hono: 4.12.34 + hono: 4.13.2 /@hono/swagger-ui@0.5.2(hono@4.9.1): resolution: {integrity: sha512-7wxLKdb8h7JTdZ+K8DJNE3KXQMIpJejkBTQjrYlUWF28Z1PGOKw6kUykARe5NTfueIN37jbyG/sBYsbzXzG53A==} @@ -10911,7 +10945,7 @@ packages: cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 1.11.1 + '@emnapi/runtime': 1.10.0 optional: true /@img/sharp-wasm32@0.34.4: @@ -10920,7 +10954,7 @@ packages: cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 1.11.1 + '@emnapi/runtime': 1.10.0 optional: true /@img/sharp-win32-arm64@0.34.4: @@ -11442,18 +11476,11 @@ packages: resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} - /@isaacs/brace-expansion@5.0.0: - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/balanced-match': 4.0.1 - /@isaacs/brace-expansion@5.0.1: resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} engines: {node: 20 || >=22} dependencies: '@isaacs/balanced-match': 4.0.1 - dev: false /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -12234,13 +12261,13 @@ packages: cors: 2.8.5 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.0.3 + eventsource-parser: 3.0.6 express: 5.1.0 express-rate-limit: 7.5.1(express@5.1.0) pkce-challenge: 5.0.0 - raw-body: 3.0.0 + raw-body: 3.0.1 zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - supports-color dev: false @@ -12255,7 +12282,7 @@ packages: '@cfworker/json-schema': optional: true dependencies: - '@hono/node-server': 1.19.17(hono@4.12.34) + '@hono/node-server': 1.19.17(hono@4.13.2) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -12264,8 +12291,8 @@ packages: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 8.6.1(express@5.2.1) - hono: 4.12.34 + express-rate-limit: 8.6.2(express@5.2.1) + hono: 4.13.2 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 @@ -12285,7 +12312,7 @@ packages: '@cfworker/json-schema': optional: true dependencies: - '@hono/node-server': 1.19.17(hono@4.12.34) + '@hono/node-server': 1.19.17(hono@4.13.2) ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 @@ -12294,8 +12321,8 @@ packages: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 8.6.1(express@5.2.1) - hono: 4.12.34 + express-rate-limit: 8.6.2(express@5.2.1) + hono: 4.13.2 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.0 @@ -12521,9 +12548,9 @@ packages: resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} requiresBuild: true dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 dev: false optional: true @@ -12535,16 +12562,16 @@ packages: '@tybys/wasm-util': 0.9.0 dev: true - /@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + /@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} requiresBuild: true peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 dev: false optional: true @@ -12738,7 +12765,7 @@ packages: minimatch: 9.0.5 read-pkg: 9.0.1 semver: 7.7.2 - yaml: 2.8.1 + yaml: 2.8.2 yargs: 17.7.2 dev: true @@ -12770,7 +12797,7 @@ packages: ansis: 4.1.0 clean-stack: 5.3.0 execa: 8.0.1 - fdir: 6.4.6 + fdir: 6.5.0(picomatch@4.0.3) figures: 6.1.0 filter-obj: 6.1.0 hot-shots: 11.1.0 @@ -12802,7 +12829,7 @@ packages: ts-node: 10.9.2(@swc/core@1.5.29)(@types/node@24.2.1)(typescript@5.9.3) typescript: 5.9.3 uuid: 11.1.0 - yaml: 2.8.1 + yaml: 2.8.2 yargs: 17.7.2 zod: 3.25.76 transitivePeerDependencies: @@ -12854,7 +12881,7 @@ packages: read-package-up: 11.0.0 tomlify-j0.4: 3.0.0 validate-npm-package-name: 5.0.1 - yaml: 2.8.1 + yaml: 2.8.2 yargs: 17.7.2 zod: 4.3.5 dev: true @@ -13165,7 +13192,7 @@ packages: engines: {node: '>=18.14.0'} hasBin: true dependencies: - '@babel/parser': 7.28.0 + '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.5.0 @@ -15276,13 +15303,13 @@ packages: dev: false optional: true - /@oxc-minify/binding-wasm32-wasi@0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + /@oxc-minify/binding-wasm32-wasi@0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): resolution: {integrity: sha512-0bOaEuh7QX8MfqyrRjNPOWhcsYl0IGoHX1nPtFIFGm0f/AJsJ+3wbyI9WvkAOXZmRgI9DMKGbDJdU6J59JxA7w==} engines: {node: '>=14.0.0'} cpu: [wasm32] requiresBuild: true dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -15415,13 +15442,13 @@ packages: dev: false optional: true - /@oxc-parser/binding-wasm32-wasi@0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + /@oxc-parser/binding-wasm32-wasi@0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): resolution: {integrity: sha512-VoCtQZIsRZN8mszbdizh+5MwzbgbMxsPgT2hOzzILQLNY2o2OXG3xSiFNFakVhbWc9qSTaZ/MRDsqR+IM3fLFw==} engines: {node: '>=14.0.0'} cpu: [wasm32] requiresBuild: true dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -15562,13 +15589,13 @@ packages: dev: false optional: true - /@oxc-transform/binding-wasm32-wasi@0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + /@oxc-transform/binding-wasm32-wasi@0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): resolution: {integrity: sha512-rn3c2wGT3ha6j0VLykYOkXU5YyQYIeGXRsDPP7xyiZHVTVssoM0X1BuheFlgxmC1POXMT+dAAcVOFG5MdW1bnQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] requiresBuild: true dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -17794,9 +17821,6 @@ packages: dev: true optional: true - /@rolldown/pluginutils@1.0.0: - resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} - /@rolldown/pluginutils@1.0.0-beta.29: resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} dev: false @@ -17805,6 +17829,9 @@ packages: resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} dev: true + /@rolldown/pluginutils@1.0.1: + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + /@rollup/plugin-alias@5.1.1(rollup@4.50.2): resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -20270,8 +20297,8 @@ packages: minimatch: 9.0.5 dev: true - /@tybys/wasm-util@0.10.3: - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + /@tybys/wasm-util@0.10.1: + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} requiresBuild: true dependencies: tslib: 2.8.1 @@ -21415,6 +21442,69 @@ packages: - graphql dev: false + /@valkey/valkey-glide-darwin-arm64@2.3.1: + resolution: {integrity: sha512-S3uCwGoBqQuVieBsevftlXzy/lFkDf49wQkg0UaRGMoWhmaik8d9rwTzKky1+otLjWCblWtMDcqIWGMs5ppo8Q==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@valkey/valkey-glide-darwin-x64@2.3.1: + resolution: {integrity: sha512-7F+UoyMhO0QiAHEDZApy0+PNR7v7Gct5SMiRH+8RuUk8Nf1Cc3mNVtFXk0jwwbhatiWiSNEaMDkTRPWYZN6IcQ==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@valkey/valkey-glide-linux-arm64-gnu@2.3.1: + resolution: {integrity: sha512-Aigxpq6oSxPhdX7V0JZsQrBW4pzcNibCb1m6XorGdWpG62sttV/3DKz/06V+h1dSGfqpkjWPxRJS1EmTqPYmZw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@valkey/valkey-glide-linux-arm64-musl@2.3.1: + resolution: {integrity: sha512-vmohWbUncgH+Rz1OnXPeSDVChCFMy0MYL38qcmAS05aGwL2sgMjipsEWiAsc82IAauKsVE7Ou68YZcyYK/mhag==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@valkey/valkey-glide-linux-x64-gnu@2.3.1: + resolution: {integrity: sha512-v/pJefhNO2fjEKq7uQAked896S2N3ZymiO/ei2v8JRxbYk1M+/Ffl6NH6SG+Mo/k2dyeRN1B5PXNq79jv149Rg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@valkey/valkey-glide-linux-x64-musl@2.3.1: + resolution: {integrity: sha512-GAcWs6MrS4v6/mjoaYyd8ay+L6Zl2L9YjkuKsupWzjSsT+PEVV/3yX0ecNeK7SGMn5Wfd3InZ3gfGPEsWEhc0g==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@valkey/valkey-glide@2.3.1: + resolution: {integrity: sha512-HzKCoNRSJYG83vKrYVJk+By2a0qVt/VuQfziCtdM6Q3Lt2WUE1PqxUAwO1Br2flSgrTxNdGgaGmX8qdnM25d5w==} + engines: {node: '>=16'} + dependencies: + long: 5.3.2 + protobufjs: 7.5.3 + optionalDependencies: + '@valkey/valkey-glide-darwin-arm64': 2.3.1 + '@valkey/valkey-glide-darwin-x64': 2.3.1 + '@valkey/valkey-glide-linux-arm64-gnu': 2.3.1 + '@valkey/valkey-glide-linux-arm64-musl': 2.3.1 + '@valkey/valkey-glide-linux-x64-gnu': 2.3.1 + '@valkey/valkey-glide-linux-x64-musl': 2.3.1 + dev: false + /@vercel/nft@0.29.4(supports-color@10.2.2): resolution: {integrity: sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==} engines: {node: '>=18'} @@ -21501,7 +21591,7 @@ packages: '@babel/core': 7.28.5 '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0 + '@rolldown/pluginutils': 1.0.1 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) vite: 7.2.7(@types/node@24.2.1)(jiti@2.6.1) vue: 3.5.22(typescript@5.9.3) @@ -23184,7 +23274,7 @@ packages: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} dependencies: follow-redirects: 1.15.11(debug@4.3.2) - form-data: 4.0.4 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -23198,7 +23288,6 @@ packages: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false /b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -23591,7 +23680,7 @@ packages: engines: {node: '>=18'} dependencies: bytes: 3.1.2 - content-type: 2.0.0 + content-type: 2.1.0 debug: 4.4.3(supports-color@10.2.2) http-errors: 2.0.1 iconv-lite: 0.7.3 @@ -23825,8 +23914,8 @@ packages: confbox: 0.2.4 defu: 6.1.7 dotenv: 17.4.2 - exsolve: 1.0.8 - giget: 3.2.0 + exsolve: 1.1.1 + giget: 3.3.1 jiti: 2.6.1 ohash: 2.0.11 pathe: 2.0.3 @@ -24184,7 +24273,7 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} dependencies: - readdirp: 5.0.0 + readdirp: 5.1.1 dev: false /chownr@1.1.4: @@ -24714,9 +24803,9 @@ packages: engines: {node: '>=18'} dev: true - /commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} + /commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} dev: false /commander@2.20.3: @@ -24931,8 +25020,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - /content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + /content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} /conventional-changelog-angular@7.0.0: @@ -25060,7 +25149,6 @@ packages: /cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - dev: false /copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} @@ -25923,8 +26011,8 @@ packages: bundle-name: 4.1.0 default-browser-id: 5.0.0 - /default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + /default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} engines: {node: '>=18'} dependencies: bundle-name: 4.1.0 @@ -26213,7 +26301,7 @@ packages: resolution: {integrity: sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==} engines: {node: '>= 6.0.0'} dependencies: - yaml: 2.8.1 + yaml: 2.8.2 dev: false /docker-modem@5.0.6: @@ -26736,8 +26824,8 @@ packages: has-tostringtag: 1.0.2 hasown: 2.0.2 - /es-toolkit@1.46.1: - resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + /es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} dev: false /es6-error@4.1.1: @@ -26957,7 +27045,7 @@ packages: eslint: 9.33.0 eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 - minimatch: 10.0.3 + minimatch: 10.1.2 semver: 7.7.2 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 @@ -27008,7 +27096,7 @@ packages: peerDependencies: eslint: '>=9.29.0' dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@eslint-community/eslint-utils': 4.9.0(eslint@9.33.0) '@eslint/plugin-kit': 0.3.5 change-case: 5.4.4 @@ -27217,11 +27305,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - /eventsource-parser@3.0.3: - resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} - engines: {node: '>=20.0.0'} - dev: false - /eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -27376,15 +27459,15 @@ packages: express: 5.1.0 dev: false - /express-rate-limit@8.6.1(express@5.2.1): - resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} + /express-rate-limit@8.6.2(express@5.2.1): + resolution: {integrity: sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' dependencies: debug: 4.4.3(supports-color@10.2.2) express: 5.2.1 - ip-address: 10.4.0 + ip-address: 10.5.0 transitivePeerDependencies: - supports-color @@ -27498,8 +27581,8 @@ packages: /exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} - /exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + /exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} dev: false /ext-list@2.2.2: @@ -27702,15 +27785,6 @@ packages: pend: 1.2.0 dev: true - /fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - dev: true - /fdir@6.5.0(picomatch@4.0.3): resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -28075,7 +28149,6 @@ packages: es-set-tostringtag: 2.1.0 hasown: 2.0.2 mime-types: 2.1.35 - dev: false /format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} @@ -28497,8 +28570,8 @@ packages: nypm: 0.6.1 pathe: 2.0.3 - /giget@3.2.0: - resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + /giget@3.3.1: + resolution: {integrity: sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==} hasBin: true dev: false @@ -28617,7 +28690,7 @@ packages: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.0.3 + minimatch: 10.1.2 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.0 @@ -28639,7 +28712,7 @@ packages: /glob@7.1.4: resolution: {integrity: sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -28664,7 +28737,7 @@ packages: /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -28676,6 +28749,7 @@ packages: /glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me dependencies: fs.realpath: 1.0.0 minimatch: 8.0.4 @@ -29297,8 +29371,8 @@ packages: resolution: {integrity: sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww==} engines: {node: '>=16.9.0'} - /hono@4.12.34: - resolution: {integrity: sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==} + /hono@4.13.2: + resolution: {integrity: sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==} engines: {node: '>=16.9.0'} /hono@4.9.1: @@ -29828,8 +29902,8 @@ packages: engines: {node: '>= 12'} dev: true - /ip-address@10.4.0: - resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} + /ip-address@10.5.0: + resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} engines: {node: '>= 12'} /ipaddr.js@1.9.1: @@ -30916,8 +30990,8 @@ packages: dependencies: argparse: 2.0.1 - /js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + /js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true dependencies: argparse: 2.0.1 @@ -31321,7 +31395,7 @@ packages: '@langchain/textsplitters': 0.1.0(@langchain/core@0.3.70) axios: 1.11.0 js-tiktoken: 1.0.21 - js-yaml: 4.1.1 + js-yaml: 4.2.0 jsonpointer: 5.0.1 langsmith: 0.3.58(openai@4.104.0) openapi-types: 12.1.3 @@ -33198,18 +33272,11 @@ packages: - bufferutil - utf-8-validate - /minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/brace-expansion': 5.0.0 - /minimatch@10.1.2: resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} engines: {node: 20 || >=22} dependencies: '@isaacs/brace-expansion': 5.0.1 - dev: false /minimatch@3.0.5: resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==} @@ -33611,7 +33678,7 @@ packages: array-differ: 3.0.0 array-union: 2.1.0 arrify: 2.0.1 - minimatch: 3.0.5 + minimatch: 3.1.2 dev: true /multiparty@4.2.3: @@ -34396,7 +34463,7 @@ packages: dependencies: boolbase: 1.0.0 - /nuxt@4.1.3(@biomejs/biome@1.9.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.2.1)(@vue/compiler-sfc@3.5.22)(eslint@9.33.0)(typescript@5.9.3)(vite@7.2.7): + /nuxt@4.1.3(@biomejs/biome@1.9.4)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.2.1)(@vue/compiler-sfc@3.5.22)(eslint@9.33.0)(typescript@5.9.3)(vite@7.2.7): resolution: {integrity: sha512-FPl+4HNIOTRYWQXtsZe5KJAr/eddFesuXABvcSTnFLYckIfnxcistwmbtPlkJhkW6vr/Jdhef5QqqYYkBsowGg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -34448,9 +34515,9 @@ packages: ofetch: 1.4.1 ohash: 2.0.11 on-change: 6.0.0 - oxc-minify: 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - oxc-parser: 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - oxc-transform: 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + oxc-minify: 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + oxc-parser: 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + oxc-transform: 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) oxc-walker: 0.5.2(oxc-parser@0.94.0) pathe: 2.0.3 perfect-debounce: 2.0.0 @@ -34556,7 +34623,7 @@ packages: '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.6 - axios: 1.11.0 + axios: 1.13.5 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -34621,7 +34688,7 @@ packages: '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.2 '@zkochan/js-yaml': 0.0.7 - axios: 1.11.0 + axios: 1.13.5 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -34648,7 +34715,7 @@ packages: tmp: 0.2.5 tsconfig-paths: 4.2.0 tslib: 2.8.1 - yaml: 2.8.1 + yaml: 2.8.2 yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: @@ -34926,7 +34993,7 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} dependencies: - default-browser: 5.5.0 + default-browser: 5.5.1 define-lazy-prop: 3.0.0 is-in-ssh: 1.0.0 is-inside-container: 1.0.0 @@ -35110,7 +35177,7 @@ packages: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} dev: true - /oxc-minify@0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + /oxc-minify@0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): resolution: {integrity: sha512-7+9iyxwpzfjuiEnSqNJYzTsC1Oud742PPkr/4S1bGY930U4tApdLEK8zmgbT57c1/56cfNOndqZaeQZiAfnJ5A==} engines: {node: ^20.19.0 || >=22.12.0} optionalDependencies: @@ -35126,7 +35193,7 @@ packages: '@oxc-minify/binding-linux-s390x-gnu': 0.94.0 '@oxc-minify/binding-linux-x64-gnu': 0.94.0 '@oxc-minify/binding-linux-x64-musl': 0.94.0 - '@oxc-minify/binding-wasm32-wasi': 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@oxc-minify/binding-wasm32-wasi': 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@oxc-minify/binding-win32-arm64-msvc': 0.94.0 '@oxc-minify/binding-win32-x64-msvc': 0.94.0 transitivePeerDependencies: @@ -35134,7 +35201,7 @@ packages: - '@emnapi/runtime' dev: false - /oxc-parser@0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + /oxc-parser@0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): resolution: {integrity: sha512-refms9HQoAlTYIazONYkuX5A3rFGPddbD6Otyc+A0/pj1WTttR8TsZRlMzQxCfhexxfrbinqd7ebkEoYNuCmLQ==} engines: {node: ^20.19.0 || >=22.12.0} dependencies: @@ -35152,7 +35219,7 @@ packages: '@oxc-parser/binding-linux-s390x-gnu': 0.94.0 '@oxc-parser/binding-linux-x64-gnu': 0.94.0 '@oxc-parser/binding-linux-x64-musl': 0.94.0 - '@oxc-parser/binding-wasm32-wasi': 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@oxc-parser/binding-wasm32-wasi': 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@oxc-parser/binding-win32-arm64-msvc': 0.94.0 '@oxc-parser/binding-win32-x64-msvc': 0.94.0 transitivePeerDependencies: @@ -35160,7 +35227,7 @@ packages: - '@emnapi/runtime' dev: false - /oxc-transform@0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): + /oxc-transform@0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): resolution: {integrity: sha512-nHFFyPVWNNe7WLsAiQ6iwfsuTW/1esT+BJg+9rlvcSa0mfcZTpNo3TlBfj9IerLdDmYHJnSYsx8jjFZhoGfZ1w==} engines: {node: ^20.19.0 || >=22.12.0} optionalDependencies: @@ -35176,7 +35243,7 @@ packages: '@oxc-transform/binding-linux-s390x-gnu': 0.94.0 '@oxc-transform/binding-linux-x64-gnu': 0.94.0 '@oxc-transform/binding-linux-x64-musl': 0.94.0 - '@oxc-transform/binding-wasm32-wasi': 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@oxc-transform/binding-wasm32-wasi': 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@oxc-transform/binding-win32-arm64-msvc': 0.94.0 '@oxc-transform/binding-win32-x64-msvc': 0.94.0 transitivePeerDependencies: @@ -35190,7 +35257,7 @@ packages: oxc-parser: '>=0.72.0' dependencies: magic-regexp: 0.10.0 - oxc-parser: 0.94.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + oxc-parser: 0.94.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) dev: false /p-cancelable@1.1.0: @@ -36770,16 +36837,6 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 - /raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 - unpipe: 1.0.0 - dev: false - /raw-body@3.0.1: resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} engines: {node: '>= 0.10'} @@ -37221,8 +37278,8 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - /readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + /readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} dev: false @@ -38079,8 +38136,8 @@ packages: hasBin: true requiresBuild: true - /semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + /semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true dev: false @@ -40212,7 +40269,7 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} dependencies: - content-type: 2.0.0 + content-type: 2.1.0 media-typer: 1.1.0 mime-types: 3.0.1 @@ -40766,7 +40823,7 @@ packages: unplugin: 2.3.10 unplugin-utils: 0.2.5 vue-router: 4.5.1(vue@3.5.22) - yaml: 2.8.1 + yaml: 2.8.2 transitivePeerDependencies: - typescript - vue @@ -42583,7 +42640,6 @@ packages: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true - dev: false /yargs-parser@20.2.4: resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} @@ -42694,7 +42750,7 @@ packages: '@poppinss/colors': 4.1.5 '@poppinss/dumper': 0.6.4 '@speed-highlight/core': 1.2.7 - cookie: 1.0.2 + cookie: 1.1.1 youch-core: 0.3.3 /zhead@2.2.4: