Skip to content
Draft
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
60 changes: 60 additions & 0 deletions .github/changelog-config-typescript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"categories": [
{
"title": "### Upgrades",
"labels": [
"breaking-change",
"upgrade"
]
},
{
"title": "### Features",
"labels": [
"feature"
]
},
{
"title": "### Bug fixes",
"labels": [
"fix"
]
},
{
"title": "### Performance",
"labels": [
"performance"
]
},
{
"title": "### Maintenance",
"labels": [
"maintenance"
]
},
{
"title": "### Docs",
"labels": [
"docs"
]
},
{
"title": "### Tests",
"labels": [
"test"
]
}
],
"ignore_labels": [
"release"
],
"tag_resolver": {
"method": "semver",
"filter": {
"pattern": "typescript\\/v(.+)",
"flags": "gu"
}
},
"template": "## Fraise TypeScript SDK #{{TO_TAG}}\n\n#{{CHANGELOG}}\n### Other changes\n\n#{{UNCATEGORIZED}}\n\n**Full changelog**: https://github.com/RonsenbergVI/fraise/compare/#{{FROM_TAG}}...#{{TO_TAG}}",
"pr_template": "- #{{TITLE}} (##{{NUMBER}})",
"empty_template": "_No changes since #{{FROM_TAG}}._"
}
105 changes: 105 additions & 0 deletions .github/workflows/typescript.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: TypeScript SDK

on:
push:
branches: [main]
tags:
- "typescript/v[0-9]+.[0-9]+.[0-9]+"
- "typescript/v[0-9]+.[0-9]+.[0-9]+-*" # pre-releases: -alpha.N / -beta.N / -rc.N
pull_request:
paths:
- "sdk/typescript/**"
- ".github/workflows/typescript.yaml"

permissions:
contents: read

jobs:

lint:
name: Lint TypeScript SDK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v7
with:
version-file: "pyproject.toml"
- name: Lint
run: make lint-ts

test:
name: Test Typescript SDK
runs-on: ubuntu-latest
needs: lint
defaults:
run:
working-directory: sdk/typescript
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: sdk/typescript/package-lock.json

- name: Build
run: make build-ts

- name: Test
run: make test-ts

publish:
name: Publish to npm
if: startsWith(github.ref, 'refs/tags/typescript/v')
needs: test
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # OIDC for npm trusted publishing + provenance
defaults:
run:
working-directory: sdk/typescript
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: "https://registry.npmjs.org"

# Trusted publishing + --provenance need npm >= 11.5.1, newer than the
# npm bundled with Node 22.
- name: Upgrade npm
run: npm install -g npm@latest

- name: Version must match tag
id: ver
run: |
# git tag pre-release spelling maps 1:1 to npm/semver, no normalisation.
TAG_VERSION="${GITHUB_REF_NAME#typescript/v}"
if [[ ! "$TAG_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[0-9]+)?$ ]]; then
echo "::error::bad tag '$GITHUB_REF_NAME' — expected typescript/vX.Y.Z or -alpha.N/-beta.N/-rc.N"; exit 1
fi
PKG_VERSION=$(node -p "require('./package.json').version")
[ "$TAG_VERSION" = "$PKG_VERSION" ] || { echo "::error::tag $TAG_VERSION != package.json $PKG_VERSION"; exit 1; }
# Route pre-releases to their own dist-tag so they never become `latest`.
case "$TAG_VERSION" in
*-alpha.*) DIST_TAG=alpha ;;
*-beta.*) DIST_TAG=beta ;;
*-rc.*) DIST_TAG=rc ;;
*) DIST_TAG=latest ;;
esac
echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT"

- name: Build
run: |
pnpm install --frozen-lockfile
pnpm run build

# No NODE_AUTH_TOKEN: auth comes from the OIDC id-token via the trusted
# publisher configured for this package on npmjs.com.
- name: Publish to npm
run: npm publish --provenance --access public --tag "${{ steps.ver.outputs.dist_tag }}"
23 changes: 23 additions & 0 deletions Dockerfile.typescript
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# TypeScript SDK integration tests: builds the SDK, then runs vitest against the
# `fraise` service over the compose network. The test project depends on the SDK
# via a file: reference (../../../sdk/typescript), so the repo layout is kept.
FROM node:22-slim

# CI=true keeps pnpm non-interactive (no "reinstall node_modules?" prompt that
# would hang the build) and silences the update notifier.
ENV CI=true

RUN corepack enable

WORKDIR /app

# Build the SDK first so the file: dependency resolves to its dist/ output.
COPY sdk/typescript/ ./sdk/typescript/
RUN cd sdk/typescript && pnpm install --frozen-lockfile && pnpm run build

COPY tests/integration/typescript/ ./tests/integration/typescript/
# No committed lockfile for the throwaway test project, so opt out of the
# frozen default that CI=true turns on.
RUN cd tests/integration/typescript && pnpm install --no-frozen-lockfile

# Runner command lives in docker-compose.tests.yaml (typescript-sdk-integration-tests service).
File renamed without changes.
23 changes: 23 additions & 0 deletions examples/openai-agents-ts/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Build context is the repository root (see docker-compose.yaml) so this image
# can build and install the local, unpublished fraise-sdk alongside the example.
FROM node:22-slim

# pnpm via corepack (the SDK pins its own pnpm version through packageManager).
RUN corepack enable

