Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,43 @@ jobs:

- name: Run tests
run: pytest -q

typescript-sdk:
runs-on: ubuntu-latest

defaults:
run:
working-directory: sdk/typescript

steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: sdk/typescript/package-lock.json

- name: Install SDK dependencies
run: npm ci

- name: Build and test SDK
run: npm test

provider-contracts:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Install official provider SDKs
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev,providers]"

- name: Run official provider contract tests
run: pytest -q tests/test_official_provider_contracts.py
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __pycache__/
.venv/
venv/
env/
node_modules/

build/
dist/
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ bench = [
langchain = [
"langchain-classic>=1.0",
]
providers = [
"anthropic>=0.120,<1",
"langgraph>=1.2,<2",
"openai-agents>=0.19,<1",
]
production = [
"annoy>=1.17",
"faiss-cpu>=1.8",
Expand Down
15 changes: 15 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# WaveMind TypeScript HTTP SDK

```ts
import { WaveMindClient } from "@wavemind/http";

const memory = new WaveMindClient({ baseUrl: "http://localhost:8000" });
await memory.remember({ text: "The deployment uses a canary.", namespace: "agent" });
const packet = await memory.compileExperiencePacket({
query: "How should I deploy?",
namespace: "agent",
});
```

The client has no runtime dependencies and works with Node.js 18+ or modern
browsers that provide `fetch`.
33 changes: 33 additions & 0 deletions sdk/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions sdk/typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@wavemind/http",
"version": "0.1.0",
"description": "Typed TypeScript client for the WaveMind HTTP API",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && node --test test/*.test.mjs"
},
"engines": {
"node": ">=18"
},
"license": "MIT",
"devDependencies": {
"typescript": "5.9.3"
}
}
187 changes: 187 additions & 0 deletions sdk/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
export interface WaveMindClientOptions {
baseUrl: string;
apiKey?: string;
fetch?: typeof globalThis.fetch;
}

export interface RememberInput {
text: string;
namespace?: string;
tags?: string[];
ttl_seconds?: number;
metadata?: Record<string, unknown>;
priority?: number;
}

export interface QueryInput {
text: string;
namespace?: string;
top_k?: number;
tags?: string[];
min_score?: number;
}

export interface QueryResult {
id: number;
text: string;
score: number;
vector_score: number;
field_score: number;
graph_score: number;
namespace: string;
tags: string[];
metadata: Record<string, unknown>;
}

export interface ExperiencePacketInput {
query: string;
namespace?: string;
token_budget?: number;
top_k?: number;
domains?: string[];
task_types?: string[];
tools?: string[];
include_canary?: boolean;
}

export interface ExperiencePacketItem {
experience_id: string;
version: number;
kind: string;
title: string;
excerpt: string;
score: number;
signals: Record<string, number>;
citation: string;
detail_ref: string;
provenance: Record<string, unknown>;
estimated_tokens: number;
canary: boolean;
}

export interface ExperiencePacket {
schema: "wavemind.experience_packet.v1";
namespace: string;
query: string;
token_budget: number;
estimated_tokens: number;
items: ExperiencePacketItem[];
omitted_count: number;
generated_at: number;
compiler_policy: Record<string, unknown>;
citations: string[];
}

export interface TrajectoryInput {
payload: unknown;
provider?: "openai" | "anthropic" | "mcp" | "generic";
namespace?: string;
trajectory_id?: string;
trust?: string;
status?: string;
confidence?: number;
}

export class WaveMindHTTPError extends Error {
readonly status: number;
readonly body: unknown;

constructor(status: number, body: unknown) {
super(`WaveMind HTTP request failed with status ${status}`);
this.name = "WaveMindHTTPError";
this.status = status;
this.body = body;
}
}

export class WaveMindClient {
readonly baseUrl: string;
readonly apiKey?: string;
private readonly fetchImpl: typeof globalThis.fetch;

constructor(options: WaveMindClientOptions) {
const baseUrl = options.baseUrl.trim().replace(/\/+$/, "");
if (!baseUrl) {
throw new Error("baseUrl must not be empty");
}
this.baseUrl = baseUrl;
if (options.apiKey !== undefined) {
this.apiKey = options.apiKey;
}
this.fetchImpl = options.fetch ?? globalThis.fetch;
if (!this.fetchImpl) {
throw new Error("A fetch implementation is required");
}
}

remember(input: RememberInput): Promise<{ id: number }> {
return this.request("POST", "/remember", input);
}

query(input: QueryInput): Promise<{ results: QueryResult[] }> {
return this.request("POST", "/query", input);
}

compileExperiencePacket(
input: ExperiencePacketInput,
): Promise<ExperiencePacket> {
return this.request("POST", "/experience/packet", input);
}

getExperience(
experienceId: string,
namespace = "default",
): Promise<Record<string, unknown>> {
const path =
`/experience/${encodeURIComponent(experienceId)}` +
`?namespace=${encodeURIComponent(namespace)}`;
return this.request("GET", path);
}

ingestTrajectory(
input: TrajectoryInput,
): Promise<Record<string, unknown>> {
return this.request("POST", "/experience/trajectories", input);
}

exportExperienceBundle(
namespace?: string,
): Promise<Record<string, unknown>> {
return this.request("POST", "/experience/export", { namespace });
}

importExperienceBundle(
bundle: Record<string, unknown>,
): Promise<Record<string, unknown>> {
return this.request("POST", "/experience/import", { bundle });
}

private async request<T>(
method: string,
path: string,
body?: unknown,
): Promise<T> {
const headers: Record<string, string> = {
accept: "application/json",
};
if (body !== undefined) {
headers["content-type"] = "application/json";
}
if (this.apiKey) {
headers.authorization = `Bearer ${this.apiKey}`;
}
const init: RequestInit = { method, headers };
if (body !== undefined) {
init.body = JSON.stringify(body);
}
const response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
const contentType = response.headers.get("content-type") ?? "";
const payload = contentType.includes("application/json")
? await response.json()
: await response.text();
if (!response.ok) {
throw new WaveMindHTTPError(response.status, payload);
}
return payload as T;
}
}
Loading
Loading