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
18 changes: 18 additions & 0 deletions .changeset/sep-phase1-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@smooai/smooth-extension-sdk": minor
"@smooai/smooth-operator": patch
---

Add the SEP TypeScript extension SDK — Phase 1 (the tool path).

New published package `@smooai/smooth-extension-sdk`: build Smooth Extension Protocol
extensions in TypeScript. `defineExtension`/`defineTool` (zod v4 via `z.toJSONSchema`, with
raw JSON-Schema / TypeBox pass-through), a symmetric JSON-RPC 2.0 `Peer`, an ndjson stdio
transport (plus an in-memory `linkedPair`), `createTestHost` for driving an extension
in-process, and `runConformance` to replay the shared fixtures against a real extension
subprocess. Ships the `hello` demo extension (`hello.greet` — zod schema, streamed
`tool/update` progress, `$/cancel` cancellation). Wired into the TypeScript CI lane.

Extends `spec/extension/conformance/fixtures.json` for the tool path: `is_error` and
`details` tool results, a message-only `tool/update`, and invalid fixtures (missing
`content`, out-of-range `progress`).
16 changes: 8 additions & 8 deletions .github/workflows/typescript.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Typecheck + test the TypeScript trees: the published SDK (typescript/ — client,
# React bindings, web-component widget) AND the native TS WebSocket server
# (typescript/server/). Both are pnpm-workspace members; one root install links them.
# The default `vitest run` (the `test` script) excludes the live-gateway E2E, so this
# job needs no secrets. spec/ is a trigger path because the conformance test validates
# against spec/conformance/fixtures.json.
# React bindings, web-component widget), the native TS WebSocket server
# (typescript/server/), and the published SEP extension SDK (typescript/extension-sdk/).
# All are pnpm-workspace members; one root install links them. The default `vitest run`
# (the `test` script) excludes the live-gateway E2E, so this job needs no secrets. spec/
# is a trigger path because the conformance tests validate against spec/**/fixtures.json.
name: TypeScript

on:
Expand Down Expand Up @@ -45,10 +45,10 @@ jobs:
- name: Install (frozen lockfile)
run: pnpm install --frozen-lockfile

# Scope to the two TypeScript packages the lane owns; `console` (the private
# Scope to the TypeScript packages the lane owns; `console` (the private
# admin app) is a workspace member too but out of scope for this lane.
- name: Typecheck
run: pnpm --filter "@smooai/smooth-operator" --filter "@smooai/smooth-operator-server" typecheck
run: pnpm --filter "@smooai/smooth-operator" --filter "@smooai/smooth-operator-server" --filter "@smooai/smooth-extension-sdk" typecheck

- name: Test
run: pnpm --filter "@smooai/smooth-operator" --filter "@smooai/smooth-operator-server" test
run: pnpm --filter "@smooai/smooth-operator" --filter "@smooai/smooth-operator-server" --filter "@smooai/smooth-extension-sdk" test
30 changes: 30 additions & 0 deletions pnpm-lock.yaml

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

5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
# speaks the protocol and runs `@smooai/smooth-operator-core` per turn (the TS
# sibling of `rust/smooth-operator-server` and `dotnet/server`). Private (not
# published); the client in `typescript/` is the published SDK.
#
# `typescript/extension-sdk` is the published SEP (Smooth Extension Protocol) SDK
# — `defineExtension`/`defineTool`, a stdio JSON-RPC transport, an in-process test
# host, and a conformance runner for building extensions in TypeScript.
packages:
- typescript
- typescript/server
- typescript/extension-sdk
- console
28 changes: 28 additions & 0 deletions spec/extension/conformance/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,30 @@
"instance": { "content": "hello", "is_error": false }
},

"tool_execute_result_error": {
"$schema_ref": "methods/tool-execute.schema.json#/$defs/Result",
"description": "A failed tool execution — the host surfaces it as an error tool-result.",
"instance": { "content": "unknown tool: nope", "is_error": true }
},

"tool_execute_result_with_details": {
"$schema_ref": "methods/tool-execute.schema.json#/$defs/Result",
"description": "A tool result carrying structured `details` for UI rendering alongside the LLM-facing content.",
"instance": { "content": "3 files changed", "is_error": false, "details": { "files": ["a.ts", "b.ts", "c.ts"] } }
},

"tool_update_params": {
"$schema_ref": "methods/tool-update.schema.json#/$defs/Params",
"description": "Progress notification for an in-flight tool/execute.",
"instance": { "call_id": "call-1", "message": "working...", "progress": 0.5 }
},

"tool_update_params_message_only": {
"$schema_ref": "methods/tool-update.schema.json#/$defs/Params",
"description": "A progress notification with only a message (no fractional progress) — the minimal tool/update.",
"instance": { "call_id": "call-1", "message": "started" }
},