# 1. Build the local TypeScript SDK and pack it to a tarball (contains dist/
# only, per the SDK's "files" field).
WORKDIR /sdk
COPY sdk/typescript/ ./
RUN pnpm install --frozen-lockfile && pnpm run build && pnpm pack --pack-destination /pkg

# 2. Install the example's own dependencies, then add the packed SDK tarball.
WORKDIR /app
COPY examples/openai-agents-ts/package.json ./
RUN pnpm install && pnpm add /pkg/fraise-sdk-*.tgz

# 3. The example source. Copied last so edits don't bust the dependency layers.
# Run via Node's built-in TypeScript type-stripping — no bundler/tsx needed.
COPY examples/openai-agents-ts/agent.ts ./

CMD ["node", "--experimental-strip-types", "agent.ts"]
24 changes: 24 additions & 0 deletions examples/openai-agents-ts/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Ignore-file for this example's image build (build context is the repo root).
# BuildKit uses this in place of the repo-root .dockerignore. Keep sdk/typescript
# (its sources + lockfile) and this example; drop everything else — and always
# drop any host-built node_modules/dist so the image builds them fresh.
.git
.claude
**/node_modules
**/dist
internal
pkg
cmd
go.mod
go.sum
coverage.out
coverage.html
coverage.txt
docs
assets
tests
sdk/python
examples/openai-agents
examples/claude-agent-sdk
Dockerfile*
docker-compose*
46 changes: 46 additions & 0 deletions examples/openai-agents-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# OpenAI Agents (TypeScript) + Fraise memory

An [OpenAI Agents SDK](https://github.com/openai/openai-agents-js) agent (in
TypeScript) that stores and recalls facts through a Fraise server, using the
built-in memory tools from `fraise-sdk/integrations/openai-agents`.

The demo runs two turns as **separate agent runs with no shared history**, so the
second turn (`What is my favourite colour?`) can only succeed by recalling what
the first turn remembered. The tools are wired with an `OpenAIEmbedder`, so
memory **vectorises implicitly** — each fact is stored with its embedding and
recall searches by vector too.

## Run

Everything runs in Docker — the compose file builds and starts Fraise too, and
builds the local TypeScript SDK into the agent image:

```bash
export OPENAI_API_KEY=sk-...
docker compose run --rm agent
```

The `agent` service waits for Fraise's health check, then runs
[`agent.ts`](agent.ts) directly via Node's built-in TypeScript type-stripping
(`node --experimental-strip-types`). Tear down with `docker compose down`.

## What's here

- [`agent.ts`](agent.ts) — the agent and its two-turn demo.
- [`Dockerfile`](Dockerfile) — builds the local `fraise-sdk`, installs it plus `@openai/agents`, runs the script with Node's type-stripping.
- [`docker-compose.yaml`](docker-compose.yaml) — `fraise` + `agent` services on one network.
- [`fraise.config.toml`](fraise.config.toml) — Fraise server config mounted into the `fraise` service.

The Docker build context is the repository root so the image can build and
install the unpublished SDK from `sdk/typescript`.

## Running locally (without Docker)

Build the SDK, link it in, then start the agent against a running Fraise server:

```bash
(cd ../../sdk/typescript && pnpm install && pnpm run build)
pnpm install
pnpm add ../../sdk/typescript # link the freshly built SDK
FRAISE_URL=http://localhost:9876 OPENAI_API_KEY=sk-... pnpm start
```
77 changes: 77 additions & 0 deletions examples/openai-agents-ts/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
MIT License

Copyright (c) 2026 René-Jean Corneille

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
* An OpenAI Agents (TypeScript) agent that uses Fraise for long-term memory.
*
* Two turns run as separate agent runs with no shared history, so the second
* turn can only answer by recalling what the first turn stored in Fraise.
*
* Environment:
* FRAISE_URL base URL of the Fraise server (default http://localhost:9876)
* OPENAI_API_KEY required by the OpenAI Agents SDK
*
* Run it with Docker (brings up Fraise too):
* OPENAI_API_KEY=sk-... docker compose run --rm agent
*/

import { Agent, run } from "@openai/agents";
import { FraiseClient } from "fraise-sdk";
import { memoryTools } from "fraise-sdk/integrations/openai-agents";
import { OpenAIEmbedder } from "fraise-sdk/providers";

async function main(): Promise<void> {
const fraise = new FraiseClient({
baseUrl: process.env["FRAISE_URL"] ?? "http://localhost:9876",
});

// Passing an embedder makes the memory tools vectorise implicitly: remember
// stores each fact with its embedding and recall searches by vector too.
// Drop the `embedder` option for plain keyword memory.
const embedder = new OpenAIEmbedder({ dimensions: 256 });

const agent = new Agent({
name: "Assistant",
instructions:
"You have a long-term memory. When the user shares a durable fact about themselves, " +
"store it with the remember tool. When answering a question, first recall relevant " +
"facts from memory.",
model: "gpt-5-nano",
tools: memoryTools(fraise, { embedder }),
});

console.log("turn 1 > My favourite colour is orange.");
const first = await run(agent, "My favourite colour is orange.");
console.log("assistant:", first.finalOutput);

// Fresh run — no chat history carried over — so memory is the only source.
console.log("\nturn 2 > What is my favourite colour?");
const second = await run(agent, "What is my favourite colour?");
console.log("assistant:", second.finalOutput);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Loading
Loading