"cancel_params": {
"$schema_ref": "methods/cancel.schema.json#/$defs/Params",
"description": "Cancellation of an in-flight request by numeric id.",
Expand Down Expand Up @@ -339,6 +357,16 @@
"context": { "token": "epoch-7", "tier": "command" }
}
},
{
"name": "tool_execute_result_missing_content",
"$schema_ref": "methods/tool-execute.schema.json#/$defs/Result",
"instance": { "is_error": true }
},
{
"name": "tool_update_progress_out_of_range",
"$schema_ref": "methods/tool-update.schema.json#/$defs/Params",
"instance": { "call_id": "call-1", "progress": 1.5 }
},
{
"name": "frame_wrong_jsonrpc_version",
"$schema_ref": "methods/envelope.schema.json#/$defs/Request",
Expand Down
94 changes: 94 additions & 0 deletions typescript/extension-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# @smooai/smooth-extension-sdk

Build **SEP** (Smooth Extension Protocol) extensions in TypeScript.

An extension is a long-lived subprocess that speaks JSON-RPC 2.0 over ndjson on
its stdin/stdout to a SEP host (`smooth-operator-core` and its polyglot servers).
This SDK is the DX centerpiece: describe your extension declaratively, `serve()`
it, test it in-process, and gate it against the shared conformance fixtures.

## Quick start

```ts
import { z } from 'zod';
import { defineExtension, defineTool } from '@smooai/smooth-extension-sdk';

export const hello = defineExtension((smooth) => {
smooth.name = 'hello';
smooth.version = '0.1.0';

smooth.registerTool(
defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: z.object({ name: z.string() }),
async execute(args, ctx) {
ctx.onUpdate({ message: `greeting ${args.name}`, progress: 0.5 });
return { content: `Hello, ${args.name}!` };
},
}),
);
});

hello.serve(); // wire to stdin/stdout and run
```

The host exposes the tool to the LLM as `hello.greet`.

## Schemas

`parameters` accepts three shapes — the wire truth is always JSON Schema:

- a **zod v4** schema → converted with `z.toJSONSchema()`
- a **TypeBox** schema → TypeBox schemas already ARE JSON Schema, passed through
- a **raw JSON Schema** object → passed through unchanged

## Tool context

`execute(args, ctx)` receives a `ctx` with:

- `ctx.onUpdate({ message?, progress?, details? })` — stream `tool/update` progress
- `ctx.signal` — an `AbortSignal` that fires when the host sends `$/cancel`
- `ctx.callId` / `ctx.context` — the call id and dispatch context (epoch token + tier)

Return a `{ content, is_error?, details? }` result, or just a string shorthand for
`{ content }`.

## Testing

```ts
import { createTestHost } from '@smooai/smooth-extension-sdk';
import { hello } from './hello.js';

const host = createTestHost(hello); // in-process, no subprocess
await host.initialize();
const res = await host.callTool('greet', { name: 'Ada' });
// res === { content: 'Hello, Ada!' }
host.close();
```

`runConformance` replays the shared SEP fixtures against a **real** extension
subprocess, validating every reply against its schema:

```ts
import { runConformance } from '@smooai/smooth-extension-sdk';

const report = await runConformance({ command: 'node', args: ['./hello.js'] });
// report.passed === true
```

## API

- `defineExtension((smooth) => void)` — set `smooth.name`/`version`, `registerTool`, `on(event)`, `log`.
- `defineTool({ name, description, parameters, deferred?, execute })`
- `createTestHost(extension)` → `{ initialize, callTool, ping, sendEvent, shutdown, close }`
- `runConformance({ command, args?, env?, cwd?, specDir? })` → `ConformanceReport`
- `Peer`, `stdioTransport`, `linkedPair`, `toJsonSchema` — the building blocks
- `PROTOCOL_VERSION`, `method`, `errorCode` and the wire types

## Scope (Phase 1)

The tool path: registration, execute, streamed progress, cancellation, plus
observe `on(event)` subscriptions and lifecycle (`initialize`/`ping`/`shutdown`).
Hooks, commands, ui/kv/session/exec land in later phases — the wire and API were
shaped to grow into them without breaking the tool path.
33 changes: 33 additions & 0 deletions typescript/extension-sdk/examples/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* `hello` — the Phase 1 demo extension. One tool, `hello.greet`, exercising the
* whole tool path: a zod-typed schema, streamed progress, and cancellation.
*
* Run it as a real SEP subprocess: `tsx examples/hello.ts`
* The host handshakes, then dispatches `hello.greet` like any native tool.
*/
import { z } from 'zod';
import { defineExtension, defineTool } from '../src/index.js';

export const hello = defineExtension((smooth) => {
smooth.name = 'hello';
smooth.version = '0.1.0';

smooth.registerTool(
defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: z.object({ name: z.string().describe('Who to greet.') }),
async execute(args, ctx) {
ctx.onUpdate({ message: `greeting ${args.name}`, progress: 0.5 });
return { content: `Hello, ${args.name}!` };
},
}),
);

smooth.on('turn_start', () => smooth.log('info', 'a turn started'));
});

// When run directly (not imported by a test), serve over stdio.
if (import.meta.url === `file://${process.argv[1]}`) {
hello.serve();
}
58 changes: 58 additions & 0 deletions typescript/extension-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@smooai/smooth-extension-sdk",
"version": "0.1.0",
"description": "TypeScript SDK for building Smooth Extension Protocol (SEP) extensions: `defineExtension`, `defineTool`, a stdio JSON-RPC transport, an in-process test host, and a conformance runner. Extensions are subprocesses speaking JSON-RPC 2.0 ndjson to any SEP host (smooth-operator-core and its polyglot servers).",
"license": "MIT",
"type": "module",
"engines": {
"node": ">=22"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/SmooAI/smooth-operator.git",
"directory": "typescript/extension-sdk"
},
"homepage": "https://github.com/SmooAI/smooth-operator/tree/main/typescript/extension-sdk",
"keywords": [
"smooth-operator",
"sep",
"extension",
"json-rpc",
"agent",
"tools",
"sdk"
],
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"zod": "^4.0.0"
},
"devDependencies": {
"@types/node": "^25.9.2",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
}
}
Loading
Loading